mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-23 12:50:59 +02:00
chore: add cross-platform webdriver tests
This commit is contained in:
Generated
+221
-70
@@ -442,7 +442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
|
||||
dependencies = [
|
||||
"atk-sys",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -452,10 +452,10 @@ version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -912,7 +912,7 @@ checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"cairo-sys-rs",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"thiserror 1.0.69",
|
||||
@@ -924,9 +924,9 @@ version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1027,7 +1027,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.20.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1857,6 +1867,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-cross-platform-webdriver",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
@@ -2470,7 +2481,7 @@ dependencies = [
|
||||
"gdk-pixbuf",
|
||||
"gdk-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
@@ -2483,7 +2494,7 @@ checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
|
||||
dependencies = [
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"once_cell",
|
||||
]
|
||||
@@ -2494,11 +2505,11 @@ version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2509,13 +2520,13 @@ checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2525,11 +2536,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
|
||||
dependencies = [
|
||||
"gdk-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2541,7 +2552,7 @@ dependencies = [
|
||||
"gdk",
|
||||
"gdkx11-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"x11",
|
||||
]
|
||||
@@ -2553,9 +2564,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
|
||||
dependencies = [
|
||||
"gdk-sys",
|
||||
"glib-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
"x11",
|
||||
]
|
||||
|
||||
@@ -2653,8 +2664,8 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"gio-sys 0.18.1",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
@@ -2668,13 +2679,26 @@ version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio-sys"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22"
|
||||
dependencies = [
|
||||
"glib-sys 0.21.5",
|
||||
"gobject-sys 0.21.5",
|
||||
"libc",
|
||||
"system-deps 7.0.8",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.18.5"
|
||||
@@ -2687,10 +2711,10 @@ dependencies = [
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib-macros",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-macros 0.18.5",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@@ -2698,6 +2722,27 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"gio-sys 0.21.5",
|
||||
"glib-macros 0.21.5",
|
||||
"glib-sys 0.21.5",
|
||||
"gobject-sys 0.21.5",
|
||||
"libc",
|
||||
"memchr",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.18.5"
|
||||
@@ -2712,6 +2757,19 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.18.1"
|
||||
@@ -2719,7 +2777,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2747,9 +2815,20 @@ version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294"
|
||||
dependencies = [
|
||||
"glib-sys 0.21.5",
|
||||
"libc",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2765,7 +2844,7 @@ dependencies = [
|
||||
"gdk",
|
||||
"gdk-pixbuf",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"gtk-sys",
|
||||
"gtk3-macros",
|
||||
"libc",
|
||||
@@ -2783,12 +2862,12 @@ dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gdk-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3427,7 +3506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"javascriptcore-rs-sys",
|
||||
]
|
||||
|
||||
@@ -3437,10 +3516,10 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3596,7 +3675,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
|
||||
dependencies = [
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"gtk",
|
||||
"gtk-sys",
|
||||
"libappindicator-sys",
|
||||
@@ -4278,6 +4357,16 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-javascript-core"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-open-directory"
|
||||
version = "0.3.2"
|
||||
@@ -4301,6 +4390,17 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-security"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-ui-kit"
|
||||
version = "0.3.2"
|
||||
@@ -4344,6 +4444,8 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"objc2-javascript-core",
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4461,7 +4563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
|
||||
dependencies = [
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pango-sys",
|
||||
@@ -4473,10 +4575,10 @@ version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5290,8 +5392,8 @@ checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -6149,7 +6251,7 @@ checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"soup3-sys",
|
||||
]
|
||||
@@ -6160,11 +6262,11 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6363,13 +6465,26 @@ version = "6.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"cfg-expr 0.15.8",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.2",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "7.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
|
||||
dependencies = [
|
||||
"cfg-expr 0.20.8",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.35.3"
|
||||
@@ -6444,6 +6559,12 @@ version = "0.12.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.11.5"
|
||||
@@ -6589,6 +6710,36 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-cross-platform-webdriver"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"block2",
|
||||
"cairo-rs",
|
||||
"glib 0.21.5",
|
||||
"gtk",
|
||||
"javascriptcore-rs",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-web-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.4.9"
|
||||
@@ -7032,9 +7183,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
version = "1.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -7694,9 +7845,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.4"
|
||||
version = "1.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
|
||||
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
@@ -7964,10 +8115,10 @@ dependencies = [
|
||||
"gdk",
|
||||
"gdk-sys",
|
||||
"gio",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib 0.18.5",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"gtk",
|
||||
"gtk-sys",
|
||||
"javascriptcore-rs",
|
||||
@@ -7986,15 +8137,15 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cairo-sys-rs",
|
||||
"gdk-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"gtk-sys",
|
||||
"javascriptcore-rs-sys",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"soup3-sys",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -42,6 +42,7 @@ tauri-plugin-macos-permissions = "2"
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-clipboard-manager = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
tauri-plugin-cross-platform-webdriver = { path = "../../tauri-cross-platform-webdriver/crates/tauri-plugin-cross-platform-webdriver", optional = true }
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
@@ -186,3 +187,4 @@ default = ["custom-protocol"]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
e2e = ["dep:tauri-plugin-cross-platform-webdriver"]
|
||||
|
||||
@@ -2037,6 +2037,14 @@ rm "{}"
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_app_updates() -> Result<Option<AppUpdateInfo>, String> {
|
||||
#[cfg(feature = "e2e")]
|
||||
if tauri_plugin_cross_platform_webdriver::automation_enabled()
|
||||
&& std::env::var_os("DONUT_E2E_DISABLE_STARTUP_NETWORK").is_some()
|
||||
{
|
||||
log::info!("E2E: skipping automatic app update check");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if crate::app_dirs::is_portable() {
|
||||
log::info!("App auto-updates disabled in portable mode");
|
||||
return Ok(None);
|
||||
@@ -2090,6 +2098,14 @@ pub async fn restart_application() -> Result<(), String> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_app_updates_manual() -> Result<Option<AppUpdateInfo>, String> {
|
||||
#[cfg(feature = "e2e")]
|
||||
if tauri_plugin_cross_platform_webdriver::automation_enabled()
|
||||
&& std::env::var_os("DONUT_E2E_DISABLE_STARTUP_NETWORK").is_some()
|
||||
{
|
||||
log::info!("E2E: skipping manual app update check");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
log::info!("Manual app update check triggered");
|
||||
let updater = AppAutoUpdater::instance();
|
||||
updater
|
||||
|
||||
@@ -779,6 +779,13 @@ impl CloudAuthManager {
|
||||
|
||||
/// Launch/drive profiles programmatically (local API + MCP automation).
|
||||
pub async fn can_use_browser_automation(&self) -> bool {
|
||||
#[cfg(feature = "e2e")]
|
||||
if crate::e2e_automation_enabled()
|
||||
&& std::env::var_os("WAYFERN_TEST_TOKEN").is_some_and(|token| !token.is_empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
self
|
||||
.entitlements()
|
||||
.await
|
||||
@@ -788,6 +795,13 @@ impl CloudAuthManager {
|
||||
|
||||
/// Edit fingerprints / use a non-native OS fingerprint.
|
||||
pub async fn can_use_cross_os_fingerprints(&self) -> bool {
|
||||
#[cfg(feature = "e2e")]
|
||||
if crate::e2e_automation_enabled()
|
||||
&& std::env::var_os("WAYFERN_TEST_TOKEN").is_some_and(|token| !token.is_empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
self
|
||||
.entitlements()
|
||||
.await
|
||||
@@ -1224,6 +1238,16 @@ impl CloudAuthManager {
|
||||
|
||||
/// Get the current wayfern token, if any.
|
||||
pub async fn get_wayfern_token(&self) -> Option<String> {
|
||||
#[cfg(feature = "e2e")]
|
||||
if tauri_plugin_cross_platform_webdriver::automation_enabled() {
|
||||
if let Some(token) = std::env::var_os("WAYFERN_TEST_TOKEN")
|
||||
.filter(|token| !token.is_empty())
|
||||
.and_then(|token| token.into_string().ok())
|
||||
{
|
||||
return Some(token);
|
||||
}
|
||||
}
|
||||
|
||||
let wt = self.wayfern_token.lock().await;
|
||||
wt.clone()
|
||||
}
|
||||
|
||||
@@ -295,9 +295,23 @@ impl BlocklistManager {
|
||||
}
|
||||
|
||||
pub async fn fetch_blocklist(level: BlocklistLevel) -> Result<PathBuf, String> {
|
||||
let url = level
|
||||
let production_url = level
|
||||
.url()
|
||||
.ok_or_else(|| format!("No URL for level {:?}", level))?;
|
||||
#[cfg(feature = "e2e")]
|
||||
let url = std::env::var("DONUT_E2E_DNS_BLOCKLIST_BASE_URL")
|
||||
.ok()
|
||||
.filter(|base| !base.is_empty())
|
||||
.map(|base| {
|
||||
format!(
|
||||
"{}/{}",
|
||||
base.trim_end_matches('/'),
|
||||
level.filename().unwrap_or("blocklist.txt")
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| production_url.to_string());
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
let url = production_url.to_string();
|
||||
let path =
|
||||
Self::cached_file_path(level).ok_or_else(|| format!("No filename for level {:?}", level))?;
|
||||
|
||||
@@ -311,7 +325,7 @@ impl BlocklistManager {
|
||||
);
|
||||
|
||||
let response = HTTP_CLIENT
|
||||
.get(url)
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch blocklist: {e}"))?;
|
||||
|
||||
@@ -1261,6 +1261,14 @@ mod tests {
|
||||
pub async fn ensure_active_browsers_downloaded(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<Vec<String>, String> {
|
||||
#[cfg(feature = "e2e")]
|
||||
if tauri_plugin_cross_platform_webdriver::automation_enabled()
|
||||
&& std::env::var_os("DONUT_E2E_DISABLE_STARTUP_NETWORK").is_some()
|
||||
{
|
||||
log::info!("E2E: skipping proactive browser download");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let registry = DownloadedBrowsersRegistry::instance();
|
||||
let version_manager = crate::browser_version_manager::BrowserVersionManager::instance();
|
||||
let mut downloaded = Vec::new();
|
||||
@@ -1410,6 +1418,14 @@ pub async fn check_missing_binaries() -> Result<Vec<(String, String, String)>, S
|
||||
pub async fn ensure_all_binaries_exist(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<Vec<String>, String> {
|
||||
#[cfg(feature = "e2e")]
|
||||
if tauri_plugin_cross_platform_webdriver::automation_enabled()
|
||||
&& std::env::var_os("DONUT_E2E_DISABLE_STARTUP_NETWORK").is_some()
|
||||
{
|
||||
log::info!("E2E: skipping proactive binary and GeoIP downloads");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let registry = DownloadedBrowsersRegistry::instance();
|
||||
registry
|
||||
.ensure_all_binaries_exist(&app_handle)
|
||||
|
||||
@@ -141,13 +141,22 @@ impl GeoIPDownloader {
|
||||
},
|
||||
);
|
||||
|
||||
// Fetch latest release from GitHub
|
||||
let releases = self.fetch_geoip_releases().await?;
|
||||
let latest_release = releases.first().ok_or("No GeoIP database releases found")?;
|
||||
#[cfg(feature = "e2e")]
|
||||
let fixture_url = std::env::var("DONUT_E2E_GEOIP_DOWNLOAD_URL")
|
||||
.ok()
|
||||
.filter(|url| !url.is_empty());
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
let fixture_url: Option<String> = None;
|
||||
|
||||
let download_url = self
|
||||
.find_city_mmdb_asset(latest_release)
|
||||
.ok_or("No compatible GeoIP database asset found")?;
|
||||
let download_url = if let Some(url) = fixture_url {
|
||||
url
|
||||
} else {
|
||||
let releases = self.fetch_geoip_releases().await?;
|
||||
let latest_release = releases.first().ok_or("No GeoIP database releases found")?;
|
||||
self
|
||||
.find_city_mmdb_asset(latest_release)
|
||||
.ok_or("No compatible GeoIP database asset found")?
|
||||
};
|
||||
|
||||
// Create cache directory
|
||||
let cache_dir = Self::get_cache_dir();
|
||||
|
||||
+207
-187
@@ -3,6 +3,7 @@ use std::env;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{Emitter, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_log::{Target, TargetKind};
|
||||
|
||||
@@ -14,6 +15,17 @@ static PENDING_URLS: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
// to the confirmation dialog.
|
||||
static QUIT_CONFIRMED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn e2e_automation_enabled() -> bool {
|
||||
#[cfg(feature = "e2e")]
|
||||
{
|
||||
tauri_plugin_cross_platform_webdriver::automation_enabled()
|
||||
}
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
mod api_client;
|
||||
mod api_server;
|
||||
mod app_auto_updater;
|
||||
@@ -1263,6 +1275,7 @@ fn hide_to_tray(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
fn show_main_window(app_handle: &tauri::AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
@@ -1302,6 +1315,7 @@ fn update_tray_menu(
|
||||
/// Build the system tray. Best-effort: on Linux the tray depends on
|
||||
/// libayatana-appindicator at runtime, so any failure here must not abort app
|
||||
/// startup — the caller logs and continues without a tray.
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
fn setup_system_tray(app: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::sync::atomic::Ordering;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder};
|
||||
@@ -1391,54 +1405,65 @@ pub fn run() {
|
||||
}),
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::new()
|
||||
.clear_targets() // Clear default targets to avoid duplicates
|
||||
.target(Target::new(TargetKind::Stdout))
|
||||
.target(Target::new(TargetKind::Webview))
|
||||
.target(file_log_target)
|
||||
// 5 MB per rotated file × KeepAll — the previous 100 KB limit
|
||||
// truncated useful context in customer support reports; 50 MB
|
||||
// turned out to be excessive disk pressure.
|
||||
.max_file_size(5 * 1024 * 1024)
|
||||
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll)
|
||||
.level(log::LevelFilter::Info)
|
||||
.format(|out, message, record| {
|
||||
use chrono::Local;
|
||||
let now = Local::now();
|
||||
let timestamp = format!(
|
||||
"{}.{:03}",
|
||||
now.format("%Y-%m-%d %H:%M:%S"),
|
||||
now.timestamp_subsec_millis()
|
||||
);
|
||||
out.finish(format_args!(
|
||||
"[{}][{}][{}] {}",
|
||||
timestamp,
|
||||
record.target(),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_single_instance::init(
|
||||
|app_handle, args, _cwd| {
|
||||
log::info!("Single instance triggered with args: {args:?}");
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
},
|
||||
))
|
||||
let builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(feature = "e2e")]
|
||||
let builder = builder.plugin(tauri_plugin_cross_platform_webdriver::init());
|
||||
|
||||
let builder = builder.plugin(
|
||||
tauri_plugin_log::Builder::new()
|
||||
.clear_targets() // Clear default targets to avoid duplicates
|
||||
.target(Target::new(TargetKind::Stdout))
|
||||
.target(Target::new(TargetKind::Webview))
|
||||
.target(file_log_target)
|
||||
// 5 MB per rotated file × KeepAll — the previous 100 KB limit
|
||||
// truncated useful context in customer support reports; 50 MB
|
||||
// turned out to be excessive disk pressure.
|
||||
.max_file_size(5 * 1024 * 1024)
|
||||
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll)
|
||||
.level(log::LevelFilter::Info)
|
||||
.format(|out, message, record| {
|
||||
use chrono::Local;
|
||||
let now = Local::now();
|
||||
let timestamp = format!(
|
||||
"{}.{:03}",
|
||||
now.format("%Y-%m-%d %H:%M:%S"),
|
||||
now.timestamp_subsec_millis()
|
||||
);
|
||||
out.finish(format_args!(
|
||||
"[{}][{}][{}] {}",
|
||||
timestamp,
|
||||
record.target(),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.build(),
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
let builder = builder.plugin(tauri_plugin_single_instance::init(
|
||||
|app_handle, args, _cwd| {
|
||||
log::info!("Single instance triggered with args: {args:?}");
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
let builder = builder
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_macos_permissions::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init());
|
||||
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
let builder = builder
|
||||
// Persist window size/position across restarts. VISIBLE is excluded
|
||||
// because the app hides to tray: restoring visibility would otherwise
|
||||
// relaunch with an invisible window after quitting from the tray while
|
||||
@@ -1453,8 +1478,9 @@ pub fn run() {
|
||||
& !tauri_plugin_window_state::StateFlags::FULLSCREEN,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.setup(|app| {
|
||||
);
|
||||
|
||||
builder.setup(|app| {
|
||||
// Recover ephemeral dir mappings from RAM-backed storage (tmpfs/ramdisk)
|
||||
ephemeral_dirs::recover_ephemeral_dirs();
|
||||
|
||||
@@ -1476,6 +1502,20 @@ pub fn run() {
|
||||
.focused(true)
|
||||
.visible(true);
|
||||
|
||||
#[cfg(feature = "e2e")]
|
||||
let win_builder =
|
||||
match tauri_plugin_cross_platform_webdriver::automation_profile_dir() {
|
||||
Some(profile_dir) => win_builder
|
||||
.data_directory(profile_dir.join("webview"))
|
||||
// WKWebView ignores data_directory on macOS. Incognito gives every
|
||||
// launched app process a non-persistent data store there, and also
|
||||
// prevents WebView2/WebKitGTK caches from escaping the session on
|
||||
// the other platforms. Durable app state is still exercised via
|
||||
// DONUTBROWSER_DATA_ROOT; only browser-engine storage is ephemeral.
|
||||
.incognito(true),
|
||||
None => win_builder,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let win_builder = win_builder.decorations(false);
|
||||
|
||||
@@ -1486,8 +1526,11 @@ pub fn run() {
|
||||
// dialog's "Minimize" action hides the window. Best-effort: a tray
|
||||
// failure (e.g. missing libayatana-appindicator on Linux) must never
|
||||
// prevent the app from launching, so we log and continue without it.
|
||||
if let Err(e) = setup_system_tray(app.handle()) {
|
||||
log::warn!("System tray unavailable, continuing without it: {e}");
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
{
|
||||
if let Err(e) = setup_system_tray(app.handle()) {
|
||||
log::warn!("System tray unavailable, continuing without it: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Intercept the window close so the frontend can ask the user whether
|
||||
@@ -1530,7 +1573,7 @@ pub fn run() {
|
||||
log::warn!("Failed to set global event emitter: {e}");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[cfg(all(windows, not(feature = "e2e")))]
|
||||
{
|
||||
// For Windows, register all deep links at runtime
|
||||
if let Err(e) = app.deep_link().register_all() {
|
||||
@@ -1538,7 +1581,7 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(all(target_os = "macos", not(feature = "e2e")))]
|
||||
{
|
||||
// On macOS, try to register deep links for development builds
|
||||
if let Err(e) = app.deep_link().register_all() {
|
||||
@@ -1548,28 +1591,28 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
app.deep_link().on_open_url({
|
||||
let handle = handle.clone();
|
||||
move |event| {
|
||||
let urls = event.urls();
|
||||
log::info!("Deep link event received with {} URLs", urls.len());
|
||||
#[cfg(not(feature = "e2e"))]
|
||||
{
|
||||
app.deep_link().on_open_url({
|
||||
let handle = handle.clone();
|
||||
move |event| {
|
||||
let urls = event.urls();
|
||||
log::info!("Deep link event received with {} URLs", urls.len());
|
||||
|
||||
for url in urls {
|
||||
let url_string = url.to_string();
|
||||
log::info!("Deep link received: {url_string}");
|
||||
for url in urls {
|
||||
let url_string = url.to_string();
|
||||
log::info!("Deep link received: {url_string}");
|
||||
let handle_clone = handle.clone();
|
||||
|
||||
// Clone the handle for each async task
|
||||
let handle_clone = handle.clone();
|
||||
|
||||
// Handle the URL asynchronously
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = handle_url_open(handle_clone, url_string.clone()).await {
|
||||
log::error!("Failed to handle deep link URL: {e}");
|
||||
}
|
||||
});
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = handle_url_open(handle_clone, url_string.clone()).await {
|
||||
log::error!("Failed to handle deep link URL: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(startup_url) = startup_url {
|
||||
let handle_clone = handle.clone();
|
||||
@@ -1581,30 +1624,29 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize and start background version updater
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let version_updater = get_version_updater();
|
||||
if !e2e_automation_enabled() {
|
||||
// Initialize and start background version updater
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let version_updater = get_version_updater();
|
||||
|
||||
// Set the app handle
|
||||
{
|
||||
let mut updater_guard = version_updater.lock().await;
|
||||
updater_guard.set_app_handle(app_handle);
|
||||
}
|
||||
|
||||
// Run startup check without holding the lock
|
||||
{
|
||||
let updater_guard = version_updater.lock().await;
|
||||
if let Err(e) = updater_guard.start_background_updates().await {
|
||||
log::error!("Failed to start background updates: {e}");
|
||||
{
|
||||
let mut updater_guard = version_updater.lock().await;
|
||||
updater_guard.set_app_handle(app_handle);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start the background update task separately
|
||||
tauri::async_runtime::spawn(async move {
|
||||
version_updater::VersionUpdater::run_background_task().await;
|
||||
});
|
||||
{
|
||||
let updater_guard = version_updater.lock().await;
|
||||
if let Err(e) = updater_guard.start_background_updates().await {
|
||||
log::error!("Failed to start background updates: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
version_updater::VersionUpdater::run_background_task().await;
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-start MCP server if it was previously enabled. Always log the
|
||||
// decision so customer logs reveal whether MCP is actually running —
|
||||
@@ -1765,12 +1807,12 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
let app_handle_auto_updater = app.handle().clone();
|
||||
|
||||
// Start the auto-update check task separately
|
||||
tauri::async_runtime::spawn(async move {
|
||||
auto_updater::check_for_updates_with_progress(app_handle_auto_updater).await;
|
||||
});
|
||||
if !e2e_automation_enabled() {
|
||||
let app_handle_auto_updater = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
auto_updater::check_for_updates_with_progress(app_handle_auto_updater).await;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle any pending URLs that were received before the window was ready
|
||||
let handle_pending = handle.clone();
|
||||
@@ -1793,107 +1835,85 @@ pub fn run() {
|
||||
}
|
||||
});
|
||||
|
||||
// Start periodic cleanup task for unused binaries
|
||||
// Only runs when sync is not in progress to avoid deleting browsers
|
||||
// that might be needed for profiles being synced from the cloud
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(43200)); // Every 12 hours
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
// Check if sync is in progress before running cleanup
|
||||
if let Some(scheduler) = sync::get_global_scheduler() {
|
||||
if scheduler.is_sync_in_progress().await {
|
||||
log::debug!("Skipping cleanup: sync is in progress");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let registry =
|
||||
crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance();
|
||||
if let Err(e) = registry.cleanup_unused_binaries() {
|
||||
log::error!("Periodic cleanup failed: {e}");
|
||||
} else {
|
||||
log::debug!("Periodic cleanup completed successfully");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// DNS blocklist refresh task (every 12 hours)
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let manager = dns_blocklist::BlocklistManager::instance();
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(43200));
|
||||
interval.tick().await; // Skip the immediate first tick
|
||||
loop {
|
||||
interval.tick().await;
|
||||
manager.refresh_all_stale().await;
|
||||
}
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(3 * 60 * 60));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
log::info!("Checking for app updates...");
|
||||
// Route through check_for_app_updates (not the raw check_for_updates)
|
||||
// so the background loop respects portable mode and the
|
||||
// disable_auto_updates setting. Previously it bypassed both, so a
|
||||
// portable install would auto-download and run the NSIS installer,
|
||||
// clobbering the portable folder instead of updating in place.
|
||||
match app_auto_updater::check_for_app_updates().await {
|
||||
Ok(Some(update_info)) => {
|
||||
log::info!(
|
||||
"App update available: {} -> {}",
|
||||
update_info.current_version,
|
||||
update_info.new_version
|
||||
);
|
||||
if let Err(e) = events::emit("app-update-available", &update_info) {
|
||||
log::error!("Failed to emit app update event: {e}");
|
||||
if !e2e_automation_enabled() {
|
||||
// Start periodic cleanup task for unused binaries.
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(43200));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Some(scheduler) = sync::get_global_scheduler() {
|
||||
if scheduler.is_sync_in_progress().await {
|
||||
log::debug!("Skipping cleanup: sync is in progress");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
log::debug!("No app updates available");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check for app updates: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check and download GeoIP database at startup if needed
|
||||
let app_handle_geoip = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// Wait a bit for the app to fully initialize
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
let geoip_downloader = crate::geoip_downloader::GeoIPDownloader::instance();
|
||||
match geoip_downloader.check_missing_geoip_database() {
|
||||
Ok(true) => {
|
||||
log::info!(
|
||||
"GeoIP database is missing for Wayfern profiles, downloading at startup..."
|
||||
);
|
||||
let geoip_downloader = GeoIPDownloader::instance();
|
||||
if let Err(e) = geoip_downloader
|
||||
.download_geoip_database(&app_handle_geoip)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to download GeoIP database at startup: {e}");
|
||||
let registry =
|
||||
crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance();
|
||||
if let Err(e) = registry.cleanup_unused_binaries() {
|
||||
log::error!("Periodic cleanup failed: {e}");
|
||||
} else {
|
||||
log::info!("GeoIP database downloaded successfully at startup");
|
||||
log::debug!("Periodic cleanup completed successfully");
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
// No Wayfern profiles or GeoIP database already available
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let manager = dns_blocklist::BlocklistManager::instance();
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(43200));
|
||||
interval.tick().await;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
manager.refresh_all_stale().await;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check GeoIP database status at startup: {e}");
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(3 * 60 * 60));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
log::info!("Checking for app updates...");
|
||||
match app_auto_updater::check_for_app_updates().await {
|
||||
Ok(Some(update_info)) => {
|
||||
log::info!(
|
||||
"App update available: {} -> {}",
|
||||
update_info.current_version,
|
||||
update_info.new_version
|
||||
);
|
||||
if let Err(e) = events::emit("app-update-available", &update_info) {
|
||||
log::error!("Failed to emit app update event: {e}");
|
||||
}
|
||||
}
|
||||
Ok(None) => log::debug!("No app updates available"),
|
||||
Err(e) => log::error!("Failed to check for app updates: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let app_handle_geoip = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
let geoip_downloader = crate::geoip_downloader::GeoIPDownloader::instance();
|
||||
match geoip_downloader.check_missing_geoip_database() {
|
||||
Ok(true) => {
|
||||
log::info!(
|
||||
"GeoIP database is missing for Wayfern profiles, downloading at startup..."
|
||||
);
|
||||
let geoip_downloader = GeoIPDownloader::instance();
|
||||
if let Err(e) = geoip_downloader
|
||||
.download_geoip_database(&app_handle_geoip)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to download GeoIP database at startup: {e}");
|
||||
} else {
|
||||
log::info!("GeoIP database downloaded successfully at startup");
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => log::error!("Failed to check GeoIP database status at startup: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start proxy cleanup task for dead browser processes
|
||||
let app_handle_proxy_cleanup = app.handle().clone();
|
||||
|
||||
+120
-150
@@ -492,6 +492,9 @@ impl SyncEngine {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let reconciled_profile = self.reconcile_profile_metadata(profile).await?;
|
||||
let profile = &reconciled_profile;
|
||||
|
||||
// Derive encryption key if encrypted sync
|
||||
let encryption_key = if profile.is_encrypted_sync() {
|
||||
let password = encryption::load_e2e_password()
|
||||
@@ -697,11 +700,6 @@ impl SyncEngine {
|
||||
log::debug!("Deleted remote file: {}", path);
|
||||
}
|
||||
|
||||
// Upload metadata.json (sanitized profile)
|
||||
self
|
||||
.upload_profile_metadata(&profile_id, profile, &key_prefix)
|
||||
.await?;
|
||||
|
||||
// If this sync changed the local profile directory (downloaded files and/or
|
||||
// deleted local files), the manifest generated at the START of the sync is
|
||||
// now stale. Uploading it would advertise wrong hashes/mtimes for the files
|
||||
@@ -747,37 +745,18 @@ impl SyncEngine {
|
||||
let _ = self.sync_vpn(vpn_id, Some(app_handle)).await;
|
||||
}
|
||||
|
||||
// Download remote metadata and merge changes (name, tags, notes, etc.)
|
||||
let remote_metadata_key = format!("{}profiles/{}/metadata.json", key_prefix, profile_id);
|
||||
if let Ok(remote_meta) = self.download_profile_metadata(&remote_metadata_key).await {
|
||||
let mut updated_profile = profile.clone();
|
||||
// Merge fields that can be changed on other devices
|
||||
updated_profile.name = remote_meta.name;
|
||||
updated_profile.tags = remote_meta.tags;
|
||||
updated_profile.note = remote_meta.note;
|
||||
updated_profile.proxy_id = remote_meta.proxy_id;
|
||||
updated_profile.vpn_id = remote_meta.vpn_id;
|
||||
updated_profile.group_id = remote_meta.group_id;
|
||||
updated_profile.extension_group_id = remote_meta.extension_group_id;
|
||||
updated_profile.window_color = remote_meta.window_color;
|
||||
updated_profile.last_sync = Some(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
);
|
||||
let _ = profile_manager.save_profile(&updated_profile);
|
||||
} else {
|
||||
// Fallback: just update last_sync
|
||||
let mut updated_profile = profile.clone();
|
||||
updated_profile.last_sync = Some(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
);
|
||||
let _ = profile_manager.save_profile(&updated_profile);
|
||||
}
|
||||
let mut updated_profile = profile.clone();
|
||||
updated_profile.last_sync = Some(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
);
|
||||
profile_manager
|
||||
.save_profile(&updated_profile)
|
||||
.map_err(|e| {
|
||||
SyncError::IoError(format!("Failed to save reconciled profile metadata: {e}"))
|
||||
})?;
|
||||
let _ = events::emit("profiles-changed", ());
|
||||
|
||||
let _ = events::emit(
|
||||
@@ -881,6 +860,45 @@ impl SyncEngine {
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
async fn reconcile_profile_metadata(
|
||||
&self,
|
||||
profile: &BrowserProfile,
|
||||
) -> SyncResult<BrowserProfile> {
|
||||
let profile_id = profile.id.to_string();
|
||||
let key_prefix = Self::get_team_key_prefix(profile).await;
|
||||
let remote_key = format!("{key_prefix}profiles/{profile_id}/metadata.json");
|
||||
let stat = self.client.stat(&remote_key).await?;
|
||||
|
||||
if !stat.exists {
|
||||
self
|
||||
.upload_profile_metadata(&profile_id, profile, &key_prefix)
|
||||
.await?;
|
||||
return Ok(profile.clone());
|
||||
}
|
||||
|
||||
let local_updated = profile.updated_at.unwrap_or(0);
|
||||
let remote_updated = self.remote_updated_at(&stat, &remote_key).await;
|
||||
if local_updated > remote_updated {
|
||||
self
|
||||
.upload_profile_metadata(&profile_id, profile, &key_prefix)
|
||||
.await?;
|
||||
return Ok(profile.clone());
|
||||
}
|
||||
if remote_updated <= local_updated {
|
||||
return Ok(profile.clone());
|
||||
}
|
||||
|
||||
let mut remote = self.download_profile_metadata(&remote_key).await?;
|
||||
// Process state is device-local and deliberately stripped from uploads.
|
||||
remote.process_id = profile.process_id;
|
||||
remote.last_launch = profile.last_launch;
|
||||
remote.last_sync = profile.last_sync;
|
||||
ProfileManager::instance()
|
||||
.save_profile(&remote)
|
||||
.map_err(|e| SyncError::IoError(format!("Failed to save remote profile metadata: {e}")))?;
|
||||
Ok(remote)
|
||||
}
|
||||
|
||||
/// Sync only metadata for cross-OS profiles (tags, notes, proxies, groups).
|
||||
/// No browser files are synced.
|
||||
async fn sync_cross_os_metadata(
|
||||
@@ -889,34 +907,8 @@ impl SyncEngine {
|
||||
profile: &BrowserProfile,
|
||||
) -> SyncResult<()> {
|
||||
let profile_id = profile.id.to_string();
|
||||
let key_prefix = Self::get_team_key_prefix(profile).await;
|
||||
let profile_manager = ProfileManager::instance();
|
||||
|
||||
// Upload our metadata
|
||||
self
|
||||
.upload_profile_metadata(&profile_id, profile, &key_prefix)
|
||||
.await?;
|
||||
|
||||
// Download remote metadata and merge if remote has changes
|
||||
let remote_metadata_key = format!("{}profiles/{}/metadata.json", key_prefix, profile_id);
|
||||
if let Ok(remote_meta) = self.download_profile_metadata(&remote_metadata_key).await {
|
||||
let mut updated = profile.clone();
|
||||
updated.name = remote_meta.name;
|
||||
updated.tags = remote_meta.tags;
|
||||
updated.note = remote_meta.note;
|
||||
updated.proxy_id = remote_meta.proxy_id;
|
||||
updated.vpn_id = remote_meta.vpn_id;
|
||||
updated.group_id = remote_meta.group_id;
|
||||
updated.extension_group_id = remote_meta.extension_group_id;
|
||||
updated.window_color = remote_meta.window_color;
|
||||
updated.last_sync = Some(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
);
|
||||
let _ = profile_manager.save_profile(&updated);
|
||||
}
|
||||
let reconciled_profile = self.reconcile_profile_metadata(profile).await?;
|
||||
let profile = &reconciled_profile;
|
||||
|
||||
// Sync associated entities
|
||||
if let Some(proxy_id) = &profile.proxy_id {
|
||||
@@ -954,18 +946,9 @@ impl SyncEngine {
|
||||
let json = serde_json::to_string_pretty(&sanitized)
|
||||
.map_err(|e| SyncError::SerializationError(format!("Failed to serialize profile: {e}")))?;
|
||||
|
||||
let (payload, content_type) = encryption::maybe_seal_for_upload(json.as_bytes())
|
||||
.map_err(|e| SyncError::InvalidData(format!("Failed to seal profile metadata: {e}")))?;
|
||||
|
||||
let remote_key = format!("{}profiles/{}/metadata.json", key_prefix, profile_id);
|
||||
let presign = self
|
||||
.client
|
||||
.presign_upload(&remote_key, Some(content_type))
|
||||
.await?;
|
||||
|
||||
self
|
||||
.client
|
||||
.upload_bytes(&presign.url, &payload, Some(content_type))
|
||||
.upload_config_json(&remote_key, &json, sanitized.updated_at.unwrap_or(0))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -4023,28 +4006,55 @@ pub async fn rollover_encryption_for_all_entities(
|
||||
) -> Result<(), String> {
|
||||
let _ = events::emit("e2e-rollover-started", ());
|
||||
|
||||
let internal_error = |detail: String| {
|
||||
serde_json::json!({ "code": "INTERNAL_ERROR", "params": { "detail": detail } }).to_string()
|
||||
};
|
||||
let engine = SyncEngine::create_from_settings(&app_handle)
|
||||
.await
|
||||
.map_err(&internal_error)?;
|
||||
let profile_manager = ProfileManager::instance();
|
||||
let profiles = profile_manager
|
||||
.list_profiles()
|
||||
.map_err(|e| format!("Failed to list profiles: {e}"))?;
|
||||
.map_err(|e| internal_error(format!("Failed to list profiles: {e}")))?;
|
||||
|
||||
let synced_profiles: Vec<_> = profiles
|
||||
.iter()
|
||||
.filter(|p| p.sync_mode != SyncMode::Disabled)
|
||||
.collect();
|
||||
|
||||
let total_profiles = synced_profiles.len();
|
||||
let mut running_profile_ids: std::collections::HashSet<uuid::Uuid> =
|
||||
std::collections::HashSet::new();
|
||||
if synced_profiles
|
||||
.iter()
|
||||
.any(|profile| profile.process_id.is_some())
|
||||
{
|
||||
return Err(serde_json::json!({ "code": "PROFILE_RUNNING" }).to_string());
|
||||
}
|
||||
|
||||
let total_profiles = synced_profiles.len();
|
||||
for (i, profile) in synced_profiles.iter().enumerate() {
|
||||
if profile.process_id.is_some() {
|
||||
running_profile_ids.insert(profile.id);
|
||||
}
|
||||
let id_str = profile.id.to_string();
|
||||
if let Err(e) = trigger_sync_for_profile(app_handle.clone(), id_str.clone()).await {
|
||||
log::warn!("Rollover: profile {} re-sync failed: {e}", id_str);
|
||||
}
|
||||
// The remote manifest may be encrypted with the previous password. Delete
|
||||
// only that manifest so the normal sync path treats every local file as an
|
||||
// upload and rewrites it with the current password. Existing remote files
|
||||
// remain available until their replacements have uploaded.
|
||||
let key_prefix = SyncEngine::get_team_key_prefix(profile).await;
|
||||
engine
|
||||
.upload_profile_metadata(&id_str, profile, &key_prefix)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
internal_error(format!(
|
||||
"Failed to roll over profile metadata {id_str}: {e}"
|
||||
))
|
||||
})?;
|
||||
let manifest_key = format!("{key_prefix}profiles/{id_str}/manifest.json");
|
||||
engine
|
||||
.client
|
||||
.delete(&manifest_key, None)
|
||||
.await
|
||||
.map_err(|e| internal_error(format!("Failed to reset profile manifest: {e}")))?;
|
||||
engine
|
||||
.sync_profile(&app_handle, profile)
|
||||
.await
|
||||
.map_err(|e| internal_error(format!("Failed to roll over profile {id_str}: {e}")))?;
|
||||
let _ = events::emit(
|
||||
"e2e-rollover-progress",
|
||||
serde_json::json!({
|
||||
@@ -4055,37 +4065,14 @@ pub async fn rollover_encryption_for_all_entities(
|
||||
);
|
||||
}
|
||||
|
||||
// Determine which entity ids are referenced by running profiles, so we can
|
||||
// defer their re-upload (changing their files mid-session would cause the
|
||||
// running browser to see a different proxy/extension config than what it
|
||||
// launched with).
|
||||
let mut deferred_proxy_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut deferred_vpn_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut deferred_group_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
for p in &profiles {
|
||||
if running_profile_ids.contains(&p.id) {
|
||||
if let Some(id) = &p.proxy_id {
|
||||
deferred_proxy_ids.insert(id.clone());
|
||||
}
|
||||
if let Some(id) = &p.vpn_id {
|
||||
deferred_vpn_ids.insert(id.clone());
|
||||
}
|
||||
if let Some(id) = &p.group_id {
|
||||
deferred_group_ids.insert(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let proxies = crate::proxy_manager::PROXY_MANAGER.get_stored_proxies();
|
||||
let synced_proxies: Vec<_> = proxies.iter().filter(|p| p.sync_enabled).collect();
|
||||
let total_proxies = synced_proxies.len();
|
||||
let mut deferred = Vec::new();
|
||||
for (i, proxy) in synced_proxies.iter().enumerate() {
|
||||
if deferred_proxy_ids.contains(&proxy.id) {
|
||||
deferred.push(proxy.id.clone());
|
||||
} else if let Some(scheduler) = super::get_global_scheduler() {
|
||||
scheduler.queue_proxy_sync(proxy.id.clone()).await;
|
||||
}
|
||||
engine
|
||||
.upload_proxy(proxy)
|
||||
.await
|
||||
.map_err(|e| internal_error(format!("Failed to roll over proxy {}: {e}", proxy.id)))?;
|
||||
let _ = events::emit(
|
||||
"e2e-rollover-progress",
|
||||
serde_json::json!({"stage": "proxies", "done": i + 1, "total": total_proxies}),
|
||||
@@ -4095,17 +4082,15 @@ pub async fn rollover_encryption_for_all_entities(
|
||||
let groups = {
|
||||
let gm = crate::group_manager::GROUP_MANAGER.lock().unwrap();
|
||||
gm.get_all_groups()
|
||||
.map_err(|e| format!("Failed to get groups: {e}"))?
|
||||
.map_err(|e| internal_error(format!("Failed to get groups: {e}")))?
|
||||
};
|
||||
let synced_groups: Vec<_> = groups.iter().filter(|g| g.sync_enabled).collect();
|
||||
let total_groups = synced_groups.len();
|
||||
let mut deferred_groups = Vec::new();
|
||||
for (i, group) in synced_groups.iter().enumerate() {
|
||||
if deferred_group_ids.contains(&group.id) {
|
||||
deferred_groups.push(group.id.clone());
|
||||
} else if let Some(scheduler) = super::get_global_scheduler() {
|
||||
scheduler.queue_group_sync(group.id.clone()).await;
|
||||
}
|
||||
engine
|
||||
.upload_group(group)
|
||||
.await
|
||||
.map_err(|e| internal_error(format!("Failed to roll over group {}: {e}", group.id)))?;
|
||||
let _ = events::emit(
|
||||
"e2e-rollover-progress",
|
||||
serde_json::json!({"stage": "groups", "done": i + 1, "total": total_groups}),
|
||||
@@ -4116,17 +4101,15 @@ pub async fn rollover_encryption_for_all_entities(
|
||||
let storage = crate::vpn::VPN_STORAGE.lock().unwrap();
|
||||
storage
|
||||
.list_configs()
|
||||
.map_err(|e| format!("Failed to list VPN configs: {e}"))?
|
||||
.map_err(|e| internal_error(format!("Failed to list VPN configs: {e}")))?
|
||||
};
|
||||
let synced_vpns: Vec<_> = vpns.iter().filter(|v| v.sync_enabled).collect();
|
||||
let total_vpns = synced_vpns.len();
|
||||
let mut deferred_vpns = Vec::new();
|
||||
for (i, config) in synced_vpns.iter().enumerate() {
|
||||
if deferred_vpn_ids.contains(&config.id) {
|
||||
deferred_vpns.push(config.id.clone());
|
||||
} else if let Some(scheduler) = super::get_global_scheduler() {
|
||||
scheduler.queue_vpn_sync(config.id.clone()).await;
|
||||
}
|
||||
engine
|
||||
.upload_vpn(config)
|
||||
.await
|
||||
.map_err(|e| internal_error(format!("Failed to roll over VPN {}: {e}", config.id)))?;
|
||||
let _ = events::emit(
|
||||
"e2e-rollover-progress",
|
||||
serde_json::json!({"stage": "vpns", "done": i + 1, "total": total_vpns}),
|
||||
@@ -4136,14 +4119,15 @@ pub async fn rollover_encryption_for_all_entities(
|
||||
let extensions = {
|
||||
let em = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap();
|
||||
em.list_extensions()
|
||||
.map_err(|e| format!("Failed to list extensions: {e}"))?
|
||||
.map_err(|e| internal_error(format!("Failed to list extensions: {e}")))?
|
||||
};
|
||||
let synced_exts: Vec<_> = extensions.iter().filter(|e| e.sync_enabled).collect();
|
||||
let total_exts = synced_exts.len();
|
||||
for (i, ext) in synced_exts.iter().enumerate() {
|
||||
if let Some(scheduler) = super::get_global_scheduler() {
|
||||
scheduler.queue_extension_sync(ext.id.clone()).await;
|
||||
}
|
||||
engine
|
||||
.upload_extension(ext)
|
||||
.await
|
||||
.map_err(|e| internal_error(format!("Failed to roll over extension {}: {e}", ext.id)))?;
|
||||
let _ = events::emit(
|
||||
"e2e-rollover-progress",
|
||||
serde_json::json!({"stage": "extensions", "done": i + 1, "total": total_exts}),
|
||||
@@ -4153,37 +4137,23 @@ pub async fn rollover_encryption_for_all_entities(
|
||||
let ext_groups = {
|
||||
let em = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap();
|
||||
em.list_groups()
|
||||
.map_err(|e| format!("Failed to list extension groups: {e}"))?
|
||||
.map_err(|e| internal_error(format!("Failed to list extension groups: {e}")))?
|
||||
};
|
||||
let synced_ext_groups: Vec<_> = ext_groups.iter().filter(|g| g.sync_enabled).collect();
|
||||
let total_eg = synced_ext_groups.len();
|
||||
for (i, group) in synced_ext_groups.iter().enumerate() {
|
||||
if let Some(scheduler) = super::get_global_scheduler() {
|
||||
scheduler.queue_extension_group_sync(group.id.clone()).await;
|
||||
}
|
||||
engine.upload_extension_group(group).await.map_err(|e| {
|
||||
internal_error(format!(
|
||||
"Failed to roll over extension group {}: {e}",
|
||||
group.id
|
||||
))
|
||||
})?;
|
||||
let _ = events::emit(
|
||||
"e2e-rollover-progress",
|
||||
serde_json::json!({"stage": "extension_groups", "done": i + 1, "total": total_eg}),
|
||||
);
|
||||
}
|
||||
|
||||
if !deferred.is_empty() || !deferred_groups.is_empty() || !deferred_vpns.is_empty() {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
if let Some(scheduler) = super::get_global_scheduler() {
|
||||
for id in deferred {
|
||||
scheduler.queue_proxy_sync(id).await;
|
||||
}
|
||||
for id in deferred_groups {
|
||||
scheduler.queue_group_sync(id).await;
|
||||
}
|
||||
for id in deferred_vpns {
|
||||
scheduler.queue_vpn_sync(id).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let _ = events::emit("e2e-rollover-completed", ());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use super::types::{SyncError, SyncResult};
|
||||
use crate::profile::types::BrowserProfile;
|
||||
|
||||
/// Default exclude patterns for volatile browser profile files.
|
||||
/// Patterns use `**/` prefix to match at any directory depth, since the sync
|
||||
@@ -61,6 +60,10 @@ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[
|
||||
"**/DawnWebGPUCache/**",
|
||||
"**/BrowserMetrics*",
|
||||
"**/.DS_Store",
|
||||
// Profile metadata is a separately reconciled LWW config object. Including
|
||||
// it in the browser-file manifest creates two competing sync mechanisms and
|
||||
// lets a stale in-memory profile overwrite a metadata download.
|
||||
"metadata.json",
|
||||
".donut-sync/**",
|
||||
// Orphaned local-only marker from earlier rollover-based fingerprint
|
||||
// regeneration. Keep excluding it so any markers left on disk from
|
||||
@@ -224,39 +227,6 @@ fn hash_file(path: &Path) -> Result<Option<String>, SyncError> {
|
||||
Ok(Some(hasher.finalize().to_hex().to_string()))
|
||||
}
|
||||
|
||||
/// Compute blake3 hash of metadata.json after sanitizing volatile fields.
|
||||
/// This prevents infinite sync loops where updating last_sync triggers a new sync.
|
||||
fn hash_sanitized_metadata(path: &Path) -> Result<Option<String>, SyncError> {
|
||||
let content = match fs::read_to_string(path) {
|
||||
Ok(c) => c,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) => {
|
||||
return Err(SyncError::IoError(format!(
|
||||
"Failed to read metadata at {}: {e}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let mut profile: BrowserProfile = serde_json::from_str(&content).map_err(|e| {
|
||||
SyncError::SerializationError(format!("Failed to parse metadata for hashing: {e}"))
|
||||
})?;
|
||||
|
||||
// Sanitize volatile fields that should not trigger a re-sync
|
||||
profile.last_sync = None;
|
||||
profile.process_id = None;
|
||||
profile.last_launch = None;
|
||||
|
||||
let sanitized_json = serde_json::to_string(&profile).map_err(|e| {
|
||||
SyncError::SerializationError(format!("Failed to serialize sanitized metadata: {e}"))
|
||||
})?;
|
||||
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(sanitized_json.as_bytes());
|
||||
|
||||
Ok(Some(hasher.finalize().to_hex().to_string()))
|
||||
}
|
||||
|
||||
/// Get mtime as unix timestamp
|
||||
/// Returns None if the file doesn't exist (was deleted)
|
||||
fn get_mtime(path: &Path) -> Result<Option<i64>, SyncError> {
|
||||
@@ -372,19 +342,7 @@ pub fn generate_manifest(
|
||||
*max_mtime = (*max_mtime).max(mtime);
|
||||
|
||||
// Check cache for existing hash
|
||||
let hash = if relative_path == "metadata.json" {
|
||||
// Special case: sanitize metadata.json before hashing to prevent sync loops
|
||||
match hash_sanitized_metadata(&path)? {
|
||||
Some(computed_hash) => computed_hash,
|
||||
None => {
|
||||
log::debug!(
|
||||
"File disappeared during manifest generation, skipping: {}",
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if let Some(cached_hash) = cache.get(&relative_path, size, mtime) {
|
||||
let hash = if let Some(cached_hash) = cache.get(&relative_path, size, mtime) {
|
||||
cached_hash.to_string()
|
||||
} else {
|
||||
match hash_file(&path)? {
|
||||
@@ -651,21 +609,15 @@ mod tests {
|
||||
fs::create_dir_all(profile_dir.join("profile/Crashpad")).unwrap();
|
||||
fs::write(profile_dir.join("profile/Crashpad/report"), "exclude").unwrap();
|
||||
|
||||
// metadata.json at root
|
||||
let profile = BrowserProfile::default();
|
||||
fs::write(
|
||||
profile_dir.join("metadata.json"),
|
||||
serde_json::to_string(&profile).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(profile_dir.join("metadata.json"), "{}").unwrap();
|
||||
|
||||
let mut cache = HashCache::default();
|
||||
let manifest = generate_manifest("test-profile", &profile_dir, &mut cache).unwrap();
|
||||
|
||||
let paths: Vec<&str> = manifest.files.iter().map(|f| f.path.as_str()).collect();
|
||||
assert!(
|
||||
paths.contains(&"metadata.json"),
|
||||
"metadata.json should be synced"
|
||||
!paths.contains(&"metadata.json"),
|
||||
"metadata.json is reconciled separately from browser files"
|
||||
);
|
||||
assert!(
|
||||
paths.contains(&"profile/Default/Cookies"),
|
||||
@@ -865,85 +817,4 @@ mod tests {
|
||||
assert!(diff.files_to_delete_remote.is_empty());
|
||||
assert!(diff.files_to_delete_local.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_manifest_sanitizes_metadata() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let profile_dir = temp_dir.path().join("profile");
|
||||
fs::create_dir_all(&profile_dir).unwrap();
|
||||
|
||||
let profile_id = uuid::Uuid::new_v4();
|
||||
let metadata_path = profile_dir.join("metadata.json");
|
||||
|
||||
let profile = BrowserProfile {
|
||||
id: profile_id,
|
||||
name: "test-profile".to_string(),
|
||||
last_sync: Some(100),
|
||||
process_id: Some(1234),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
fs::write(&metadata_path, serde_json::to_string(&profile).unwrap()).unwrap();
|
||||
|
||||
let mut cache = HashCache::default();
|
||||
let manifest1 = generate_manifest(&profile_id.to_string(), &profile_dir, &mut cache).unwrap();
|
||||
let hash1 = manifest1
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.path == "metadata.json")
|
||||
.unwrap()
|
||||
.hash
|
||||
.clone();
|
||||
|
||||
// Update volatile fields
|
||||
let profile2 = BrowserProfile {
|
||||
id: profile_id,
|
||||
name: "test-profile".to_string(),
|
||||
last_sync: Some(200),
|
||||
process_id: Some(5678),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
fs::write(&metadata_path, serde_json::to_string(&profile2).unwrap()).unwrap();
|
||||
|
||||
let manifest2 = generate_manifest(&profile_id.to_string(), &profile_dir, &mut cache).unwrap();
|
||||
let hash2 = manifest2
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.path == "metadata.json")
|
||||
.unwrap()
|
||||
.hash
|
||||
.clone();
|
||||
|
||||
// Hash should be identical because volatile fields are sanitized
|
||||
assert_eq!(
|
||||
hash1, hash2,
|
||||
"Metadata hash should be stable across last_sync/process_id updates"
|
||||
);
|
||||
|
||||
// Change a non-volatile field
|
||||
let profile3 = BrowserProfile {
|
||||
id: profile_id,
|
||||
name: "changed-name".to_string(),
|
||||
last_sync: Some(200),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
fs::write(&metadata_path, serde_json::to_string(&profile3).unwrap()).unwrap();
|
||||
|
||||
let manifest3 = generate_manifest(&profile_id.to_string(), &profile_dir, &mut cache).unwrap();
|
||||
let hash3 = manifest3
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.path == "metadata.json")
|
||||
.unwrap()
|
||||
.hash
|
||||
.clone();
|
||||
|
||||
// Hash should be different because name changed
|
||||
assert_ne!(
|
||||
hash1, hash3,
|
||||
"Metadata hash should change when non-volatile fields change"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user