Add native self-hosted instance connection to fluxer_desktop

Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 +
tools/ci) with a "Connect to a Different Server" menu item and popout
that lets the desktop app switch to any self-hosted Fluxer instance,
plus fixes for well-known discovery on single-domain self-hosted
deployments and a false-positive ERR_ABORTED on same-origin client
redirects during the switch. Defaults to chat.fluxr.chat and uses an
isolated userData directory from the official build.
This commit is contained in:
2026-07-01 18:22:43 -04:00
commit 682afacd30
1763 changed files with 613720 additions and 0 deletions
+905
View File
@@ -0,0 +1,905 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "block-buffer"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
dependencies = [
"hybrid-array",
]
[[package]]
name = "cc"
version = "1.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "convert_case"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "ctor"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a"
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fluxer_desktop_native"
version = "0.1.0"
dependencies = [
"sha2",
]
[[package]]
name = "fluxer_platform_info"
version = "0.0.0"
dependencies = [
"fluxer_desktop_native",
"napi",
"napi-build",
"napi-derive",
"objc2",
"objc2-foundation",
"objc2-metal",
"tempfile",
"windows",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
"wasip3",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hybrid-array"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
dependencies = [
"typenum",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "log"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "napi"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad513ff22558f1830b595ea6eb4091da48145d09a222ce157e781896f78be0b9"
dependencies = [
"bitflags",
"ctor",
"futures",
"napi-build",
"napi-sys",
"nohash-hasher",
"rustc-hash",
]
[[package]]
name = "napi-build"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
[[package]]
name = "napi-derive"
version = "3.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b3f766e04667e6da0e181e2da4f85475d5a6513b7cf6a80bea184e224a5b42"
dependencies = [
"convert_case",
"ctor",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "5.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"semver",
"syn",
]
[[package]]
name = "napi-sys"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f5bcdf71abd3a50d00b49c1c2c75251cb3c913777d6139cd37dabc093a5e400"
dependencies = [
"libloading",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
"objc2-exception-helper",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-exception-helper"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
dependencies = [
"cc",
]
[[package]]
name = "objc2-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags",
"objc2",
]
[[package]]
name = "objc2-metal"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
dependencies = [
"bitflags",
"objc2",
"objc2-foundation",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustc-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen 0.57.1",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
@@ -0,0 +1,34 @@
[package]
name = "fluxer_platform_info"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
fluxer_desktop_native = {path = "../rust"}
napi = {version = "3.9.1", default-features = false, features = ["dyn-symbols", "napi8"]}
napi-derive = "3.5.6"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = {version = "0.6.4", features = ["exception"]}
objc2-foundation = {version = "0.3.2", default-features = false, features = ["NSArray", "NSObject", "NSString", "std"]}
objc2-metal = {version = "0.3.2", default-features = false, features = ["MTLDevice", "std"]}
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Dxgi",
]}
[build-dependencies]
napi-build = "2.3.2"
[dev-dependencies]
tempfile = "3.27"
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
fn main() {
napi_build::setup();
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
println!("cargo:rustc-link-lib=framework=CoreGraphics");
}
}
+33
View File
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export type PlatformGpuInfoSource = 'metal' | 'dxgi' | 'linux-sysfs';
export interface PlatformGpuDeviceInfo {
active: boolean;
vendorId: number;
deviceId: number;
vendorName?: string;
deviceString?: string;
driverVendor?: string;
driverVersion?: string;
dedicatedVideoMemory?: number;
sharedSystemMemory?: number;
subsystemVendorId?: number;
subsystemDeviceId?: number;
registryId?: string;
adapterLuid?: string;
pciPath?: string;
integrated?: boolean;
removable?: boolean;
headless?: boolean;
source: PlatformGpuInfoSource;
}
export interface PlatformGpuInfo {
devices: ReadonlyArray<PlatformGpuDeviceInfo>;
source: PlatformGpuInfoSource;
error?: string;
}
export declare const getGpuInfo: (() => PlatformGpuInfo) | null;
export declare const loadError: Error | null;
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const {existsSync} = require('node:fs');
const {join, sep} = require('node:path');
const {createNativeLoadError, loadNativeBinding} = require('./loader-diagnostics.cjs');
const MODULE_NAME = '@fluxer/platform-info';
const SKIP_NATIVE_PROBE_ENV = 'FLUXER_PLATFORM_INFO_SKIP_NATIVE_PROBE';
function resolveNativeRoot() {
const asarSegment = `${sep}app.asar${sep}`;
if (!__dirname.includes(asarSegment)) return __dirname;
const unpackedDir = __dirname.replace(asarSegment, `${sep}app.asar.unpacked${sep}`);
return existsSync(unpackedDir) ? unpackedDir : __dirname;
}
function nativeFileName(platform = process.platform, arch = process.arch) {
switch (platform) {
case 'darwin':
if (arch === 'x64' || arch === 'arm64') return `platform-info.darwin-${arch}.node`;
break;
case 'win32':
if (arch === 'x64' || arch === 'arm64') return `platform-info.win32-${arch}-msvc.node`;
break;
case 'linux':
if (arch === 'x64' || arch === 'arm64') return `platform-info.linux-${arch}-gnu.node`;
break;
}
throw new Error(`Unsupported platform-info target: ${platform}/${arch}`);
}
let binding = null;
let loadError = null;
try {
const nativeRoot = resolveNativeRoot();
const nativePath = join(nativeRoot, nativeFileName());
const loaded = loadNativeBinding({
moduleName: MODULE_NAME,
nativePath,
nativeRoot,
packageDir: __dirname,
skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
});
binding = loaded.binding;
loadError = loaded.loadError;
if (loadError) throw loadError;
} catch (error) {
loadError = createNativeLoadError({
moduleName: MODULE_NAME,
nativeRoot: resolveNativeRoot(),
packageDir: __dirname,
reason: 'native loader threw before binding load completed',
cause: error,
skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
});
throw loadError;
}
module.exports = {
getGpuInfo: binding ? binding.getGpuInfo : null,
loadError,
_private: {
nativeFileName,
},
};
@@ -0,0 +1,524 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const {existsSync, readdirSync, readFileSync, statSync} = require('node:fs');
const os = require('node:os');
const {basename} = require('node:path');
const {spawnSync} = require('node:child_process');
const NATIVE_LOAD_ERROR_MARKER = Symbol.for('fluxer.nativeLoadError');
const MAX_TEXT_LENGTH = 6000;
const MAX_DIRECTORY_ENTRIES = 80;
function trimText(value, limit = MAX_TEXT_LENGTH) {
const text = Buffer.isBuffer(value) ? value.toString('utf8') : String(value ?? '');
const trimmed = text.trim();
if (!trimmed) return null;
return trimmed.length > limit ? `${trimmed.slice(0, limit)}\n...<truncated>` : trimmed;
}
function errorDiagnostic(error) {
if (!error) return null;
if (error instanceof Error) {
return {
name: error.name || 'Error',
message: error.message,
code: error.code || null,
stack: trimText(error.stack || error.message),
};
}
return {
name: typeof error,
message: trimText(String(error)),
code: null,
stack: null,
};
}
function formatErrorDiagnostic(diagnostic) {
if (!diagnostic) return null;
const lines = [];
if (diagnostic.code) lines.push(`code=${diagnostic.code}`);
if (diagnostic.stack) lines.push(diagnostic.stack);
else if (diagnostic.message) lines.push(diagnostic.message);
return trimText(lines.join('\n'));
}
function fileDiagnostic(filePath) {
if (!filePath) return {path: null, exists: false, error: 'not resolved'};
try {
const stat = statSync(filePath);
return {
path: filePath,
exists: true,
size: stat.size,
mode: `0${(stat.mode & 0o777).toString(8)}`,
mtime: stat.mtime.toISOString(),
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
};
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
return {path: filePath, exists: false, error: reason};
}
}
function formatFileDiagnostic(diagnostic) {
if (!diagnostic) return 'not resolved';
if (!diagnostic.exists) return `exists=false, statError=${diagnostic.error || '<unknown>'}`;
return [
`exists=true`,
`size=${diagnostic.size}`,
`mode=${diagnostic.mode}`,
`mtime=${diagnostic.mtime}`,
`isFile=${diagnostic.isFile}`,
].join(', ');
}
function directoryDiagnostic(dirPath) {
if (!dirPath) return {path: null, ok: false, error: 'not resolved', entries: [], total: 0, omitted: 0};
try {
const entries = readdirSync(dirPath, {withFileTypes: true}).map((entry) => ({
name: entry.name,
type: entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : 'other',
}));
entries.sort((a, b) => a.name.localeCompare(b.name));
const visible = entries.slice(0, MAX_DIRECTORY_ENTRIES);
return {
path: dirPath,
ok: true,
entries: visible,
total: entries.length,
omitted: Math.max(0, entries.length - visible.length),
};
} catch (error) {
return {
path: dirPath,
ok: false,
error: error instanceof Error ? error.message : String(error),
entries: [],
total: 0,
omitted: 0,
};
}
}
function formatDirectoryDiagnostic(diagnostic) {
if (!diagnostic) return '<unavailable>';
if (!diagnostic.ok) return `directory listing failed: ${diagnostic.error || '<unknown>'}`;
const entries = diagnostic.entries.map((entry) => `${entry.name}${entry.type === 'directory' ? '/' : ''}`);
const suffix = diagnostic.omitted > 0 ? [`...<${diagnostic.omitted} more entries>`] : [];
return [...entries, ...suffix].join('\n') || '<empty>';
}
function selectedEnvironmentNames(skipNativeProbeEnv) {
const names = [
'ELECTRON_RUN_AS_NODE',
'FLUXER_NATIVE_MODULE_PREFLIGHT_CHILD',
'LD_LIBRARY_PATH',
'DYLD_LIBRARY_PATH',
'DISPLAY',
'WAYLAND_DISPLAY',
'XDG_CURRENT_DESKTOP',
'XDG_SESSION_TYPE',
'DBUS_SESSION_BUS_ADDRESS',
'PULSE_SERVER',
'PIPEWIRE_REMOTE',
'PATH',
];
if (skipNativeProbeEnv) names.push(skipNativeProbeEnv);
return names;
}
function environmentDiagnostics(skipNativeProbeEnv) {
return Object.fromEntries(
selectedEnvironmentNames(skipNativeProbeEnv).map((name) => [name, process.env[name] ?? null]),
);
}
function formatEnvironment(diagnostic) {
return Object.entries(diagnostic)
.map(([name, value]) => `${name}=${value ?? '<unset>'}`)
.join('\n');
}
function runtimeDiagnostics() {
const versions = process.versions || {};
let reportHeader = null;
if (process.report && typeof process.report.getReport === 'function') {
try {
reportHeader = process.report.getReport().header || null;
} catch {
reportHeader = null;
}
}
const glibcRuntime = versions.glibcVersionRuntime || reportHeader?.glibcVersionRuntime || '<unknown>';
const glibcCompiler = versions.glibcVersionCompiler || reportHeader?.glibcVersionCompiler || '<unknown>';
return {
node: versions.node || null,
electron: versions.electron || null,
modules: versions.modules || null,
napi: versions.napi || null,
v8: versions.v8 || null,
uv: versions.uv || null,
openssl: versions.openssl || null,
glibcRuntime,
glibcCompiler,
platform: process.platform,
arch: process.arch,
osType: os.type(),
osRelease: os.release(),
osVersion: typeof os.version === 'function' ? os.version() : null,
execPath: process.execPath,
resourcesPath: process.resourcesPath || null,
cwd: process.cwd(),
};
}
function formatRuntimeDiagnostics(diagnostic) {
return [
`node=${diagnostic.node || '<unknown>'}`,
`electron=${diagnostic.electron || '<none>'}`,
`modules=${diagnostic.modules || '<unknown>'}`,
`napi=${diagnostic.napi || '<unknown>'}`,
`v8=${diagnostic.v8 || '<unknown>'}`,
`uv=${diagnostic.uv || '<unknown>'}`,
`openssl=${diagnostic.openssl || '<unknown>'}`,
`glibcRuntime=${diagnostic.glibcRuntime || '<unknown>'}`,
`glibcCompiler=${diagnostic.glibcCompiler || '<unknown>'}`,
`process=${diagnostic.platform}/${diagnostic.arch}`,
`os=${diagnostic.osType} ${diagnostic.osRelease} ${diagnostic.osVersion || '<unknown>'}`,
`execPath=${diagnostic.execPath}`,
`resourcesPath=${diagnostic.resourcesPath || '<unknown>'}`,
`cwd=${diagnostic.cwd}`,
].join('\n');
}
const REDISTRIBUTABLE_RUNTIME_PATTERNS = [
/^vcruntime\d+(?:_\d+)?\.dll$/i,
/^msvcp\d+(?:_\d+)?\.dll$/i,
/^msvcr\d+(?:_\d+)?\.dll$/i,
/^concrt\d+\.dll$/i,
/^vcamp\d+\.dll$/i,
/^vcomp\d+\.dll$/i,
];
function readPeImports(filePath) {
let buffer;
try {
buffer = readFileSync(filePath);
} catch {
return null;
}
if (buffer.length < 0x40) return null;
const peOffset = buffer.readUInt32LE(0x3c);
if (peOffset <= 0 || peOffset + 24 >= buffer.length) return null;
if (buffer.readUInt32LE(peOffset) !== 0x4550) return null;
const coffOffset = peOffset + 4;
const numberOfSections = buffer.readUInt16LE(coffOffset + 2);
const sizeOfOptionalHeader = buffer.readUInt16LE(coffOffset + 16);
const optionalHeaderOffset = coffOffset + 20;
if (optionalHeaderOffset + sizeOfOptionalHeader > buffer.length) return null;
const magic = buffer.readUInt16LE(optionalHeaderOffset);
if (magic !== 0x10b && magic !== 0x20b) return null;
const dataDirectoriesOffset = optionalHeaderOffset + (magic === 0x20b ? 112 : 96);
const importEntryOffset = dataDirectoriesOffset + 8;
if (importEntryOffset + 8 > buffer.length) return null;
const importRva = buffer.readUInt32LE(importEntryOffset);
if (importRva === 0) return [];
const sections = [];
const sectionTableOffset = optionalHeaderOffset + sizeOfOptionalHeader;
for (let i = 0; i < numberOfSections; i++) {
const base = sectionTableOffset + i * 40;
if (base + 40 > buffer.length) return null;
sections.push({
virtualSize: buffer.readUInt32LE(base + 8),
virtualAddress: buffer.readUInt32LE(base + 12),
rawSize: buffer.readUInt32LE(base + 16),
rawPointer: buffer.readUInt32LE(base + 20),
});
}
const rvaToOffset = (rva) => {
for (const s of sections) {
const span = Math.max(s.virtualSize, s.rawSize);
if (rva >= s.virtualAddress && rva < s.virtualAddress + span) {
return rva - s.virtualAddress + s.rawPointer;
}
}
return -1;
};
const readCString = (offset) => {
let end = offset;
while (end < buffer.length && buffer[end] !== 0) end++;
return buffer.toString('ascii', offset, end);
};
const importTableOffset = rvaToOffset(importRva);
if (importTableOffset < 0) return [];
const imports = new Set();
for (let i = 0; i < 1024; i++) {
const base = importTableOffset + i * 20;
if (base + 20 > buffer.length) break;
const lookupRva = buffer.readUInt32LE(base);
const nameRva = buffer.readUInt32LE(base + 12);
const iatRva = buffer.readUInt32LE(base + 16);
if (lookupRva === 0 && nameRva === 0 && iatRva === 0) break;
const nameOffset = rvaToOffset(nameRva);
if (nameOffset < 0) continue;
const name = readCString(nameOffset);
if (name) imports.add(name);
}
return Array.from(imports);
}
function windowsImportProbe(nativePath) {
const imports = readPeImports(nativePath);
if (imports === null) return null;
const sortedImports = [...imports].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
const redistributable = sortedImports.filter((dll) =>
REDISTRIBUTABLE_RUNTIME_PATTERNS.some((pattern) => pattern.test(dll)),
);
return {
command: ['pe-imports', nativePath],
status: 0,
signal: null,
error: null,
stdout: sortedImports.join('\n') || null,
stderr: null,
missing: [],
redistributable,
};
}
function dependencyProbe(nativePath) {
if (!nativePath || !existsSync(nativePath)) return null;
if (process.platform === 'win32') return windowsImportProbe(nativePath);
const command =
process.platform === 'linux'
? ['ldd', nativePath]
: process.platform === 'darwin'
? ['otool', '-L', nativePath]
: null;
if (!command) return null;
const [bin, ...args] = command;
const result = spawnSync(bin, args, {
encoding: 'utf8',
timeout: 4000,
stdio: ['ignore', 'pipe', 'pipe'],
});
const stdout = trimText(result.stdout);
const stderr = trimText(result.stderr);
const missing =
process.platform === 'linux' && stdout
? stdout
.split('\n')
.map((line) => line.trim())
.filter((line) => line.includes('not found'))
: [];
return {
command,
status: result.status,
signal: result.signal || null,
error: result.error ? result.error.message : null,
stdout,
stderr,
missing,
redistributable: [],
};
}
function formatDependencyProbe(diagnostic) {
if (!diagnostic) return null;
const status = diagnostic.error
? `error=${diagnostic.error}`
: diagnostic.signal
? `signal=${diagnostic.signal}`
: `status=${diagnostic.status}`;
return [
`$ ${diagnostic.command.join(' ')}`,
status,
diagnostic.missing?.length ? `missing:\n${diagnostic.missing.join('\n')}` : null,
diagnostic.redistributable?.length
? `redistributableRuntimeImports (require VC++ redist on host):\n${diagnostic.redistributable.join('\n')}`
: null,
diagnostic.stdout ? `stdout:\n${diagnostic.stdout}` : null,
diagnostic.stderr ? `stderr:\n${diagnostic.stderr}` : null,
]
.filter(Boolean)
.join('\n');
}
function formatExtraDiagnostic(diagnostic) {
if (!diagnostic) return null;
if (typeof diagnostic === 'string') return diagnostic;
if (typeof diagnostic === 'object' && diagnostic.name && diagnostic.text) {
return `${diagnostic.name}:\n${diagnostic.text}`;
}
return `extra:\n${trimText(JSON.stringify(diagnostic, null, 2))}`;
}
function collectNativeDiagnostics({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason,
cause,
skipNativeProbeEnv,
extraDiagnostics = [],
}) {
return {
schemaVersion: 1,
moduleName,
reason,
target: {
platform: process.platform,
arch: process.arch,
},
packageDir: packageDir || null,
nativeRoot: nativeRoot || null,
nativePath: nativePath || null,
nativeFile: nativePath ? basename(nativePath) : null,
nativeFileStat: fileDiagnostic(nativePath),
runtime: runtimeDiagnostics(),
environment: environmentDiagnostics(skipNativeProbeEnv),
nativeRootEntries: directoryDiagnostic(nativeRoot),
dependencyProbe: dependencyProbe(nativePath),
extraDiagnostics: extraDiagnostics.filter(Boolean),
cause: errorDiagnostic(cause),
};
}
function formatNativeDiagnostics(diagnostics) {
const sections = [
`module=${diagnostics.moduleName}`,
`reason=${diagnostics.reason}`,
`target=${diagnostics.target.platform}/${diagnostics.target.arch}`,
`packageDir=${diagnostics.packageDir || '<unknown>'}`,
`nativeRoot=${diagnostics.nativeRoot || '<unknown>'}`,
`nativePath=${diagnostics.nativePath || '<unknown>'}`,
`nativeFile=${diagnostics.nativeFile || '<unknown>'}`,
`nativeFileStat=${formatFileDiagnostic(diagnostics.nativeFileStat)}`,
`runtime:\n${formatRuntimeDiagnostics(diagnostics.runtime)}`,
`environment:\n${formatEnvironment(diagnostics.environment)}`,
`nativeRootEntries:\n${formatDirectoryDiagnostic(diagnostics.nativeRootEntries)}`,
...diagnostics.extraDiagnostics.map(formatExtraDiagnostic).filter(Boolean),
];
const dependencyOutput = formatDependencyProbe(diagnostics.dependencyProbe);
if (dependencyOutput) sections.push(`dependencyProbe:\n${dependencyOutput}`);
const causeText = formatErrorDiagnostic(diagnostics.cause);
if (causeText) sections.push(`cause:\n${causeText}`);
return sections.join('\n');
}
function isNativeLoadError(error) {
return Boolean(error?.[NATIVE_LOAD_ERROR_MARKER]);
}
function createNativeLoadError({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason,
cause,
skipNativeProbeEnv,
extraDiagnostics = [],
}) {
if (isNativeLoadError(cause)) return cause;
const diagnostics = collectNativeDiagnostics({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason,
cause,
skipNativeProbeEnv,
extraDiagnostics,
});
const error = new Error(`${moduleName} native module failed to load.\n${formatNativeDiagnostics(diagnostics)}`);
error.name = 'NativeModuleLoadError';
error[NATIVE_LOAD_ERROR_MARKER] = true;
error.nativeDiagnostics = diagnostics;
error.toJSON = () => ({
name: error.name,
message: error.message,
nativeDiagnostics: diagnostics,
});
if (cause) error.cause = cause;
return error;
}
function probeNativeBinary({moduleName, nativePath, nativeRoot, packageDir, skipNativeProbeEnv, timeoutMs = 5000}) {
if (!skipNativeProbeEnv || process.env[skipNativeProbeEnv] === '1') {
return null;
}
const result = spawnSync(process.execPath, ['-e', 'require(process.argv[1])', nativePath], {
env: {...process.env, ELECTRON_RUN_AS_NODE: '1', [skipNativeProbeEnv]: '1'},
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: timeoutMs,
});
if (result.status === 0) return null;
const reason = result.error
? result.error.message
: result.signal
? `safety probe terminated by signal ${result.signal}`
: `safety probe exited with code ${result.status}`;
return createNativeLoadError({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason,
skipNativeProbeEnv,
extraDiagnostics: [
result.stdout ? {name: 'probeStdout', text: trimText(result.stdout)} : null,
result.stderr ? {name: 'probeStderr', text: trimText(result.stderr)} : null,
],
});
}
function loadNativeBinding({moduleName, nativePath, nativeRoot, packageDir, skipNativeProbeEnv, probe = true}) {
if (!existsSync(nativePath)) {
return {
binding: null,
loadError: createNativeLoadError({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason: 'native binary not found',
skipNativeProbeEnv,
}),
};
}
const nativeProbeError = probe
? probeNativeBinary({moduleName, nativePath, nativeRoot, packageDir, skipNativeProbeEnv})
: null;
if (nativeProbeError) {
return {binding: null, loadError: nativeProbeError};
}
try {
return {binding: require(nativePath), loadError: null};
} catch (error) {
return {
binding: null,
loadError: createNativeLoadError({
moduleName,
nativePath,
nativeRoot,
packageDir,
reason: 'require(nativePath) threw',
cause: error,
skipNativeProbeEnv,
}),
};
}
}
module.exports = {
collectNativeDiagnostics,
createNativeLoadError,
formatNativeDiagnostics,
isNativeLoadError,
loadNativeBinding,
probeNativeBinary,
};
@@ -0,0 +1,28 @@
{
"name": "@fluxer/platform-info",
"version": "0.0.0",
"description": "",
"private": true,
"license": "AGPL-3.0-or-later",
"cpu": [
"x64",
"arm64"
],
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.js",
"index.d.ts",
"loader-diagnostics.cjs",
"platform-info.darwin-x64.node",
"platform-info.darwin-arm64.node",
"platform-info.win32-x64-msvc.node",
"platform-info.win32-arm64-msvc.node",
"platform-info.linux-x64-gnu.node",
"platform-info.linux-arm64-gnu.node"
],
"scripts": {
"build": "cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon",
"test": "cargo test --manifest-path Cargo.toml && node --test test/*.test.mjs"
}
}
@@ -0,0 +1,539 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use napi_derive::napi;
pub const SOURCE_METAL: &str = "metal";
pub const SOURCE_DXGI: &str = "dxgi";
pub const SOURCE_LINUX_SYSFS: &str = "linux-sysfs";
#[napi(object)]
pub struct PlatformGpuDeviceInfo {
pub active: bool,
#[napi(js_name = "vendorId")]
pub vendor_id: u32,
#[napi(js_name = "deviceId")]
pub device_id: u32,
#[napi(js_name = "vendorName")]
pub vendor_name: Option<String>,
#[napi(js_name = "deviceString")]
pub device_string: Option<String>,
#[napi(js_name = "driverVendor")]
pub driver_vendor: Option<String>,
#[napi(js_name = "driverVersion")]
pub driver_version: Option<String>,
#[napi(js_name = "dedicatedVideoMemory")]
pub dedicated_video_memory: Option<f64>,
#[napi(js_name = "sharedSystemMemory")]
pub shared_system_memory: Option<f64>,
#[napi(js_name = "subsystemVendorId")]
pub subsystem_vendor_id: Option<u32>,
#[napi(js_name = "subsystemDeviceId")]
pub subsystem_device_id: Option<u32>,
#[napi(js_name = "registryId")]
pub registry_id: Option<String>,
#[napi(js_name = "adapterLuid")]
pub adapter_luid: Option<String>,
#[napi(js_name = "pciPath")]
pub pci_path: Option<String>,
pub integrated: Option<bool>,
pub removable: Option<bool>,
pub headless: Option<bool>,
pub source: String,
}
#[napi(object)]
pub struct PlatformGpuInfo {
pub devices: Vec<PlatformGpuDeviceInfo>,
pub source: String,
pub error: Option<String>,
}
#[napi(js_name = "getGpuInfo")]
pub fn get_gpu_info() -> PlatformGpuInfo {
platform::gpu_info()
}
#[allow(dead_code)]
fn base_device(
active: bool,
vendor_id: u32,
device_id: u32,
source: &str,
) -> PlatformGpuDeviceInfo {
PlatformGpuDeviceInfo {
active,
vendor_id,
device_id,
vendor_name: fluxer_desktop_native::platform_info::probe_helpers::vendor_name(vendor_id)
.map(str::to_owned),
device_string: None,
driver_vendor: None,
driver_version: None,
dedicated_video_memory: None,
shared_system_memory: None,
subsystem_vendor_id: None,
subsystem_device_id: None,
registry_id: None,
adapter_luid: None,
pci_path: None,
integrated: None,
removable: None,
headless: None,
source: source.to_owned(),
}
}
#[allow(dead_code)]
fn empty_info(source: &str) -> PlatformGpuInfo {
PlatformGpuInfo {
devices: Vec::new(),
source: source.to_owned(),
error: None,
}
}
#[cfg(target_os = "macos")]
mod platform {
use fluxer_desktop_native::platform_info::probe_helpers::{
vendor_id_from_name, vendor_name, write_hex_u64,
};
use objc2::exception::catch;
use objc2::rc::autoreleasepool;
use objc2_metal::{MTLCopyAllDevices, MTLCreateSystemDefaultDevice, MTLDevice};
use super::{PlatformGpuDeviceInfo, PlatformGpuInfo, SOURCE_METAL, empty_info};
pub(super) fn gpu_info() -> PlatformGpuInfo {
match catch(|| autoreleasepool(|_| gpu_info_inner())) {
Ok(info) => info,
Err(exception) => {
let mut info = empty_info(SOURCE_METAL);
info.error = Some(exception.map_or_else(
|| "macOS GPU probe failed".to_owned(),
|exception| exception.to_string(),
));
info
}
}
}
fn gpu_info_inner() -> PlatformGpuInfo {
let default_device = MTLCreateSystemDefaultDevice();
let default_registry_id = default_device.as_deref().map(MTLDevice::registryID);
let all_devices = MTLCopyAllDevices();
let mut devices = Vec::new();
for index in 0..all_devices.count() {
let device = all_devices.objectAtIndex(index);
let active =
default_registry_id.is_some_and(|registry_id| registry_id == device.registryID());
devices.push(mac_device(&device, active));
}
if devices.is_empty() {
if let Some(device) = default_device {
devices.push(mac_device(&device, true));
}
} else if !devices.iter().any(|device| device.active) {
devices[0].active = true;
}
PlatformGpuInfo {
devices,
source: SOURCE_METAL.to_owned(),
error: None,
}
}
fn mac_device(
device: &objc2::runtime::ProtocolObject<dyn MTLDevice>,
active: bool,
) -> PlatformGpuDeviceInfo {
let name = device.name().to_string();
let vendor_id = vendor_id_from_name(&name);
PlatformGpuDeviceInfo {
active,
vendor_id,
device_id: 0,
vendor_name: vendor_name(vendor_id).map(str::to_owned),
device_string: Some(if name.is_empty() {
"Metal GPU".to_owned()
} else {
name
}),
driver_vendor: None,
driver_version: None,
dedicated_video_memory: None,
shared_system_memory: None,
subsystem_vendor_id: None,
subsystem_device_id: None,
registry_id: Some(write_hex_u64(device.registryID())),
adapter_luid: None,
pci_path: None,
integrated: Some(device.isLowPower() || vendor_id == 0x106b),
removable: Some(device.isRemovable()),
headless: Some(device.isHeadless()),
source: SOURCE_METAL.to_owned(),
}
}
}
#[allow(dead_code)]
mod dxgi_helpers {
pub fn utf16_description(raw: &[u16; 128]) -> String {
let end = raw.iter().position(|ch| *ch == 0).unwrap_or(raw.len());
let text = String::from_utf16_lossy(&raw[..end]);
if text.is_empty() {
"DXGI adapter".to_owned()
} else {
text
}
}
pub fn luid_string(high_part: i32, low_part: u32) -> String {
format!("{:08x}:{low_part:08x}", high_part as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn luid_format_matches_legacy_contract() {
assert_eq!("ffffffff:1234abcd", luid_string(-1, 0x1234_abcd));
assert_eq!("00000002:00000001", luid_string(2, 1));
}
#[test]
fn utf16_description_trims_at_first_nul() {
let mut raw = [0u16; 128];
raw[0] = 'G' as u16;
raw[1] = 'P' as u16;
raw[2] = 'U' as u16;
raw[3] = 0;
raw[4] = 'X' as u16;
assert_eq!("GPU", utf16_description(&raw));
}
}
}
#[cfg(target_os = "windows")]
mod platform {
use super::dxgi_helpers::{luid_string, utf16_description};
use fluxer_desktop_native::platform_info::probe_helpers::vendor_name;
use windows::Win32::Graphics::Dxgi::{
CreateDXGIFactory1, DXGI_ADAPTER_DESC1, DXGI_ADAPTER_FLAG_SOFTWARE,
DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IDXGIAdapter1, IDXGIFactory1, IDXGIFactory6,
};
use super::{PlatformGpuDeviceInfo, PlatformGpuInfo, SOURCE_DXGI, empty_info};
pub(super) fn gpu_info() -> PlatformGpuInfo {
let mut info = empty_info(SOURCE_DXGI);
info.devices = enumerate_dxgi_adapters();
info
}
fn enumerate_dxgi_adapters() -> Vec<PlatformGpuDeviceInfo> {
unsafe {
if let Ok(factory) = CreateDXGIFactory1::<IDXGIFactory6>() {
return enumerate_factory6(&factory);
}
if let Ok(factory) = CreateDXGIFactory1::<IDXGIFactory1>() {
return enumerate_factory1(&factory);
}
}
Vec::new()
}
unsafe fn enumerate_factory6(factory: &IDXGIFactory6) -> Vec<PlatformGpuDeviceInfo> {
let mut devices = Vec::new();
let mut ordered_index = 0;
loop {
let Ok(adapter) = (unsafe {
factory.EnumAdapterByGpuPreference::<IDXGIAdapter1>(
ordered_index,
DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,
)
}) else {
break;
};
if let Ok(desc) = unsafe { adapter.GetDesc1() } {
push_dxgi_desc(&mut devices, &desc, ordered_index);
}
ordered_index += 1;
}
devices
}
unsafe fn enumerate_factory1(factory: &IDXGIFactory1) -> Vec<PlatformGpuDeviceInfo> {
let mut devices = Vec::new();
let mut ordered_index = 0;
loop {
let Ok(adapter) = (unsafe { factory.EnumAdapters1(ordered_index) }) else {
break;
};
if let Ok(desc) = unsafe { adapter.GetDesc1() } {
push_dxgi_desc(&mut devices, &desc, ordered_index);
}
ordered_index += 1;
}
devices
}
fn push_dxgi_desc(
devices: &mut Vec<PlatformGpuDeviceInfo>,
desc: &DXGI_ADAPTER_DESC1,
ordered_index: u32,
) {
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32) != 0 {
return;
}
let mut device = PlatformGpuDeviceInfo {
active: ordered_index == 0,
vendor_id: desc.VendorId,
device_id: desc.DeviceId,
vendor_name: vendor_name(desc.VendorId).map(str::to_owned),
device_string: Some(utf16_description(&desc.Description)),
driver_vendor: None,
driver_version: None,
dedicated_video_memory: Some(desc.DedicatedVideoMemory as f64),
shared_system_memory: Some(desc.SharedSystemMemory as f64),
subsystem_vendor_id: Some(desc.SubSysId >> 16),
subsystem_device_id: Some(desc.SubSysId & 0xffff),
registry_id: None,
adapter_luid: None,
pci_path: None,
integrated: None,
removable: None,
headless: None,
source: SOURCE_DXGI.to_owned(),
};
device.adapter_luid = Some(luid_string(
desc.AdapterLuid.HighPart,
desc.AdapterLuid.LowPart,
));
devices.push(device);
}
}
#[cfg(target_os = "linux")]
mod platform {
use std::fs;
use std::path::Path;
use fluxer_desktop_native::platform_info::probe_helpers::{
basename, is_drm_card_name, parse_hex_id, vendor_name,
};
use super::{PlatformGpuDeviceInfo, PlatformGpuInfo, SOURCE_LINUX_SYSFS, base_device};
pub(super) fn gpu_info() -> PlatformGpuInfo {
linux_gpu_info_from_drm_root(Path::new("/sys/class/drm"))
}
fn linux_gpu_info_from_drm_root(root: &Path) -> PlatformGpuInfo {
let mut info = PlatformGpuInfo {
devices: Vec::new(),
source: SOURCE_LINUX_SYSFS.to_owned(),
error: None,
};
let Ok(entries) = fs::read_dir(root) else {
return info;
};
let mut card_names = entries
.filter_map(Result::ok)
.filter_map(|entry| entry.file_name().into_string().ok())
.filter(|name| is_drm_card_name(name))
.collect::<Vec<_>>();
card_names.sort_unstable_by_key(|name| card_sort_key(name));
for card_name in card_names {
if let Some(device) = linux_card(root, &card_name, info.devices.is_empty()) {
info.devices.push(device);
}
}
info
}
fn linux_card(root: &Path, card_name: &str, active: bool) -> Option<PlatformGpuDeviceInfo> {
let base = root.join(card_name).join("device");
let vendor_id = read_hex_file(base.join("vendor"))?;
let device_id = read_hex_file(base.join("device")).unwrap_or(0);
let mut device = base_device(active, vendor_id, device_id, SOURCE_LINUX_SYSFS);
device.subsystem_vendor_id =
Some(read_hex_file(base.join("subsystem_vendor")).unwrap_or(0));
device.subsystem_device_id =
Some(read_hex_file(base.join("subsystem_device")).unwrap_or(0));
device.pci_path = read_link_basename(&base);
device.driver_vendor = read_link_basename(base.join("driver"));
if let Some(uevent) = read_trimmed_file(base.join("uevent")) {
if let Some(driver) = uevent_value(&uevent, "DRIVER") {
device.driver_vendor = Some(driver.to_owned());
}
if let Some(pci_id) = uevent_value(&uevent, "PCI_ID") {
device.device_string = Some(match vendor_name(vendor_id) {
Some(vendor) => format!("{vendor} GPU ({pci_id})"),
None => format!("GPU ({pci_id})"),
});
}
}
if let Some(vram) = read_trimmed_file(base.join("mem_info_vram_total"))
&& let Ok(bytes) = vram.parse::<u64>()
{
device.dedicated_video_memory = Some(bytes as f64);
}
Some(device)
}
fn card_sort_key(name: &str) -> u32 {
name.strip_prefix("card")
.and_then(|rest| rest.parse::<u32>().ok())
.unwrap_or(u32::MAX)
}
fn read_hex_file(path: impl AsRef<Path>) -> Option<u32> {
let raw = fs::read_to_string(path).ok()?;
parse_hex_id(&raw).ok()
}
fn read_trimmed_file(path: impl AsRef<Path>) -> Option<String> {
let raw = fs::read_to_string(path).ok()?;
Some(raw.trim_matches([' ', '\t', '\r', '\n']).to_owned())
}
fn read_link_basename(path: impl AsRef<Path>) -> Option<String> {
let target = fs::read_link(path).ok()?;
Some(path_basename(&target))
}
fn path_basename(path: &Path) -> String {
path.file_name()
.and_then(|name| name.to_str())
.map(str::to_owned)
.unwrap_or_else(|| basename(&path.to_string_lossy()).to_owned())
}
fn uevent_value<'a>(blob: &'a str, key: &str) -> Option<&'a str> {
blob.lines().find_map(|line| {
let (line_key, value) = line.split_once('=')?;
if line_key == key {
Some(value.trim_matches([' ', '\t', '\r', '\n']))
} else {
None
}
})
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir_all, write};
use std::os::unix::fs::symlink;
use super::*;
#[test]
fn uevent_value_matches_exact_keys() {
let blob = "PCI_ID=8086:46A6\nDRIVER=i915\nNOT_DRIVER=bad\n";
assert_eq!(Some("i915"), uevent_value(blob, "DRIVER"));
assert_eq!(Some("8086:46A6"), uevent_value(blob, "PCI_ID"));
assert_eq!(None, uevent_value(blob, "MISSING"));
}
#[test]
fn card_sort_key_keeps_card_order_numeric() {
let mut names = ["card10".to_owned(), "card2".to_owned(), "card0".to_owned()];
names.sort_unstable_by_key(|name| card_sort_key(name));
assert_eq!(["card0", "card2", "card10"], names);
}
#[test]
fn linux_sysfs_fixture_ports_legacy_gpu_contract() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let pci0 = root.join("devices/pci0000:00/0000:00:02.0");
let pci1 = root.join("devices/pci0000:01/0000:01:00.0");
create_dir_all(root.join("card0")).unwrap();
create_dir_all(root.join("card1")).unwrap();
create_dir_all(&pci0).unwrap();
create_dir_all(&pci1).unwrap();
symlink(
"../devices/pci0000:00/0000:00:02.0",
root.join("card0/device"),
)
.unwrap();
symlink(
"../devices/pci0000:01/0000:01:00.0",
root.join("card1/device"),
)
.unwrap();
let card0 = root.join("card0/device");
let card1 = root.join("card1/device");
write(card0.join("vendor"), "0x8086\n").unwrap();
write(card0.join("device"), "0x46a6\n").unwrap();
write(card0.join("subsystem_vendor"), "0x1028\n").unwrap();
write(card0.join("subsystem_device"), "0x0b19\n").unwrap();
write(card0.join("uevent"), "DRIVER=i915\nPCI_ID=8086:46A6\n").unwrap();
write(card0.join("mem_info_vram_total"), "268435456\n").unwrap();
write(card1.join("vendor"), "0x10de\n").unwrap();
write(card1.join("device"), "0x1f99\n").unwrap();
write(card1.join("uevent"), "PCI_ID=10DE:1F99\n").unwrap();
symlink("/sys/bus/pci/drivers/nvidia", card1.join("driver")).unwrap();
let info = linux_gpu_info_from_drm_root(root);
assert_eq!(SOURCE_LINUX_SYSFS, info.source);
assert_eq!(2, info.devices.len());
assert!(info.devices[0].active);
assert_eq!(0x8086, info.devices[0].vendor_id);
assert_eq!(Some("Intel".to_owned()), info.devices[0].vendor_name);
assert_eq!(
Some("Intel GPU (8086:46A6)".to_owned()),
info.devices[0].device_string
);
assert_eq!(Some("i915".to_owned()), info.devices[0].driver_vendor);
assert_eq!(Some("0000:00:02.0".to_owned()), info.devices[0].pci_path);
assert_eq!(Some(268_435_456.0), info.devices[0].dedicated_video_memory);
assert!(!info.devices[1].active);
assert_eq!(Some("nvidia".to_owned()), info.devices[1].driver_vendor);
}
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
mod platform {
use super::{PlatformGpuInfo, SOURCE_LINUX_SYSFS, empty_info};
pub(super) fn gpu_info() -> PlatformGpuInfo {
empty_info(SOURCE_LINUX_SYSFS)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base_device_sets_required_contract_fields() {
let device = base_device(true, 0x10de, 0x2684, SOURCE_DXGI);
assert!(device.active);
assert_eq!(0x10de, device.vendor_id);
assert_eq!(0x2684, device.device_id);
assert_eq!(Some("NVIDIA".to_owned()), device.vendor_name);
assert_eq!(SOURCE_DXGI, device.source);
}
#[test]
fn empty_info_preserves_source_and_empty_devices() {
let info = empty_info(SOURCE_LINUX_SYSFS);
assert_eq!(SOURCE_LINUX_SYSFS, info.source);
assert!(info.devices.is_empty());
assert!(info.error.is_none());
}
}
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import test from 'node:test';
import platformInfo from '../index.js';
test('loader exposes a stable optional native surface', () => {
assert.equal(typeof platformInfo, 'object');
assert.equal(platformInfo.getGpuInfo === null || typeof platformInfo.getGpuInfo === 'function', true);
assert.equal(platformInfo.loadError === null || platformInfo.loadError instanceof Error, true);
});
test('loader resolves native binaries for supported platforms and architectures', () => {
assert.equal(platformInfo._private.nativeFileName('darwin', 'x64'), 'platform-info.darwin-x64.node');
assert.equal(platformInfo._private.nativeFileName('darwin', 'arm64'), 'platform-info.darwin-arm64.node');
assert.equal(platformInfo._private.nativeFileName('linux', 'x64'), 'platform-info.linux-x64-gnu.node');
assert.equal(platformInfo._private.nativeFileName('linux', 'arm64'), 'platform-info.linux-arm64-gnu.node');
assert.equal(platformInfo._private.nativeFileName('win32', 'x64'), 'platform-info.win32-x64-msvc.node');
assert.equal(platformInfo._private.nativeFileName('win32', 'arm64'), 'platform-info.win32-arm64-msvc.node');
});