diff options
| author | Oskar <oskar@mullvad.net> | 2025-02-10 09:28:35 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-02-11 15:37:22 +0100 |
| commit | e7e7252fc5e765bf971ff502a4f81b1cb20c92ad (patch) | |
| tree | b7381005a33b7a43214fce4572262629141a6521 /desktop | |
| parent | e93aba7e9d8c7210aac9b6d09c0067823bffc7e9 (diff) | |
| download | mullvadvpn-e7e7252fc5e765bf971ff502a4f81b1cb20c92ad.tar.xz mullvadvpn-e7e7252fc5e765bf971ff502a4f81b1cb20c92ad.zip | |
Implement shortcut parsing
Co-authored-by: David Lönnhager <david.l@mullvad.net>
Co-authored-by: Markus Pettersson <markus.pettersson@mullvad.net>
Co-authored-by: Joakim Hulthe <joakim.hulthe@mullvad.net>
Diffstat (limited to 'desktop')
| -rw-r--r-- | desktop/package-lock.json | 21 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/gulpfile.js | 12 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/package.json | 1 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/main/index.ts | 7 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts | 47 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/tasks/distribution.js | 5 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/tasks/scripts.js | 6 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/.gitignore | 9 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/Cargo.toml | 21 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/README.md | 19 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/eslint.config.mjs | 3 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/package.json | 36 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/src/index.cts | 18 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/src/index.mts | 3 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/src/load.cts | 11 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/tsconfig.json | 9 | ||||
| -rw-r--r-- | desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs | 191 |
17 files changed, 404 insertions, 15 deletions
diff --git a/desktop/package-lock.json b/desktop/package-lock.json index fc1c088646..de1e261588 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -14219,6 +14219,10 @@ "string-width": "^1.0.2 || 2" } }, + "node_modules/win-shortcuts": { + "resolved": "packages/win-shortcuts", + "link": true + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -14669,7 +14673,8 @@ "redux": "^4.2.0", "simple-plist": "^1.3.1", "sprintf-js": "^1.1.2", - "styled-components": "^6.1.13" + "styled-components": "^6.1.13", + "win-shortcuts": "0.0.0" }, "devDependencies": { "@playwright/test": "^1.41.1", @@ -14720,6 +14725,13 @@ "dependencies": { "@neon-rs/load": "^0.1.73" } + }, + "packages/win-shortcuts": { + "version": "0.0.0", + "license": "GPL-3.0", + "dependencies": { + "@neon-rs/load": "^0.1.73" + } } }, "dependencies": { @@ -22607,6 +22619,7 @@ "tsc-watch": "^5.0.3", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", + "win-shortcuts": "0.0.0", "xvfb-maybe": "^0.2.1" } }, @@ -25870,6 +25883,12 @@ "string-width": "^1.0.2 || 2" } }, + "win-shortcuts": { + "version": "file:packages/win-shortcuts", + "requires": { + "@neon-rs/load": "^0.1.73" + } + }, "word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/desktop/packages/mullvad-vpn/gulpfile.js b/desktop/packages/mullvad-vpn/gulpfile.js index 02375032f4..529b05f9c4 100644 --- a/desktop/packages/mullvad-vpn/gulpfile.js +++ b/desktop/packages/mullvad-vpn/gulpfile.js @@ -18,8 +18,18 @@ task('set-prod-env', function (done) { task('clean', function (done) { fs.rm('./build', { recursive: true, force: true }, done); }); +task('build-proto', scripts.buildProto); +task( + 'develop', + series( + 'clean', + 'set-dev-env', + scripts.buildNseventforwarder, + scripts.buildWinShortcuts, + watch.start, + ), +); task('build', series('clean', 'set-prod-env', assets.copyAll, scripts.build)); -task('develop', series('clean', 'set-dev-env', scripts.buildNseventforwarder, watch.start)); task('pack-win', series('build', dist.packWin)); task('pack-linux', series('build', dist.packLinux)); task('pack-mac', series('build', dist.packMac)); diff --git a/desktop/packages/mullvad-vpn/package.json b/desktop/packages/mullvad-vpn/package.json index 056b0be93f..0963f79914 100644 --- a/desktop/packages/mullvad-vpn/package.json +++ b/desktop/packages/mullvad-vpn/package.json @@ -25,6 +25,7 @@ "react-redux": "^7.2.9", "react-router": "^5.3.4", "redux": "^4.2.0", + "win-shortcuts": "0.0.0", "simple-plist": "^1.3.1", "sprintf-js": "^1.1.2", "styled-components": "^6.1.13" diff --git a/desktop/packages/mullvad-vpn/src/main/index.ts b/desktop/packages/mullvad-vpn/src/main/index.ts index 2186168379..a80241f527 100644 --- a/desktop/packages/mullvad-vpn/src/main/index.ts +++ b/desktop/packages/mullvad-vpn/src/main/index.ts @@ -829,7 +829,12 @@ class ApplicationMain // If the applications is a string (path) it's an application picked with the file picker // that we want to add to the list of additional applications. if (typeof application === 'string') { - const executablePath = await splitTunneling!.resolveExecutablePath(application); + let executablePath; + try { + executablePath = await splitTunneling!.resolveExecutablePath(application); + } catch { + return; + } this.settings.gui.addBrowsedForSplitTunnelingApplications(executablePath); await splitTunneling!.addApplicationPathToCache(application); await this.daemonRpc.addSplitTunnelingApplication(executablePath); diff --git a/desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts b/desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts index 7ae73827d0..4f1df5660a 100644 --- a/desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts +++ b/desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts @@ -1,7 +1,10 @@ -import { app, shell } from 'electron'; +import { app } from 'electron'; import fs from 'fs'; import path from 'path'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { readShortcut } = require('win-shortcuts'); + import { ISplitTunnelingApplication, ISplitTunnelingAppListRetriever, @@ -114,7 +117,12 @@ export class WindowsSplitTunnelingAppListRetriever implements ISplitTunnelingApp public resolveExecutablePath(providedPath: string): Promise<string> { if (path.extname(providedPath) === '.lnk') { - return Promise.resolve(shell.readShortcutLink(path.resolve(providedPath)).target); + const target = this.tryReadShortcut(path.resolve(providedPath)); + if (target) { + return Promise.resolve(target); + } else { + return Promise.reject('Failed to resolve shortcut'); + } } return Promise.resolve(providedPath); @@ -124,12 +132,14 @@ export class WindowsSplitTunnelingAppListRetriever implements ISplitTunnelingApp public async addApplicationPathToCache(applicationPath: string): Promise<void> { const parsedPath = path.parse(applicationPath); if (parsedPath.ext === '.lnk') { - const shortcutDetiails = shell.readShortcutLink(path.resolve(applicationPath)); - this.additionalShortcuts.push({ - ...shortcutDetiails, - name: path.parse(applicationPath).name, - deletable: true, - }); + const target = this.tryReadShortcut(path.resolve(applicationPath)); + if (target) { + this.additionalShortcuts.push({ + target, + name: path.parse(applicationPath).name, + deletable: true, + }); + } } else { await this.addApplicationToAdditionalShortcuts(applicationPath); } @@ -216,14 +226,14 @@ export class WindowsSplitTunnelingAppListRetriever implements ISplitTunnelingApp private resolveLinks(linkPaths: string[]): ShortcutDetails[] { return linkPaths .map((link) => { - try { + const target = this.tryReadShortcut(path.resolve(link)); + if (target) { return { - ...shell.readShortcutLink(path.resolve(link)), + target, name: path.parse(link).name, }; - } catch { - return null; } + return null; }) .filter( (shortcut): shortcut is ShortcutDetails => @@ -677,4 +687,17 @@ export class WindowsSplitTunnelingAppListRetriever implements ISplitTunnelingApp private alignDword(offset: number): number { return Math.ceil(offset / 4) * 4; } + + private tryReadShortcut(appPath: string): string | null { + try { + return readShortcut(path.resolve(appPath)); + } catch (e) { + if (e && typeof e === 'object' && 'message' in e) { + log.error(`Failed to read .lnk shortcut for ${appPath}.`, e.message); + } else { + log.error(`Failed to read .lnk shortcut for ${appPath}.`); + } + return null; + } + } } diff --git a/desktop/packages/mullvad-vpn/tasks/distribution.js b/desktop/packages/mullvad-vpn/tasks/distribution.js index 8ab8a35882..9965db3ccb 100644 --- a/desktop/packages/mullvad-vpn/tasks/distribution.js +++ b/desktop/packages/mullvad-vpn/tasks/distribution.js @@ -62,6 +62,7 @@ function newConfig() { '!node_modules/grpc-tools', '!node_modules/@types', '!node_modules/nseventforwarder/debug', + '!node_modules/win-shortcuts/debug', ], // Make sure that all files declared in "extraResources" exists and abort if they don't. @@ -300,12 +301,16 @@ async function packWin() { process.env.SETUP_SUBDIR = '.'; process.env.TARGET_SUBDIR = 'x86_64-pc-windows-msvc'; process.env.DIST_SUBDIR = ''; + + execFileSync('npm', ['-w', 'win-shortcuts', 'run', 'build-x86']); break; case 'arm64': process.env.TARGET_TRIPLE = 'aarch64-pc-windows-msvc'; process.env.SETUP_SUBDIR = 'aarch64-pc-windows-msvc'; process.env.TARGET_SUBDIR = 'aarch64-pc-windows-msvc'; process.env.DIST_SUBDIR = 'aarch64-pc-windows-msvc'; + + execFileSync('npm', ['-w', 'win-shortcuts', 'run', 'build-arm']); break; default: throw new Error('Invalid or unknown target (only one may be specified)'); diff --git a/desktop/packages/mullvad-vpn/tasks/scripts.js b/desktop/packages/mullvad-vpn/tasks/scripts.js index 066ad60c6b..baa89736d2 100644 --- a/desktop/packages/mullvad-vpn/tasks/scripts.js +++ b/desktop/packages/mullvad-vpn/tasks/scripts.js @@ -117,12 +117,18 @@ function buildNseventforwarder(callback) { } } +function buildWinShortcuts(callback) { + exec('npm -w win-shortcuts run build-debug', (err) => callback(err)); +} + compileScripts.displayName = 'compile-scripts'; buildNseventforwarder.displayName = 'build-nseventforwarder'; +buildWinShortcuts.displayName = 'build-win-shortcuts'; exports.build = series( compileScripts, parallel(makeBrowserifyPreload(false), makeBrowserifyRenderer(false)), ); exports.buildNseventforwarder = buildNseventforwarder; +exports.buildWinShortcuts = buildWinShortcuts; exports.makeWatchCompiler = makeWatchCompiler; diff --git a/desktop/packages/win-shortcuts/.gitignore b/desktop/packages/win-shortcuts/.gitignore new file mode 100644 index 0000000000..ec4fda9fef --- /dev/null +++ b/desktop/packages/win-shortcuts/.gitignore @@ -0,0 +1,9 @@ +target +index.node +**/node_modules +**/.DS_Store +npm-debug.log* +lib +*.log +dist/ +debug/ diff --git a/desktop/packages/win-shortcuts/Cargo.toml b/desktop/packages/win-shortcuts/Cargo.toml new file mode 100644 index 0000000000..ed8f3f43af --- /dev/null +++ b/desktop/packages/win-shortcuts/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "win-shortcuts" +description = "" +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +exclude = ["index.node"] + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib"] +path = "win-shortcuts-rs/lib.rs" + +[target.'cfg(target_os = "windows")'.dependencies] +neon = "1" +windows = { version = "0.59.0", features = ["Win32", "Win32_UI", "Win32_UI_Shell", "Win32_System", "Win32_System_Com", "Win32_Storage_FileSystem"] } +thiserror = { workspace = true } diff --git a/desktop/packages/win-shortcuts/README.md b/desktop/packages/win-shortcuts/README.md new file mode 100644 index 0000000000..b837b8e6a6 --- /dev/null +++ b/desktop/packages/win-shortcuts/README.md @@ -0,0 +1,19 @@ +# win-shortcuts + +## Building win-shortcuts + +Building win-shortcuts requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support). + +To run the build, run: + +```sh +$ npm run build-debug +``` + +## Learn More + +Learn more about: + +- [Neon](https://neon-bindings.com). +- [Rust](https://www.rust-lang.org). +- [Node](https://nodejs.org). diff --git a/desktop/packages/win-shortcuts/eslint.config.mjs b/desktop/packages/win-shortcuts/eslint.config.mjs new file mode 100644 index 0000000000..9e16ad57ea --- /dev/null +++ b/desktop/packages/win-shortcuts/eslint.config.mjs @@ -0,0 +1,3 @@ +import workspaceConfig from '../../eslint.config.mjs'; + +export default [...workspaceConfig, { ignores: ['lib/'] }]; diff --git a/desktop/packages/win-shortcuts/package.json b/desktop/packages/win-shortcuts/package.json new file mode 100644 index 0000000000..245c9ef179 --- /dev/null +++ b/desktop/packages/win-shortcuts/package.json @@ -0,0 +1,36 @@ +{ + "name": "win-shortcuts", + "version": "0.0.0", + "author": "Mullvad VPN", + "license": "GPL-3.0", + "description": "", + "main": "./lib/index.cjs", + "scripts": { + "cargo-build": "tsc && cargo build", + "build-debug": "npm run cargo-build && (test -d debug || mkdir debug) && cp ../../../target/debug/win_shortcuts.dll debug/index.node", + "build-arm": "npm run cargo-build -- --release --target aarch64-pc-windows-msvc && (test -d dist || mkdir dist) && (test -d dist/win32-arm64-msvc || mkdir \"dist/win32-arm64-msvc\") && cp ../../../target/aarch64-pc-windows-msvc/release/win_shortcuts.dll dist/win32-arm64-msvc/index.node", + "build-x86": "npm run cargo-build -- --release --target x86_64-pc-windows-msvc && (test -d dist || mkdir dist) && (test -d dist/win32-x64-msvc || mkdir \"dist/win32-x64-msvc\") && cp ../../../target/x86_64-pc-windows-msvc/release/win_shortcuts.dll dist/win32-x64-msvc/index.node", + "clean": "rm -rf debug; rm -rf dist", + "lint": "eslint .", + "lint-fix": "eslint --fix ." + }, + "exports": { + ".": { + "import": { + "types": "./lib/index.d.mts", + "default": "./lib/index.mjs" + }, + "require": { + "types": "./lib/index.d.cts", + "default": "./lib/index.cjs" + } + } + }, + "types": "./lib/index.d.cts", + "files": [ + "lib/**/*.?({c,m}){t,j}s" + ], + "dependencies": { + "@neon-rs/load": "^0.1.73" + } +} diff --git a/desktop/packages/win-shortcuts/src/index.cts b/desktop/packages/win-shortcuts/src/index.cts new file mode 100644 index 0000000000..717968cda4 --- /dev/null +++ b/desktop/packages/win-shortcuts/src/index.cts @@ -0,0 +1,18 @@ +// This module is the CJS entry point for the library. + +// The Rust addon. +import * as addon from './load.cjs'; + +// Use this declaration to assign types to the addon's exports, +// which otherwise by default are `any`. +declare module './load.cjs' { + function readShortcut(linkPath: string): string | null; +} + +/** + * Return path for a shortcut. + * @param linkPath absolute path to a `.lnk`. + */ +export function readShortcut(linkPath: string): string | null { + return addon.readShortcut(linkPath); +} diff --git a/desktop/packages/win-shortcuts/src/index.mts b/desktop/packages/win-shortcuts/src/index.mts new file mode 100644 index 0000000000..5e1ab260f6 --- /dev/null +++ b/desktop/packages/win-shortcuts/src/index.mts @@ -0,0 +1,3 @@ +// This module is the ESM entry point for the library. + +export * from './index.cjs'; diff --git a/desktop/packages/win-shortcuts/src/load.cts b/desktop/packages/win-shortcuts/src/load.cts new file mode 100644 index 0000000000..e58b265901 --- /dev/null +++ b/desktop/packages/win-shortcuts/src/load.cts @@ -0,0 +1,11 @@ +// This module loads the platform-specific build of the addon on +// the current system. + +/* eslint-disable @typescript-eslint/no-require-imports */ +module.exports = require('@neon-rs/load').proxy({ + platforms: { + 'win32-x64-msvc': () => require('../dist/win32-x64-msvc'), + 'win32-arm64-msvc': () => require('../dist/win32-arm64-msvc'), + }, + debug: () => require('../debug/index.node'), +}); diff --git a/desktop/packages/win-shortcuts/tsconfig.json b/desktop/packages/win-shortcuts/tsconfig.json new file mode 100644 index 0000000000..31e6603771 --- /dev/null +++ b/desktop/packages/win-shortcuts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "node16", + "declaration": true, + "outDir": "lib" + }, + "exclude": ["lib"] +} diff --git a/desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs b/desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs new file mode 100644 index 0000000000..9cc0cea0c6 --- /dev/null +++ b/desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs @@ -0,0 +1,191 @@ +#![cfg(target_os = "windows")] + +use std::marker::PhantomData; +use std::string::FromUtf16Error; +use std::sync::{mpsc, OnceLock}; + +use neon::prelude::*; +use windows::core::{Interface, HSTRING, PCWSTR}; +use windows::Win32::System::Com::{ + CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, CLSCTX_INPROC_SERVER, + COINIT_APARTMENTTHREADED, STGM_READ, +}; +use windows::Win32::UI::Shell::{IShellLinkW, ShellLink, SLGP_UNCPRIORITY}; + +/// Messages that can be sent to the thread +enum Message { + ResolveShortcut { + path: String, + result_tx: mpsc::Sender<Result<Option<String>, Error>>, + }, +} + +#[derive(thiserror::Error, Debug)] +enum Error { + /// The handler thread is down + #[error("The handler thread is down")] + ThreadDown, + + /// CoCreateInstance failed to create an IShellLinkW instance + #[error("CoCreateInstance failed to create an IShellLinkW instance")] + CreateInstance(#[source] windows::core::Error), + + /// Failed to cast shortcut to IPersistFile + #[error("Failed to cast IShellLinkW")] + CastShortcut(#[source] windows::core::Error), + + /// Failed to load shortcut + #[error("Failed to load shortcut .lnk")] + LoadShortcut(#[source] windows::core::Error), + + /// Failed to retrieve IShellLinkW path + #[error("Failed to retrieve IShellLinkW link")] + GetPath(#[source] windows::core::Error), + + /// Path is not valid UTF-16 + #[error("Path is not valid UTF-16")] + Utf16ToString(#[source] FromUtf16Error), +} + +/// Maximum path length of shortcut +/// 32 KiB: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry +const MAX_PATH_LEN: usize = 0x7fff; + +#[neon::main] +fn main(mut cx: ModuleContext<'_>) -> NeonResult<()> { + cx.export_function("readShortcut", read_shortcut)?; + + Ok(()) +} + +fn read_shortcut(mut cx: FunctionContext<'_>) -> JsResult<'_, JsValue> { + let link_path = cx.argument::<JsString>(0)?.value(&mut cx); + + match read_shortcut_inner(link_path) { + Ok(Some(path)) => Ok(cx.string(path).as_value(&mut cx)), + Ok(None) => Ok(cx.null().as_value(&mut cx)), + Err(err) => cx.throw_error(format!("Failed to read shortcut: {err}")), + } +} + +fn read_shortcut_inner(link_path: String) -> Result<Option<String>, Error> { + let tx = get_com_thread(); + + let (result_tx, result_rx) = mpsc::channel(); + tx.send(Message::ResolveShortcut { + path: link_path, + result_tx, + }) + .map_err(|_err| Error::ThreadDown)?; + + result_rx.recv().map_err(|_err| Error::ThreadDown)? +} + +/// Retrieve shortcut .lnk to its target path +fn get_shortcut_path(path: &str) -> Result<Option<String>, Error> { + let shell_link_result: windows::core::Result<IShellLinkW> = + // SAFETY: We're passing a valid GUID pointer. + unsafe { CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER) }; + let shell_link = shell_link_result.map_err(Error::CreateInstance)?; + + // Load the .lnk using IPersistFile + let path = HSTRING::from(path); + let persist_file_result: windows::core::Result<IPersistFile> = shell_link.cast(); + let persist_file = persist_file_result.map_err(Error::CastShortcut)?; + + // SAFETY: HSTRING::from will ensure that path is a valid utf16 null-terminated string. + unsafe { persist_file.Load(PCWSTR(path.as_ptr()), STGM_READ) }.map_err(Error::LoadShortcut)?; + + let mut target_buffer = [0u16; MAX_PATH_LEN]; + + // SAFETY: This function is trivially safe to call. + unsafe { + shell_link.GetPath( + &mut target_buffer, + std::ptr::null_mut(), + SLGP_UNCPRIORITY.0 as u32, + ) + } + .map_err(Error::GetPath)?; + + let utf16_slice = split_at_null(&target_buffer); + let s = String::from_utf16(utf16_slice).map_err(Error::Utf16ToString)?; + Ok(Some(s)) +} + +fn split_at_null(slice: &[u16]) -> &[u16] { + slice.split(|&c| c == 0).next().unwrap_or(slice) +} + +/// Struct for safely handling initialization and deinitialization of the Windows COM library. +/// A successful call to [CoInitializeEx] _needs_ to be accompanied by a call to [CoUninitialize], +/// which is taken care by the drop implementation on [ComContext]. +/// +/// [CoInitializeEx] sets up thread-local state. Thus this type is `!Send` to stop it being moved +/// to another thread. +struct ComContext { + // HACK: until negative impls are stable, this how we stop `Send` from being impld + _do_not_impl_send: PhantomData<*mut ()>, +} + +impl ComContext { + /// Create a new [ComContext]. + /// + /// This will call [CoInitializeEx] now, and [CoUninitialize] when dropped. + /// + /// May return an error if [CoInitializeEx] was previously called with different arguments on + /// the same thread. + fn new() -> Result<Self, windows::core::Error> { + // SAFETY: This is paired with CoUninitialize in impl Drop + unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }.ok()?; + + Ok(Self { + _do_not_impl_send: PhantomData, + }) + } +} + +impl Drop for ComContext { + fn drop(&mut self) { + // SAFETY: CoInitializeEx was called when this struct was created, + // and it was called on the same thread since ComContext is !Send. + unsafe { + CoUninitialize(); + } + } +} + +/// Retrieve a channel for communicating with the thread responsible for handling +/// COM library safely. +/// We spawn a thread in case the caller may have already initialized COM in an +/// incompatible way. +fn get_com_thread() -> mpsc::Sender<Message> { + static THREAD_SENDER: OnceLock<mpsc::Sender<Message>> = OnceLock::new(); + THREAD_SENDER + .get_or_init(move || { + let (tx, rx) = mpsc::channel(); + + std::thread::spawn(move || { + let com = match ComContext::new() { + Ok(com) => com, + Err(e) => { + eprintln!("Failed to initialize ComContext: {e}"); + return; + } + }; + + while let Ok(msg) = rx.recv() { + match msg { + Message::ResolveShortcut { path, result_tx } => { + let _ = result_tx.send(get_shortcut_path(&path)); + } + } + } + + drop(com); + }); + + tx + }) + .clone() +} |
