mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-21 23:01:26 +02:00
feat: Windows RAM profiles
This commit is contained in:
1 parent
f493c9ef7c
commit
58b0febe42
29 files changed
+3087
-109
No files matched your search
@@ -76,6 +76,7 @@ donutbrowser/
|
||||
│ │ ├── feedback.rs # Feedback to the Donut team from the dialog and the send_feedback MCP tool, optional redacted logs
|
||||
│ │ ├── sync/ # Cloud sync (engine, encryption, manifest, scheduler)
|
||||
│ │ ├── vpn/ # WireGuard tunnels
|
||||
│ │ ├── ramfs/ # Windows RAM volume for ephemeral profiles: in-memory file system on WinFsp, hosted by donut-proxy
|
||||
│ │ ├── xray/ # Xray share links (VLESS, VMess, Trojan, Hysteria2): parse, export, client config
|
||||
│ │ ├── xray_worker_runner.rs # Xray-core sidecar lifecycle
|
||||
│ │ ├── xray_worker_storage.rs # Private Xray worker state and runtime files
|
||||
|
||||
@@ -78,6 +78,7 @@ export const commandCoverage = {
|
||||
"profile_totp::add_profile_totp",
|
||||
"profile_totp::delete_profile_totp",
|
||||
"profile_totp::get_profile_totp_codes",
|
||||
"ephemeral_dirs::get_ephemeral_storage_status",
|
||||
"rename_profile",
|
||||
"detect_existing_profiles",
|
||||
"import_browser_profiles",
|
||||
|
||||
@@ -1522,6 +1522,17 @@ test("a storage state file moves cookies and Local Storage into a closed profile
|
||||
}),
|
||||
/LOCAL_STORAGE_EPHEMERAL/,
|
||||
);
|
||||
|
||||
const storage = await app.invoke("get_ephemeral_storage_status");
|
||||
assert.deepEqual(Object.keys(storage).sort(), ["in_memory", "reason"]);
|
||||
if (process.platform === "win32") {
|
||||
assert.equal(storage.in_memory, storage.reason === null);
|
||||
if (storage.reason !== null) {
|
||||
assert.match(storage.reason, /^winfsp_(missing|error)$/);
|
||||
}
|
||||
} else {
|
||||
assert.deepEqual(storage, { in_memory: true, reason: null });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Generated
+284
-65
@@ -182,7 +182,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -193,7 +193,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -224,7 +224,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
@@ -470,7 +470,7 @@ dependencies = [
|
||||
"smappservice-rs",
|
||||
"thiserror 2.0.21",
|
||||
"windows-registry",
|
||||
"windows-result",
|
||||
"windows-result 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -508,7 +508,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"log",
|
||||
"nom",
|
||||
"nom 8.0.0",
|
||||
"num-rational",
|
||||
"v_frame",
|
||||
]
|
||||
@@ -601,6 +601,26 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex 1.3.0",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
@@ -957,7 +977,7 @@ dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
"shlex 2.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -966,6 +986,15 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfb"
|
||||
version = "0.10.0"
|
||||
@@ -1056,7 +1085,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1091,6 +1120,17 @@ dependencies = [
|
||||
"inout 0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.7"
|
||||
@@ -1672,7 +1712,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1837,7 +1877,9 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-axum",
|
||||
"uuid",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
"winfsp",
|
||||
"winfsp-sys",
|
||||
"winreg",
|
||||
"wiremock",
|
||||
"yaml-rust2",
|
||||
@@ -2071,7 +2113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2560,7 +2602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3119,7 +3161,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3424,6 +3466,15 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -3476,7 +3527,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3666,7 +3717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
|
||||
dependencies = [
|
||||
"gtk-sys",
|
||||
"libloading",
|
||||
"libloading 0.7.4",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
@@ -3711,6 +3762,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
@@ -3907,6 +3968,12 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -3990,7 +4057,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4073,6 +4140,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
@@ -4625,7 +4702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.45.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4679,7 +4756,7 @@ dependencies = [
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5009,6 +5086,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
@@ -5266,7 +5353,7 @@ dependencies = [
|
||||
"built",
|
||||
"cfg-if",
|
||||
"interpolate_name",
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"libc",
|
||||
"libfuzzer-sys",
|
||||
"log",
|
||||
@@ -5621,7 +5708,7 @@ dependencies = [
|
||||
"errno 0.3.14",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6161,6 +6248,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
@@ -6289,7 +6382,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6377,6 +6470,12 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "static_vcruntime"
|
||||
version = "3.0.0"
|
||||
@@ -6522,7 +6621,7 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"objc2-open-directory",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6593,8 +6692,8 @@ dependencies = [
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
]
|
||||
@@ -6676,7 +6775,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6806,7 +6905,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry",
|
||||
"windows-result",
|
||||
"windows-result 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6925,7 +7024,7 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.21",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -7005,7 +7104,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7030,7 +7129,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
"wry",
|
||||
]
|
||||
|
||||
@@ -7090,7 +7189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
@@ -7101,10 +7200,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7590,7 +7689,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7600,7 +7699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"nom",
|
||||
"nom 8.0.0",
|
||||
"petgraph",
|
||||
]
|
||||
|
||||
@@ -7669,7 +7768,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8160,8 +8259,8 @@ checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8182,8 +8281,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8192,6 +8291,12 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -8214,7 +8319,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8235,20 +8340,42 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections 0.2.0",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future 0.2.1",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics 0.2.0",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"windows-collections 0.3.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-future 0.3.2",
|
||||
"windows-numerics 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8257,7 +8384,20 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8268,9 +8408,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8279,9 +8430,9 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
"windows-threading 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8306,20 +8457,36 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[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.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8328,9 +8495,18 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8339,7 +8515,16 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8348,7 +8533,7 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8384,7 +8569,7 @@ version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8424,7 +8609,7 @@ version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
@@ -8435,13 +8620,22 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8450,7 +8644,7 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8591,6 +8785,31 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winfsp"
|
||||
version = "0.13.1+winfsp-2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5132f201f1a350673ec9eb7d13be9162e16ea057a44043ef64635aa749b16c53"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.21",
|
||||
"widestring",
|
||||
"windows 0.61.3",
|
||||
"winfsp-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winfsp-sys"
|
||||
version = "0.12.1+winfsp-2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d42acc30c105f8d33507f556398237c738b681271c775a3b94eefd29d2b8c77e"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
@@ -8616,7 +8835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8710,8 +8929,8 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
]
|
||||
|
||||
@@ -182,7 +182,18 @@ windows = { version = "0.62", features = [
|
||||
# CryptUnprotectData, for unwrapping the source browser's os_crypt key from
|
||||
# `Local State` during profile import.
|
||||
"Win32_Security_Cryptography",
|
||||
# The RAM volume (src/ramfs): its root security descriptor, and loading the
|
||||
# installed WinFsp DLL by full path.
|
||||
"Win32_Security_Authorization",
|
||||
"Win32_System_LibraryLoader",
|
||||
] }
|
||||
# WinFsp, for the RAM volume of ephemeral profiles. Donut does not ship the
|
||||
# driver; the DLL is delay-loaded and used only when it is installed.
|
||||
winfsp = { version = "0.13", default-features = false }
|
||||
winfsp-sys = "0.12"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winfsp = { version = "0.13", default-features = false, features = ["delayload"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.27.0"
|
||||
|
||||
@@ -15,6 +15,13 @@ fn main() {
|
||||
println!("cargo:rustc-link-lib=framework=CoreServices");
|
||||
}
|
||||
|
||||
// WinFsp's DLL loads at the first call into it, so Donut starts on machines
|
||||
// that do not have WinFsp (src/ramfs).
|
||||
#[cfg(windows)]
|
||||
if build_target.contains("windows") {
|
||||
winfsp::build::winfsp_link_delayload();
|
||||
}
|
||||
|
||||
// Inject build version based on environment variables set by CI
|
||||
if let Ok(build_tag) = std::env::var("BUILD_TAG") {
|
||||
// Custom BUILD_TAG takes highest priority (used for nightly builds)
|
||||
|
||||
@@ -271,6 +271,31 @@ async fn main() {
|
||||
.help("Direct path to the Xray worker config JSON file"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("ramfs")
|
||||
.about("Serve the in-memory volume of ephemeral profiles until killed (Windows with WinFsp; internal use)")
|
||||
.arg(
|
||||
Arg::new("mount")
|
||||
.long("mount")
|
||||
.required(true)
|
||||
.value_parser(clap::value_parser!(std::path::PathBuf))
|
||||
.help("Directory to mount the volume on; it must not exist"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("capacity")
|
||||
.long("capacity")
|
||||
.required(true)
|
||||
.value_parser(clap::value_parser!(u64))
|
||||
.help("Volume size in bytes"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("state")
|
||||
.long("state")
|
||||
.required(true)
|
||||
.value_parser(clap::value_parser!(std::path::PathBuf))
|
||||
.help("File that records the mounted volume"),
|
||||
),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if let Some(proxy_matches) = matches.subcommand_matches("proxy") {
|
||||
@@ -645,6 +670,17 @@ async fn main() {
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
} else if let Some(ramfs_matches) = matches.subcommand_matches("ramfs") {
|
||||
let mount = ramfs_matches
|
||||
.get_one::<std::path::PathBuf>("mount")
|
||||
.expect("mount is required");
|
||||
let capacity = *ramfs_matches
|
||||
.get_one::<u64>("capacity")
|
||||
.expect("capacity is required");
|
||||
let state = ramfs_matches
|
||||
.get_one::<std::path::PathBuf>("state")
|
||||
.expect("state is required");
|
||||
process::exit(donutbrowser_lib::ramfs::host_main(mount, capacity, state));
|
||||
} else {
|
||||
eprintln!("No command specified");
|
||||
process::exit(1);
|
||||
|
||||
@@ -680,7 +680,11 @@ impl BrowserRunner {
|
||||
Some(prepared)
|
||||
} else {
|
||||
if profile.ephemeral {
|
||||
crate::ephemeral_dirs::create_ephemeral_dir(&profile.id.to_string())
|
||||
// Can mount a RAM disk or start the RAM volume's host and wait for it.
|
||||
let id = profile.id.to_string();
|
||||
tokio::task::spawn_blocking(move || crate::ephemeral_dirs::create_ephemeral_dir(&id))
|
||||
.await
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { e.to_string().into() })?
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { e.into() })?;
|
||||
}
|
||||
None
|
||||
|
||||
@@ -87,7 +87,7 @@ fn earlier_base_exists() -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
let ram = Path::new(MACOS_RAM_DISK).is_dir();
|
||||
#[cfg(target_os = "windows")]
|
||||
let ram = existing_windows_ramdisk().is_some();
|
||||
let ram = existing_windows_ramdisk().is_some() || crate::ramfs::live_volume().is_some();
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
||||
let ram = false;
|
||||
ram || disk_fallback_base().exists()
|
||||
@@ -96,10 +96,11 @@ fn earlier_base_exists() -> bool {
|
||||
/// Get or create the base directory for ephemeral profiles, and report whether
|
||||
/// it is actually RAM-backed.
|
||||
///
|
||||
/// Linux: /dev/shm (always tmpfs). macOS: RAM disk via hdiutil. Windows: imdisk
|
||||
/// RAM disk, which is a third-party driver this app does not ship, so on most
|
||||
/// Windows machines the disk fallback is the normal path rather than an edge
|
||||
/// case. Callers must treat `Disk` as a downgrade and erase accordingly.
|
||||
/// Linux: /dev/shm (always tmpfs). macOS: RAM disk via hdiutil. Windows: a RAM
|
||||
/// disk the person made, else the WinFsp volume (`crate::ramfs`). WinFsp is a
|
||||
/// driver this app does not ship, so on many Windows machines the disk
|
||||
/// fallback is the normal path rather than an edge case. Callers must treat
|
||||
/// `Disk` as a downgrade and erase accordingly.
|
||||
fn get_ephemeral_base_dir() -> Result<(PathBuf, EphemeralBacking), String> {
|
||||
if let Some(base) = ephemeral_base_override() {
|
||||
std::fs::create_dir_all(&base)
|
||||
@@ -203,44 +204,14 @@ fn existing_windows_ramdisk() -> Option<PathBuf> {
|
||||
.find(|base| base.is_dir())
|
||||
}
|
||||
|
||||
/// A RAM disk the person made is used as it is. Creating one needs admin
|
||||
/// rights, so Donut does not try; it mounts its WinFsp volume instead.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_or_create_windows_ramdisk() -> Result<PathBuf, String> {
|
||||
if let Some(base) = existing_windows_ramdisk() {
|
||||
return Ok(base);
|
||||
}
|
||||
|
||||
// Try to create a RAM disk using imdisk (open-source RAM disk driver)
|
||||
for letter in ['R', 'Q', 'P', 'O'] {
|
||||
let drive = format!("{}:", letter);
|
||||
if PathBuf::from(format!("{}\\", drive)).exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let output = std::process::Command::new("imdisk")
|
||||
.args(["-a", "-s", "256M", "-m", &drive, "-p", "/fs:ntfs /q /y"])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) if out.status.success() => {
|
||||
let base = PathBuf::from(format!("{}\\DonutEphemeral", drive));
|
||||
std::fs::create_dir_all(&base)
|
||||
.map_err(|e| format!("Failed to create dir on RAM disk: {e}"))?;
|
||||
log::info!("RAM disk created path={} size_mb=256", base.display());
|
||||
return Ok(base);
|
||||
}
|
||||
Ok(out) => {
|
||||
log::debug!(
|
||||
"imdisk failed drive={drive} err=\"{}\"",
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("imdisk not available: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not create Windows RAM disk".to_string())
|
||||
crate::ramfs::ensure_volume()
|
||||
}
|
||||
|
||||
pub fn create_ephemeral_dir(profile_id: &str) -> Result<PathBuf, String> {
|
||||
@@ -483,20 +454,37 @@ fn sweep_disk_fallback_residue(current_base: &Path) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the macOS RAM disk is mounted. `None` on other systems.
|
||||
/// Whether the macOS RAM disk or the Windows RAM volume is mounted. `None`
|
||||
/// where neither can exist.
|
||||
pub fn ram_disk_mounted() -> Option<bool> {
|
||||
#[cfg(target_os = "macos")]
|
||||
return Some(Path::new(MACOS_RAM_DISK).is_dir());
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
return crate::ramfs::winfsp_available()
|
||||
.ok()
|
||||
.map(|()| crate::ramfs::live_volume().is_some());
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
None
|
||||
}
|
||||
|
||||
/// Detach the macOS RAM disk when no profile directory is on it. Nothing else
|
||||
/// detaches it, so it would stay mounted until the Mac restarts.
|
||||
/// Detach the macOS RAM disk, or stop the Windows RAM volume's host, when no
|
||||
/// profile directory is on it. Nothing else does, so the macOS disk would stay
|
||||
/// mounted until the Mac restarts, and the Windows host would keep running.
|
||||
///
|
||||
/// Runs at exit, where `hdiutil detach` takes seconds, so the detach is
|
||||
/// started and not waited for: it finishes after Donut is gone.
|
||||
pub fn detach_unused_ram_disk() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if ephemeral_base_override().is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some(volume) = crate::ramfs::live_volume() {
|
||||
if holds_no_profile_dirs(&volume.mount) {
|
||||
crate::ramfs::stop_host(volume.pid, "unused_at_exit");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Stdio;
|
||||
@@ -522,7 +510,7 @@ pub fn detach_unused_ram_disk() {
|
||||
}
|
||||
|
||||
/// False when `path` cannot be read, so a missing volume is never detached.
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
|
||||
fn holds_no_profile_dirs(path: &Path) -> bool {
|
||||
std::fs::read_dir(path).is_ok_and(|entries| {
|
||||
entries
|
||||
@@ -557,6 +545,41 @@ fn cleanup_legacy_dirs() {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct EphemeralStorageStatus {
|
||||
in_memory: bool,
|
||||
/// `winfsp_missing` or `winfsp_error` when not in memory.
|
||||
reason: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// Whether ephemeral profiles keep their data in memory on this computer.
|
||||
/// Mounts nothing.
|
||||
#[tauri::command]
|
||||
pub async fn get_ephemeral_storage_status() -> EphemeralStorageStatus {
|
||||
storage_status()
|
||||
}
|
||||
|
||||
fn storage_status() -> EphemeralStorageStatus {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if existing_windows_ramdisk().is_none() {
|
||||
let reason = match crate::ramfs::status() {
|
||||
crate::ramfs::Status::InMemory => None,
|
||||
crate::ramfs::Status::WinfspMissing => Some("winfsp_missing"),
|
||||
crate::ramfs::Status::WinfspError => Some("winfsp_error"),
|
||||
};
|
||||
return EphemeralStorageStatus {
|
||||
in_memory: reason.is_none(),
|
||||
reason,
|
||||
};
|
||||
}
|
||||
}
|
||||
EphemeralStorageStatus {
|
||||
in_memory: true,
|
||||
reason: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_effective_profile_path(profile: &BrowserProfile, profiles_dir: &Path) -> PathBuf {
|
||||
if profile.ephemeral || profile.password_protected {
|
||||
if let Some(dir) = get_ephemeral_dir(&profile.id.to_string()) {
|
||||
@@ -736,6 +759,19 @@ mod tests {
|
||||
assert!(get_ephemeral_dir(&id).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_status_is_in_memory_or_names_its_reason() {
|
||||
let status = serde_json::to_value(storage_status()).unwrap();
|
||||
let in_memory = status["in_memory"].as_bool().unwrap();
|
||||
let reason = status["reason"].as_str();
|
||||
assert_eq!(in_memory, reason.is_none());
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert!(in_memory);
|
||||
if let Some(reason) = reason {
|
||||
assert!(["winfsp_missing", "winfsp_error"].contains(&reason));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn removing_an_unknown_profile_succeeds_without_doing_anything() {
|
||||
|
||||
@@ -175,6 +175,7 @@ pub mod proxy_runner;
|
||||
pub mod proxy_server;
|
||||
pub mod proxy_storage;
|
||||
pub mod proxy_udp;
|
||||
pub mod ramfs;
|
||||
mod recipes;
|
||||
mod recorder;
|
||||
mod remote_exit;
|
||||
@@ -3725,6 +3726,7 @@ pub fn run_with_builder(
|
||||
unlock_profile,
|
||||
lock_profile,
|
||||
is_profile_locked,
|
||||
ephemeral_dirs::get_ephemeral_storage_status,
|
||||
sync::restore_points::list_restore_points,
|
||||
sync::restore_points::create_restore_point,
|
||||
sync::restore_points::delete_restore_point,
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
//! The process that serves the RAM volume (`donut-proxy ramfs`), and the WinFsp
|
||||
//! loading it needs. The volume lives exactly as long as this process.
|
||||
//!
|
||||
//! WinFsp is a file system driver that Donut does not ship. When it is
|
||||
//! installed, an ordinary user can mount a user-mode file system on a
|
||||
//! directory; this process mounts one that keeps everything in its own memory.
|
||||
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use windows::core::{PCWSTR, PWSTR};
|
||||
use windows::Win32::Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL};
|
||||
use windows::Win32::Security::{PSECURITY_DESCRIPTOR, TOKEN_QUERY, TOKEN_USER};
|
||||
use winfsp::host::{FileSystemHost, VolumeParams};
|
||||
|
||||
use super::state::VolumeState;
|
||||
use super::tree::{now, ALLOCATION_UNIT, MAX_NAME_LEN};
|
||||
use super::winfsp_fs::RamFs;
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
pub const DLL_NAME: &str = "winfsp-x64.dll";
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
pub const DLL_NAME: &str = "winfsp-a64.dll";
|
||||
#[cfg(target_arch = "x86")]
|
||||
pub const DLL_NAME: &str = "winfsp-x86.dll";
|
||||
|
||||
/// How long Windows may answer free-space questions from its cache.
|
||||
const VOLUME_INFO_TIMEOUT_MS: u32 = 1000;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Unavailable {
|
||||
Missing,
|
||||
Broken(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Unavailable {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Unavailable::Missing => f.write_str("winfsp not installed"),
|
||||
Unavailable::Broken(e) => write!(f, "winfsp unusable: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wide(text: &std::ffi::OsStr) -> Vec<u16> {
|
||||
text.encode_wide().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
fn install_dir() -> Option<PathBuf> {
|
||||
use winreg::enums::HKEY_LOCAL_MACHINE;
|
||||
use winreg::RegKey;
|
||||
let machine = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
["SOFTWARE\\WOW6432Node\\WinFsp", "SOFTWARE\\WinFsp"]
|
||||
.iter()
|
||||
.find_map(|key| {
|
||||
machine
|
||||
.open_subkey(key)
|
||||
.ok()?
|
||||
.get_value::<String, _>("InstallDir")
|
||||
.ok()
|
||||
})
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
|
||||
/// Load the installed WinFsp DLL by its full path, then let the crate bind to
|
||||
/// it. Nothing may call into WinFsp before this succeeds: the DLL is
|
||||
/// delay-loaded, and a missing DLL would end the process at the first call.
|
||||
pub fn load() -> Result<(), Unavailable> {
|
||||
use windows::Win32::System::LibraryLoader::LoadLibraryW;
|
||||
static LOADED: OnceLock<()> = OnceLock::new();
|
||||
if LOADED.get().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let dll = install_dir()
|
||||
.map(|dir| dir.join("bin").join(DLL_NAME))
|
||||
.filter(|dll| dll.is_file())
|
||||
.ok_or(Unavailable::Missing)?;
|
||||
let name = wide(dll.as_os_str());
|
||||
unsafe { LoadLibraryW(PCWSTR(name.as_ptr())) }
|
||||
.map_err(|e| Unavailable::Broken(format!("load {}: {e}", dll.display())))?;
|
||||
winfsp::winfsp_init().map_err(|e| Unavailable::Broken(format!("init: {e:?}")))?;
|
||||
let _ = LOADED.set(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn user_sid() -> Result<String, String> {
|
||||
use windows::Win32::Security::Authorization::ConvertSidToStringSidW;
|
||||
use windows::Win32::Security::{GetTokenInformation, TokenUser};
|
||||
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
|
||||
unsafe {
|
||||
let mut token = HANDLE::default();
|
||||
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token)
|
||||
.map_err(|e| format!("process token: {e}"))?;
|
||||
let mut len = 0u32;
|
||||
let _ = GetTokenInformation(token, TokenUser, None, 0, &mut len);
|
||||
// u64 elements keep the buffer aligned for TOKEN_USER.
|
||||
let mut buffer = vec![0u64; (len as usize).div_ceil(8)];
|
||||
let read = GetTokenInformation(
|
||||
token,
|
||||
TokenUser,
|
||||
Some(buffer.as_mut_ptr().cast()),
|
||||
len,
|
||||
&mut len,
|
||||
);
|
||||
let _ = CloseHandle(token);
|
||||
read.map_err(|e| format!("token user: {e}"))?;
|
||||
let user = &*buffer.as_ptr().cast::<TOKEN_USER>();
|
||||
let mut text = PWSTR::null();
|
||||
ConvertSidToStringSidW(user.User.Sid, &mut text).map_err(|e| format!("user sid: {e}"))?;
|
||||
let sid = text.to_string();
|
||||
let _ = LocalFree(Some(HLOCAL(text.0.cast())));
|
||||
sid.map_err(|e| format!("user sid: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The root directory's descriptor: the user, SYSTEM and Administrators, as in
|
||||
/// the user's own profile folder, inherited by everything created below.
|
||||
fn root_security() -> Result<Vec<u8>, String> {
|
||||
use windows::Win32::Security::Authorization::{
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
|
||||
};
|
||||
let sid = user_sid()?;
|
||||
let sddl =
|
||||
format!("O:{sid}G:{sid}D:P(A;OICI;FA;;;S-1-5-18)(A;OICI;FA;;;S-1-5-32-544)(A;OICI;FA;;;{sid})");
|
||||
let sddl = wide(std::ffi::OsStr::new(&sddl));
|
||||
let mut descriptor = PSECURITY_DESCRIPTOR::default();
|
||||
let mut len = 0u32;
|
||||
unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
PCWSTR(sddl.as_ptr()),
|
||||
SDDL_REVISION_1,
|
||||
&mut descriptor,
|
||||
Some(&mut len),
|
||||
)
|
||||
.map_err(|e| format!("root security: {e}"))?;
|
||||
let bytes = std::slice::from_raw_parts(descriptor.0.cast::<u8>(), len as usize).to_vec();
|
||||
let _ = LocalFree(Some(HLOCAL(descriptor.0)));
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fn volume_params() -> VolumeParams {
|
||||
let mut params = VolumeParams::new();
|
||||
params
|
||||
.sector_size(ALLOCATION_UNIT as u16)
|
||||
.sectors_per_allocation_unit(1)
|
||||
.max_component_length(MAX_NAME_LEN as u16)
|
||||
.volume_creation_time(now())
|
||||
.volume_serial_number(now() as u32)
|
||||
.file_info_timeout(u32::MAX)
|
||||
.case_sensitive_search(false)
|
||||
.case_preserved_names(true)
|
||||
.unicode_on_disk(true)
|
||||
.persistent_acls(true)
|
||||
.post_cleanup_when_modified_only(true)
|
||||
.post_disposition_only_when_necessary(true)
|
||||
.supports_posix_unlink_rename(true)
|
||||
.reparse_points(true)
|
||||
.filesystem_name("NTFS");
|
||||
// Free space must stay current: Chromium sizes its caches and quotas from
|
||||
// it. The crate sets the timeout but not the flag that makes it count.
|
||||
// SAFETY: VolumeParams is a transparent wrapper of FSP_FSCTL_VOLUME_PARAMS.
|
||||
let raw = unsafe {
|
||||
&mut *(&mut params as *mut VolumeParams).cast::<winfsp_sys::FSP_FSCTL_VOLUME_PARAMS>()
|
||||
};
|
||||
raw.set_VolumeInfoTimeoutValid(1);
|
||||
raw.VolumeInfoTimeout = VOLUME_INFO_TIMEOUT_MS;
|
||||
params
|
||||
}
|
||||
|
||||
/// A mounted volume. Dropping it unmounts it and frees everything on it.
|
||||
pub struct Volume {
|
||||
_host: FileSystemHost<RamFs>,
|
||||
}
|
||||
|
||||
pub fn mount_volume(mount: &Path, capacity: u64) -> Result<Volume, String> {
|
||||
load().map_err(|e| e.to_string())?;
|
||||
let fs = RamFs::new(capacity, root_security()?);
|
||||
let mut host: FileSystemHost<RamFs> =
|
||||
FileSystemHost::new(volume_params(), fs).map_err(|e| format!("create: {e}"))?;
|
||||
host
|
||||
.mount(mount.as_os_str())
|
||||
.map_err(|e| format!("mount: {e}"))?;
|
||||
host.start().map_err(|e| format!("start: {e}"))?;
|
||||
Ok(Volume { _host: host })
|
||||
}
|
||||
|
||||
/// WinFsp creates the mount point itself, so the path must not exist. A
|
||||
/// crashed host can leave an empty directory or a dead junction there; either
|
||||
/// is removed. Anything with contents is left alone.
|
||||
fn clear_mount_point(mount: &Path) -> Result<(), String> {
|
||||
match std::fs::symlink_metadata(mount) {
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(e) => Err(format!("mount point unreadable: {e}")),
|
||||
Ok(_) => std::fs::remove_dir(mount).map_err(|e| format!("mount point in use: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mount, record the volume in `state_file`, and serve until killed.
|
||||
pub fn host_main(mount: &Path, capacity: u64, state_file: &Path) -> i32 {
|
||||
let started = std::time::Instant::now();
|
||||
let pid = std::process::id();
|
||||
let prepared = mount
|
||||
.parent()
|
||||
.map_or(Ok(()), std::fs::create_dir_all)
|
||||
.map_err(|e| format!("mount parent: {e}"))
|
||||
.and_then(|()| clear_mount_point(mount));
|
||||
let volume = match prepared.and_then(|()| mount_volume(mount, capacity)) {
|
||||
Ok(volume) => volume,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Ram volume mount failed mount={} pid={pid} err=\"{e}\"",
|
||||
mount.display()
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
let state = VolumeState {
|
||||
pid,
|
||||
mount: mount.to_path_buf(),
|
||||
capacity,
|
||||
};
|
||||
if let Err(e) = state.write(state_file) {
|
||||
log::error!(
|
||||
"Ram volume state write failed path={} pid={pid} err=\"{e}\"",
|
||||
state_file.display()
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
log::info!(
|
||||
"Ram volume mounted mount={} capacity_mb={} pid={pid} elapsed_ms={}",
|
||||
mount.display(),
|
||||
capacity >> 20,
|
||||
started.elapsed().as_millis()
|
||||
);
|
||||
let _volume = volume;
|
||||
loop {
|
||||
std::thread::park();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn free_bytes(path: &Path) -> u64 {
|
||||
use windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
|
||||
let mut name = path.as_os_str().to_os_string();
|
||||
name.push("\\");
|
||||
let name = wide(&name);
|
||||
let mut free = 0u64;
|
||||
unsafe { GetDiskFreeSpaceExW(PCWSTR(name.as_ptr()), Some(&mut free), None, None) }.unwrap();
|
||||
free
|
||||
}
|
||||
|
||||
fn write_synced(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
|
||||
let mut file = std::fs::File::create(path)?;
|
||||
file.write_all(bytes)?;
|
||||
file.sync_all()
|
||||
}
|
||||
|
||||
fn wait_for_volume_info() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(
|
||||
VOLUME_INFO_TIMEOUT_MS as u64 + 200,
|
||||
));
|
||||
}
|
||||
|
||||
/// Run after installing WinFsp:
|
||||
/// `cargo test --lib ramfs::host -- --ignored --nocapture`
|
||||
#[test]
|
||||
#[ignore = "needs WinFsp installed"]
|
||||
fn a_mounted_volume_serves_files_like_a_disk() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mount = root.path().join("ephemeral-ram");
|
||||
let capacity: u64 = 64 << 20;
|
||||
let volume = mount_volume(&mount, capacity).unwrap();
|
||||
|
||||
let network = mount.join("profile\\Default\\Network");
|
||||
std::fs::create_dir_all(&network).unwrap();
|
||||
std::fs::write(network.join("Cookies"), b"a=1").unwrap();
|
||||
assert_eq!(
|
||||
std::fs::read(mount.join("PROFILE\\default\\NETWORK\\cookies")).unwrap(),
|
||||
b"a=1"
|
||||
);
|
||||
let listed: Vec<String> = std::fs::read_dir(mount.join("profile\\default"))
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
|
||||
.collect();
|
||||
assert_eq!(listed, ["Network"]);
|
||||
|
||||
// An atomic write: a temporary file moved over the old one.
|
||||
let prefs = mount.join("profile\\Default\\Preferences");
|
||||
let temp = mount.join("profile\\Default\\Preferences.tmp");
|
||||
std::fs::write(&prefs, b"old").unwrap();
|
||||
std::fs::write(&temp, b"new").unwrap();
|
||||
std::fs::rename(&temp, &prefs).unwrap();
|
||||
assert_eq!(std::fs::read(&prefs).unwrap(), b"new");
|
||||
assert!(!temp.exists());
|
||||
|
||||
// Replace and delete a file that another handle still reads.
|
||||
let mut held = std::fs::File::open(&prefs).unwrap();
|
||||
std::fs::write(&temp, b"newer").unwrap();
|
||||
std::fs::rename(&temp, &prefs).unwrap();
|
||||
assert_eq!(std::fs::read(&prefs).unwrap(), b"newer");
|
||||
let mut old = String::new();
|
||||
held.read_to_string(&mut old).unwrap();
|
||||
assert_eq!(old, "new");
|
||||
std::fs::remove_file(&prefs).unwrap();
|
||||
assert!(!prefs.exists());
|
||||
drop(held);
|
||||
|
||||
// A delete-on-close file goes when its handle closes.
|
||||
let scratch = mount.join("profile\\Default\\scratch");
|
||||
{
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
const FILE_FLAG_DELETE_ON_CLOSE: u32 = 0x0400_0000;
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.custom_flags(FILE_FLAG_DELETE_ON_CLOSE)
|
||||
.open(&scratch)
|
||||
.unwrap();
|
||||
file.write_all(b"temp").unwrap();
|
||||
assert!(scratch.exists());
|
||||
}
|
||||
assert!(!scratch.exists());
|
||||
|
||||
// Windows may refuse to move a directory with an open file in it, as NTFS
|
||||
// does. Either way the open file keeps its data and one path finds it.
|
||||
let history = mount.join("profile\\Default\\History");
|
||||
std::fs::write(&history, b"visits").unwrap();
|
||||
let mut open_child = std::fs::File::open(&history).unwrap();
|
||||
let moved = std::fs::rename(
|
||||
mount.join("profile\\Default"),
|
||||
mount.join("profile\\Profile 1"),
|
||||
);
|
||||
let mut visits = String::new();
|
||||
open_child.read_to_string(&mut visits).unwrap();
|
||||
assert_eq!(visits, "visits");
|
||||
drop(open_child);
|
||||
let default = match moved {
|
||||
Ok(()) => mount.join("profile\\Profile 1"),
|
||||
Err(e) => {
|
||||
assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied, "{e}");
|
||||
mount.join("profile\\Default")
|
||||
}
|
||||
};
|
||||
assert_eq!(std::fs::read(default.join("History")).unwrap(), b"visits");
|
||||
let network = default.join("Network");
|
||||
|
||||
// An ACE added to a folder stays and passes to new files, as Chromium's
|
||||
// sandbox needs for the Network folder. AC is ALL APPLICATION PACKAGES.
|
||||
let sddl = |path: &Path| {
|
||||
let script = format!("(Get-Acl -LiteralPath '{}').Sddl", path.display());
|
||||
let out = std::process::Command::new("powershell")
|
||||
.args(["-NoProfile", "-Command", &script])
|
||||
.output()
|
||||
.unwrap();
|
||||
String::from_utf8_lossy(&out.stdout).into_owned()
|
||||
};
|
||||
let granted = std::process::Command::new("icacls")
|
||||
.arg(&network)
|
||||
.args(["/grant", "*S-1-15-2-1:(OI)(CI)(F)"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(granted.status.success(), "{granted:?}");
|
||||
let tokens = network.join("Trust Tokens");
|
||||
std::fs::write(&tokens, b"x").unwrap();
|
||||
assert!(sddl(&network).contains(";;;AC)"), "{}", sddl(&network));
|
||||
assert!(sddl(&tokens).contains(";;;AC)"), "{}", sddl(&tokens));
|
||||
|
||||
// A plain file answers "not a reparse point".
|
||||
{
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
use windows::Win32::Foundation::ERROR_NOT_A_REPARSE_POINT;
|
||||
use windows::Win32::System::IO::DeviceIoControl;
|
||||
const FSCTL_GET_REPARSE_POINT: u32 = 0x0009_00A8;
|
||||
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
|
||||
.open(network.join("Cookies"))
|
||||
.unwrap();
|
||||
let mut buffer = vec![0u8; 16 * 1024];
|
||||
let mut returned = 0u32;
|
||||
let queried = unsafe {
|
||||
DeviceIoControl(
|
||||
HANDLE(file.as_raw_handle()),
|
||||
FSCTL_GET_REPARSE_POINT,
|
||||
None,
|
||||
0,
|
||||
Some(buffer.as_mut_ptr().cast()),
|
||||
buffer.len() as u32,
|
||||
Some(&mut returned),
|
||||
None,
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
queried.unwrap_err().code(),
|
||||
ERROR_NOT_A_REPARSE_POINT.to_hresult()
|
||||
);
|
||||
}
|
||||
|
||||
// Free space follows what is written and deleted.
|
||||
wait_for_volume_info();
|
||||
let before = free_bytes(&mount);
|
||||
assert!(before < capacity);
|
||||
let cache = mount.join("profile\\cache");
|
||||
write_synced(&cache, &vec![7u8; 8 << 20]).unwrap();
|
||||
wait_for_volume_info();
|
||||
let during = free_bytes(&mount);
|
||||
assert!(
|
||||
before - during >= 8 << 20,
|
||||
"before={before} during={during}"
|
||||
);
|
||||
std::fs::remove_file(&cache).unwrap();
|
||||
wait_for_volume_info();
|
||||
assert_eq!(free_bytes(&mount), before);
|
||||
|
||||
let huge = mount.join("profile\\huge");
|
||||
assert!(write_synced(&huge, &vec![0u8; capacity as usize + 1]).is_err());
|
||||
let _ = std::fs::remove_file(&huge);
|
||||
|
||||
std::fs::remove_dir_all(mount.join("profile")).unwrap();
|
||||
assert_eq!(std::fs::read_dir(&mount).unwrap().count(), 0);
|
||||
|
||||
drop(volume);
|
||||
assert!(!mount.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stale_mount_point_is_cleared_but_contents_are_kept() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mount = root.path().join("ephemeral-ram");
|
||||
assert!(clear_mount_point(&mount).is_ok());
|
||||
std::fs::create_dir(&mount).unwrap();
|
||||
assert!(clear_mount_point(&mount).is_ok());
|
||||
assert!(!mount.exists());
|
||||
std::fs::create_dir(&mount).unwrap();
|
||||
std::fs::write(mount.join("keep"), b"x").unwrap();
|
||||
assert!(clear_mount_point(&mount).is_err());
|
||||
assert!(mount.join("keep").exists());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
//! In-memory storage for ephemeral and password-protected profiles on Windows.
|
||||
//!
|
||||
//! Windows has no tmpfs, and the RAM disk drivers that exist need admin rights.
|
||||
//! WinFsp, when the person has installed it, lets an ordinary user mount a
|
||||
//! file system served by a process: `donut-proxy ramfs` serves one that keeps
|
||||
//! every byte in its own memory. It is a separate process because a browser
|
||||
//! can outlive the app, and its profile must stay readable until it exits.
|
||||
|
||||
pub mod state;
|
||||
pub mod tree;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod host;
|
||||
#[cfg(windows)]
|
||||
mod winfsp_fs;
|
||||
|
||||
pub use state::VolumeState;
|
||||
|
||||
const MIN_CAPACITY: u64 = 512 << 20;
|
||||
const MAX_CAPACITY: u64 = 4 << 30;
|
||||
|
||||
/// A quarter of the RAM, between 512 MiB and 4 GiB.
|
||||
pub fn volume_capacity(total_ram: u64) -> u64 {
|
||||
(total_ram / 4).clamp(MIN_CAPACITY, MAX_CAPACITY)
|
||||
}
|
||||
|
||||
/// Why the volume cannot be used, or `Ok` when WinFsp is ready.
|
||||
pub fn winfsp_available() -> Result<(), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
host::load().map_err(|e| e.to_string())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Err("winfsp is for windows only".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// What the ephemeral-storage notice shows.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
InMemory,
|
||||
WinfspMissing,
|
||||
WinfspError,
|
||||
}
|
||||
|
||||
/// `donut-proxy ramfs`: mount the volume and serve it until killed.
|
||||
pub fn host_main(mount: &std::path::Path, capacity: u64, state_file: &std::path::Path) -> i32 {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
host::host_main(mount, capacity, state_file)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
log::error!(
|
||||
"Ram volume mount failed mount={} capacity_mb={} state={} err=\"windows only\"",
|
||||
mount.display(),
|
||||
capacity >> 20,
|
||||
state_file.display()
|
||||
);
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub use windows_client::{ensure_volume, live_volume, status, stop_host};
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_client {
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Mutex, PoisonError};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::host::{self, Unavailable};
|
||||
use super::{volume_capacity, Status, VolumeState};
|
||||
|
||||
const MOUNT_WAIT: Duration = Duration::from_secs(10);
|
||||
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
|
||||
|
||||
/// Set when WinFsp loaded but the last start of the volume failed.
|
||||
static LAST_START_FAILED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn mount_dir() -> PathBuf {
|
||||
crate::app_dirs::data_dir().join("ephemeral-ram")
|
||||
}
|
||||
|
||||
fn state_path() -> PathBuf {
|
||||
crate::app_dirs::data_dir().join("ephemeral-ram.json")
|
||||
}
|
||||
|
||||
fn log_path() -> PathBuf {
|
||||
std::env::temp_dir().join("donut-ramfs.log")
|
||||
}
|
||||
|
||||
fn host_alive(pid: u32) -> bool {
|
||||
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
|
||||
let pid = sysinfo::Pid::from_u32(pid);
|
||||
let mut system = System::new();
|
||||
system.refresh_processes_specifics(
|
||||
ProcessesToUpdate::Some(&[pid]),
|
||||
true,
|
||||
ProcessRefreshKind::nothing(),
|
||||
);
|
||||
system.process(pid).is_some_and(|process| {
|
||||
crate::proxy_storage::snapshot_process_is_alive(process)
|
||||
&& process
|
||||
.name()
|
||||
.to_string_lossy()
|
||||
.to_ascii_lowercase()
|
||||
.starts_with("donut-proxy")
|
||||
})
|
||||
}
|
||||
|
||||
/// A live mount point is a junction into the volume, and it lists.
|
||||
fn mount_answers(mount: &Path) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
std::fs::symlink_metadata(mount)
|
||||
.is_ok_and(|meta| meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0)
|
||||
&& std::fs::read_dir(mount).is_ok()
|
||||
}
|
||||
|
||||
/// The volume of this data directory, when its host runs and it answers.
|
||||
pub fn live_volume() -> Option<VolumeState> {
|
||||
let state = VolumeState::read(&state_path())?;
|
||||
(state.mount == mount_dir() && host_alive(state.pid) && mount_answers(&state.mount))
|
||||
.then_some(state)
|
||||
}
|
||||
|
||||
pub fn status() -> Status {
|
||||
match host::load() {
|
||||
Err(Unavailable::Missing) => Status::WinfspMissing,
|
||||
Err(Unavailable::Broken(_)) => Status::WinfspError,
|
||||
Ok(()) if LAST_START_FAILED.load(Ordering::Relaxed) => Status::WinfspError,
|
||||
Ok(()) => Status::InMemory,
|
||||
}
|
||||
}
|
||||
|
||||
/// The mounted volume's path, starting its host when no live one exists.
|
||||
pub fn ensure_volume() -> Result<PathBuf, String> {
|
||||
static STARTING: Mutex<()> = Mutex::new(());
|
||||
let _starting = STARTING.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
host::load().map_err(|e| e.to_string())?;
|
||||
if live_volume().is_some() {
|
||||
LAST_START_FAILED.store(false, Ordering::Relaxed);
|
||||
return Ok(mount_dir());
|
||||
}
|
||||
let started = start_host();
|
||||
LAST_START_FAILED.store(started.is_err(), Ordering::Relaxed);
|
||||
started
|
||||
}
|
||||
|
||||
fn start_host() -> Result<PathBuf, String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::{Command, Stdio};
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
let started = Instant::now();
|
||||
let mount = mount_dir();
|
||||
let capacity = {
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_memory();
|
||||
volume_capacity(system.total_memory())
|
||||
};
|
||||
let exe = crate::proxy_runner::find_sidecar_executable("donut-proxy")
|
||||
.map_err(|e| format!("donut-proxy not found: {e}"))?;
|
||||
let log =
|
||||
crate::app_dirs::create_owner_only(&log_path()).map_or_else(|_| Stdio::null(), Stdio::from);
|
||||
let mut child = Command::new(exe)
|
||||
.arg("ramfs")
|
||||
.arg("--mount")
|
||||
.arg(&mount)
|
||||
.arg("--capacity")
|
||||
.arg(capacity.to_string())
|
||||
.arg("--state")
|
||||
.arg(state_path())
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(log)
|
||||
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW)
|
||||
.spawn()
|
||||
.map_err(|e| format!("ramfs host spawn: {e}"))?;
|
||||
let pid = child.id();
|
||||
|
||||
loop {
|
||||
if live_volume().is_some_and(|state| state.pid == pid) {
|
||||
log::info!(
|
||||
"Ram volume host started pid={pid} mount={} capacity_mb={} elapsed_ms={}",
|
||||
mount.display(),
|
||||
capacity >> 20,
|
||||
started.elapsed().as_millis()
|
||||
);
|
||||
return Ok(mount);
|
||||
}
|
||||
if let Ok(Some(status)) = child.try_wait() {
|
||||
return Err(format!(
|
||||
"ramfs host exited ({status}) log={}",
|
||||
log_path().display()
|
||||
));
|
||||
}
|
||||
if started.elapsed() > MOUNT_WAIT {
|
||||
let _ = child.kill();
|
||||
return Err(format!(
|
||||
"ramfs host did not mount in {}s log={}",
|
||||
MOUNT_WAIT.as_secs(),
|
||||
log_path().display()
|
||||
));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
/// End the host, which unmounts the volume and frees its memory. Not waited
|
||||
/// for, because this runs at exit.
|
||||
pub fn stop_host(pid: u32, reason: &str) {
|
||||
use windows::Win32::Foundation::CloseHandle;
|
||||
use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
|
||||
let stopped = unsafe {
|
||||
OpenProcess(PROCESS_TERMINATE, false, pid).and_then(|process| {
|
||||
let result = TerminateProcess(process, 0);
|
||||
let _ = CloseHandle(process);
|
||||
result
|
||||
})
|
||||
};
|
||||
match stopped {
|
||||
Ok(()) => {
|
||||
let _ = std::fs::remove_file(state_path());
|
||||
log::info!("Ram volume host stopped pid={pid} reason={reason}");
|
||||
}
|
||||
Err(e) => log::warn!("Ram volume host stop failed pid={pid} reason={reason} err=\"{e}\""),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_missing_winfsp_is_reported_and_never_called() {
|
||||
match host::load() {
|
||||
Ok(()) => assert_ne!(status(), Status::WinfspMissing),
|
||||
Err(Unavailable::Missing) => {
|
||||
assert_eq!(status(), Status::WinfspMissing);
|
||||
assert_eq!(ensure_volume(), Err("winfsp not installed".to_string()));
|
||||
}
|
||||
Err(Unavailable::Broken(e)) => panic!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run after installing WinFsp and building the sidecar:
|
||||
/// `cargo build --bin donut-proxy`, then
|
||||
/// `cargo test --lib ramfs::windows_client -- --ignored --nocapture`
|
||||
#[test]
|
||||
#[ignore = "needs WinFsp installed and a built donut-proxy"]
|
||||
fn the_sidecar_host_serves_a_volume_until_stopped() {
|
||||
let data = tempfile::tempdir().unwrap();
|
||||
let _data = crate::app_dirs::set_test_data_dir(data.path().to_path_buf());
|
||||
let mount = ensure_volume().unwrap();
|
||||
assert_eq!(mount, data.path().join("ephemeral-ram"));
|
||||
assert_eq!(status(), Status::InMemory);
|
||||
std::fs::write(mount.join("probe"), b"x").unwrap();
|
||||
|
||||
let volume = live_volume().unwrap();
|
||||
assert_eq!(ensure_volume().unwrap(), mount);
|
||||
assert_eq!(live_volume().unwrap().pid, volume.pid);
|
||||
assert_eq!(std::fs::read(mount.join("probe")).unwrap(), b"x");
|
||||
|
||||
stop_host(volume.pid, "test");
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while live_volume().is_some() && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
assert!(live_volume().is_none());
|
||||
assert!(!mount.join("probe").exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn capacity_is_a_quarter_of_ram_within_bounds() {
|
||||
assert_eq!(volume_capacity(1 << 30), MIN_CAPACITY);
|
||||
assert_eq!(volume_capacity(8 << 30), 2 << 30);
|
||||
assert_eq!(volume_capacity(64 << 30), MAX_CAPACITY);
|
||||
assert_eq!(volume_capacity(0), MIN_CAPACITY);
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn other_systems_have_no_volume_host() {
|
||||
assert!(winfsp_available().is_err());
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
assert_eq!(
|
||||
host_main(&dir.path().join("m"), MIN_CAPACITY, &dir.path().join("s")),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//! The file the volume host writes once its volume is mounted, so the app can
|
||||
//! find a volume that an earlier run started.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VolumeState {
|
||||
pub pid: u32,
|
||||
pub mount: PathBuf,
|
||||
pub capacity: u64,
|
||||
}
|
||||
|
||||
impl VolumeState {
|
||||
/// Written to a temporary file first, so a reader never sees half of it.
|
||||
pub fn write(&self, path: &Path) -> std::io::Result<()> {
|
||||
let temporary = path.with_extension("tmp");
|
||||
std::fs::write(&temporary, serde_json::to_vec(self)?)?;
|
||||
std::fs::rename(&temporary, path)
|
||||
}
|
||||
|
||||
pub fn read(path: &Path) -> Option<Self> {
|
||||
serde_json::from_slice(&std::fs::read(path).ok()?).ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn state_round_trips_and_unreadable_state_is_none() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("ephemeral-ram.json");
|
||||
assert_eq!(VolumeState::read(&path), None);
|
||||
|
||||
let state = VolumeState {
|
||||
pid: 42,
|
||||
mount: dir.path().join("ephemeral-ram"),
|
||||
capacity: 512 << 20,
|
||||
};
|
||||
state.write(&path).unwrap();
|
||||
assert_eq!(VolumeState::read(&path), Some(state));
|
||||
assert!(!path.with_extension("tmp").exists());
|
||||
|
||||
std::fs::write(&path, b"{").unwrap();
|
||||
assert_eq!(VolumeState::read(&path), None);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,511 @@
|
||||
//! The WinFsp side of the RAM volume: each request maps onto `Tree`.
|
||||
//!
|
||||
//! WinFsp's kernel driver handles byte-range locks, memory-mapped files, the
|
||||
//! cache and change notifications; this layer keeps names, data and security
|
||||
//! descriptors.
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
|
||||
|
||||
use windows::Win32::Foundation::{
|
||||
STATUS_ACCESS_DENIED, STATUS_BUFFER_OVERFLOW, STATUS_CANNOT_DELETE, STATUS_DIRECTORY_NOT_EMPTY,
|
||||
STATUS_DISK_FULL, STATUS_END_OF_FILE, STATUS_FILE_IS_A_DIRECTORY, STATUS_INSUFFICIENT_RESOURCES,
|
||||
STATUS_INVALID_DEVICE_REQUEST, STATUS_INVALID_PARAMETER, STATUS_NOT_A_DIRECTORY,
|
||||
STATUS_NOT_A_REPARSE_POINT, STATUS_OBJECT_NAME_COLLISION, STATUS_OBJECT_NAME_INVALID,
|
||||
STATUS_OBJECT_NAME_NOT_FOUND, STATUS_OBJECT_PATH_NOT_FOUND,
|
||||
};
|
||||
use windows::Win32::Security::{GetSecurityDescriptorLength, PSECURITY_DESCRIPTOR};
|
||||
use winfsp::constants::FspCleanupFlags;
|
||||
use winfsp::filesystem::{
|
||||
DirInfo, DirMarker, FileInfo, FileSecurity, FileSystemContext, ModificationDescriptor,
|
||||
OpenFileInfo, VolumeInfo, WideNameInfo,
|
||||
};
|
||||
use winfsp::{FspError, U16CStr};
|
||||
use winfsp_sys::{FILE_ACCESS_RIGHTS, FILE_FLAGS_AND_ATTRIBUTES};
|
||||
|
||||
use super::tree::{BasicInfo, FsError, Info, NodeId, Tree, ROOT};
|
||||
|
||||
const FILE_DIRECTORY_FILE: u32 = 0x1;
|
||||
const FILE_NON_DIRECTORY_FILE: u32 = 0x40;
|
||||
const INVALID_FILE_ATTRIBUTES: u32 = u32::MAX;
|
||||
const VOLUME_LABEL: &str = "DonutEphemeral";
|
||||
|
||||
/// WinFsp reads a security descriptor by name into a 1024-byte buffer, and this
|
||||
/// binding cannot report that more room is needed. Larger descriptors are
|
||||
/// refused when they are set, so no open ever fails on one.
|
||||
pub const MAX_SECURITY_LEN: usize = 1024;
|
||||
|
||||
impl From<FsError> for FspError {
|
||||
fn from(error: FsError) -> Self {
|
||||
let status = match error {
|
||||
FsError::NotFound => STATUS_OBJECT_NAME_NOT_FOUND,
|
||||
FsError::PathNotFound => STATUS_OBJECT_PATH_NOT_FOUND,
|
||||
FsError::NameCollision => STATUS_OBJECT_NAME_COLLISION,
|
||||
FsError::NameInvalid => STATUS_OBJECT_NAME_INVALID,
|
||||
FsError::NotADirectory => STATUS_NOT_A_DIRECTORY,
|
||||
FsError::IsADirectory => STATUS_FILE_IS_A_DIRECTORY,
|
||||
FsError::DirectoryNotEmpty => STATUS_DIRECTORY_NOT_EMPTY,
|
||||
FsError::AccessDenied => STATUS_ACCESS_DENIED,
|
||||
FsError::CannotDelete => STATUS_CANNOT_DELETE,
|
||||
FsError::DiskFull => STATUS_DISK_FULL,
|
||||
FsError::EndOfFile => STATUS_END_OF_FILE,
|
||||
FsError::InvalidParameter => STATUS_INVALID_PARAMETER,
|
||||
};
|
||||
FspError::NTSTATUS(status.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn status(status: windows::Win32::Foundation::NTSTATUS) -> FspError {
|
||||
FspError::NTSTATUS(status.0)
|
||||
}
|
||||
|
||||
fn fill(out: &mut FileInfo, info: Info) {
|
||||
out.file_attributes = info.attributes;
|
||||
out.reparse_tag = 0;
|
||||
out.allocation_size = info.allocation_size;
|
||||
out.file_size = info.file_size;
|
||||
out.creation_time = info.creation_time;
|
||||
out.last_access_time = info.last_access_time;
|
||||
out.last_write_time = info.last_write_time;
|
||||
out.change_time = info.change_time;
|
||||
out.index_number = info.index_number;
|
||||
out.hard_links = 0;
|
||||
out.ea_size = 0;
|
||||
}
|
||||
|
||||
/// One open of a file or directory.
|
||||
pub struct Handle {
|
||||
id: NodeId,
|
||||
}
|
||||
|
||||
pub struct RamFs {
|
||||
tree: Mutex<Tree>,
|
||||
}
|
||||
|
||||
impl RamFs {
|
||||
pub fn new(capacity: u64, root_security: Vec<u8>) -> Self {
|
||||
RamFs {
|
||||
tree: Mutex::new(Tree::new(capacity, root_security)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A callback that panicked poisons the lock; the tree stays consistent
|
||||
/// because every change finishes before the next can fail.
|
||||
fn tree(&self) -> MutexGuard<'_, Tree> {
|
||||
self.tree.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn opened(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
id: NodeId,
|
||||
file_info: &mut OpenFileInfo,
|
||||
) -> winfsp::Result<Handle> {
|
||||
fill(file_info.as_mut(), tree.info(id)?);
|
||||
// The stored case of every name, so Windows reports what was created.
|
||||
let path = tree.full_path(id)?;
|
||||
if path.len() * 2 < file_info.normalized_name_size() as usize {
|
||||
file_info.set_normalized_name(&path, None);
|
||||
}
|
||||
Ok(Handle { id })
|
||||
}
|
||||
}
|
||||
|
||||
/// The address of `FspSetSecurityDescriptor` inside the WinFsp DLL. WinFsp
|
||||
/// frees a descriptor by comparing the function that made it to its own
|
||||
/// exports, and the delay-load stub this binary links has another address.
|
||||
fn set_security_descriptor_export() -> Option<unsafe extern "C" fn() -> i32> {
|
||||
use windows::core::{PCSTR, PCWSTR};
|
||||
use windows::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
|
||||
static EXPORT: OnceLock<Option<unsafe extern "C" fn() -> i32>> = OnceLock::new();
|
||||
*EXPORT.get_or_init(|| {
|
||||
let name: Vec<u16> = super::host::DLL_NAME
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let module = unsafe { GetModuleHandleW(PCWSTR(name.as_ptr())) }.ok()?;
|
||||
let export =
|
||||
unsafe { GetProcAddress(module, PCSTR(c"FspSetSecurityDescriptor".as_ptr().cast())) }?;
|
||||
// SAFETY: WinFsp's FspDeleteSecurityDescriptor takes this function as an
|
||||
// untyped `NTSTATUS (*)()` and only compares it.
|
||||
Some(unsafe {
|
||||
std::mem::transmute::<unsafe extern "system" fn() -> isize, unsafe extern "C" fn() -> i32>(
|
||||
export,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply a SetSecurity request to a stored descriptor.
|
||||
fn modified_security(
|
||||
current: &[u8],
|
||||
security_information: u32,
|
||||
modification: &ModificationDescriptor,
|
||||
) -> winfsp::Result<Vec<u8>> {
|
||||
let export = set_security_descriptor_export().ok_or(status(STATUS_INVALID_DEVICE_REQUEST))?;
|
||||
// The input parameter is not const, so it gets a private copy.
|
||||
let mut input = current.to_vec();
|
||||
let mut output: winfsp_sys::PSECURITY_DESCRIPTOR = std::ptr::null_mut();
|
||||
let result = unsafe {
|
||||
winfsp_sys::FspSetSecurityDescriptor(
|
||||
input.as_mut_ptr().cast(),
|
||||
security_information,
|
||||
modification.as_mut_ptr(),
|
||||
&mut output,
|
||||
)
|
||||
};
|
||||
if result < 0 {
|
||||
return Err(FspError::NTSTATUS(result));
|
||||
}
|
||||
let len = unsafe { GetSecurityDescriptorLength(PSECURITY_DESCRIPTOR(output)) } as usize;
|
||||
let updated = unsafe { std::slice::from_raw_parts(output.cast::<u8>(), len) }.to_vec();
|
||||
unsafe { winfsp_sys::FspDeleteSecurityDescriptor(output, Some(export)) };
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
impl FileSystemContext for RamFs {
|
||||
type FileContext = Handle;
|
||||
|
||||
fn get_security_by_name(
|
||||
&self,
|
||||
file_name: &U16CStr,
|
||||
security_descriptor: Option<&mut [c_void]>,
|
||||
_reparse_point_resolver: impl FnOnce(&U16CStr) -> Option<FileSecurity>,
|
||||
) -> winfsp::Result<FileSecurity> {
|
||||
let tree = self.tree();
|
||||
let id = tree.lookup(file_name.as_slice())?;
|
||||
let attributes = tree.info(id)?.attributes;
|
||||
let security = tree.security(id)?;
|
||||
if let Some(buffer) = security_descriptor {
|
||||
if buffer.len() < security.len() {
|
||||
// Never STATUS_BUFFER_OVERFLOW: WinFsp would retry with the same size.
|
||||
return Err(status(STATUS_INSUFFICIENT_RESOURCES));
|
||||
}
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
security.as_ptr(),
|
||||
buffer.as_mut_ptr().cast::<u8>(),
|
||||
security.len(),
|
||||
)
|
||||
};
|
||||
}
|
||||
Ok(FileSecurity {
|
||||
reparse: false,
|
||||
sz_security_descriptor: security.len() as u64,
|
||||
attributes,
|
||||
})
|
||||
}
|
||||
|
||||
fn open(
|
||||
&self,
|
||||
file_name: &U16CStr,
|
||||
create_options: u32,
|
||||
_granted_access: FILE_ACCESS_RIGHTS,
|
||||
file_info: &mut OpenFileInfo,
|
||||
) -> winfsp::Result<Handle> {
|
||||
let mut tree = self.tree();
|
||||
let id = tree.lookup(file_name.as_slice())?;
|
||||
let is_dir = tree.is_dir(id)?;
|
||||
if create_options & FILE_DIRECTORY_FILE != 0 && !is_dir {
|
||||
return Err(FsError::NotADirectory.into());
|
||||
}
|
||||
if create_options & FILE_NON_DIRECTORY_FILE != 0 && is_dir {
|
||||
return Err(FsError::IsADirectory.into());
|
||||
}
|
||||
tree.open(id)?;
|
||||
self.opened(&tree, id, file_info)
|
||||
}
|
||||
|
||||
fn close(&self, context: Handle) {
|
||||
self.tree().close(context.id);
|
||||
}
|
||||
|
||||
fn create(
|
||||
&self,
|
||||
file_name: &U16CStr,
|
||||
create_options: u32,
|
||||
_granted_access: FILE_ACCESS_RIGHTS,
|
||||
file_attributes: FILE_FLAGS_AND_ATTRIBUTES,
|
||||
security_descriptor: Option<&[c_void]>,
|
||||
allocation_size: u64,
|
||||
extra_buffer: Option<&[u8]>,
|
||||
extra_buffer_is_reparse_point: bool,
|
||||
file_info: &mut OpenFileInfo,
|
||||
) -> winfsp::Result<Handle> {
|
||||
if extra_buffer_is_reparse_point && extra_buffer.is_some_and(|buffer| !buffer.is_empty()) {
|
||||
return Err(status(STATUS_ACCESS_DENIED));
|
||||
}
|
||||
let mut tree = self.tree();
|
||||
let security = match security_descriptor {
|
||||
Some(descriptor) => {
|
||||
unsafe { std::slice::from_raw_parts(descriptor.as_ptr().cast::<u8>(), descriptor.len()) }
|
||||
.to_vec()
|
||||
}
|
||||
None => tree.security(ROOT)?.to_vec(),
|
||||
};
|
||||
if security.len() > MAX_SECURITY_LEN {
|
||||
return Err(status(STATUS_INSUFFICIENT_RESOURCES));
|
||||
}
|
||||
let id = tree.create(
|
||||
file_name.as_slice(),
|
||||
create_options & FILE_DIRECTORY_FILE != 0,
|
||||
file_attributes,
|
||||
security,
|
||||
allocation_size,
|
||||
)?;
|
||||
self.opened(&tree, id, file_info)
|
||||
}
|
||||
|
||||
fn cleanup(&self, context: &Handle, _file_name: Option<&U16CStr>, flags: u32) {
|
||||
let mut tree = self.tree();
|
||||
let flagged = |flag: FspCleanupFlags| flag.is_flagged(flags);
|
||||
if flagged(FspCleanupFlags::FspCleanupSetAllocationSize) {
|
||||
let _ = tree.trim_allocation(context.id);
|
||||
}
|
||||
let _ = tree.touch(
|
||||
context.id,
|
||||
flagged(FspCleanupFlags::FspCleanupSetLastAccessTime),
|
||||
flagged(FspCleanupFlags::FspCleanupSetLastWriteTime),
|
||||
flagged(FspCleanupFlags::FspCleanupSetChangeTime),
|
||||
flagged(FspCleanupFlags::FspCleanupSetArchiveBit),
|
||||
);
|
||||
if flagged(FspCleanupFlags::FspCleanupDelete) {
|
||||
tree.unlink(context.id);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self, context: Option<&Handle>, file_info: &mut FileInfo) -> winfsp::Result<()> {
|
||||
if let Some(context) = context {
|
||||
fill(file_info, self.tree().info(context.id)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_file_info(&self, context: &Handle, file_info: &mut FileInfo) -> winfsp::Result<()> {
|
||||
fill(file_info, self.tree().info(context.id)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_security(
|
||||
&self,
|
||||
context: &Handle,
|
||||
security_descriptor: Option<&mut [c_void]>,
|
||||
) -> winfsp::Result<u64> {
|
||||
let tree = self.tree();
|
||||
let security = tree.security(context.id)?;
|
||||
if let Some(buffer) = security_descriptor {
|
||||
if buffer.len() < security.len() {
|
||||
return Err(status(STATUS_BUFFER_OVERFLOW));
|
||||
}
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
security.as_ptr(),
|
||||
buffer.as_mut_ptr().cast::<u8>(),
|
||||
security.len(),
|
||||
)
|
||||
};
|
||||
}
|
||||
Ok(security.len() as u64)
|
||||
}
|
||||
|
||||
fn set_security(
|
||||
&self,
|
||||
context: &Handle,
|
||||
security_information: u32,
|
||||
modification_descriptor: ModificationDescriptor,
|
||||
) -> winfsp::Result<()> {
|
||||
let mut tree = self.tree();
|
||||
let updated = modified_security(
|
||||
tree.security(context.id)?,
|
||||
security_information,
|
||||
&modification_descriptor,
|
||||
)?;
|
||||
if updated.len() > MAX_SECURITY_LEN {
|
||||
return Err(status(STATUS_INSUFFICIENT_RESOURCES));
|
||||
}
|
||||
tree.set_security(context.id, updated)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn overwrite(
|
||||
&self,
|
||||
context: &Handle,
|
||||
file_attributes: FILE_FLAGS_AND_ATTRIBUTES,
|
||||
replace_file_attributes: bool,
|
||||
allocation_size: u64,
|
||||
_extra_buffer: Option<&[u8]>,
|
||||
file_info: &mut FileInfo,
|
||||
) -> winfsp::Result<()> {
|
||||
let mut tree = self.tree();
|
||||
tree.overwrite(
|
||||
context.id,
|
||||
file_attributes,
|
||||
replace_file_attributes,
|
||||
allocation_size,
|
||||
)?;
|
||||
fill(file_info, tree.info(context.id)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_directory(
|
||||
&self,
|
||||
context: &Handle,
|
||||
_pattern: Option<&U16CStr>,
|
||||
marker: DirMarker,
|
||||
buffer: &mut [u8],
|
||||
) -> winfsp::Result<u32> {
|
||||
let tree = self.tree();
|
||||
let mut cursor = 0u32;
|
||||
let mut entry: DirInfo = DirInfo::new();
|
||||
let mut full = false;
|
||||
tree.for_each_entry(context.id, marker.inner(), |name, info| {
|
||||
entry.reset();
|
||||
fill(entry.file_info_mut(), info);
|
||||
if entry.set_name_raw(name).is_err() {
|
||||
return true;
|
||||
}
|
||||
if !entry.append_to_buffer(buffer, &mut cursor) {
|
||||
full = true;
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})?;
|
||||
if !full {
|
||||
<DirInfo as WideNameInfo>::finalize_buffer(buffer, &mut cursor);
|
||||
}
|
||||
Ok(cursor)
|
||||
}
|
||||
|
||||
fn rename(
|
||||
&self,
|
||||
context: &Handle,
|
||||
_file_name: &U16CStr,
|
||||
new_file_name: &U16CStr,
|
||||
replace_if_exists: bool,
|
||||
) -> winfsp::Result<()> {
|
||||
self
|
||||
.tree()
|
||||
.rename(context.id, new_file_name.as_slice(), replace_if_exists)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_basic_info(
|
||||
&self,
|
||||
context: &Handle,
|
||||
file_attributes: u32,
|
||||
creation_time: u64,
|
||||
last_access_time: u64,
|
||||
last_write_time: u64,
|
||||
last_change_time: u64,
|
||||
file_info: &mut FileInfo,
|
||||
) -> winfsp::Result<()> {
|
||||
let set = |time: u64| (time != 0).then_some(time);
|
||||
let mut tree = self.tree();
|
||||
tree.set_basic(
|
||||
context.id,
|
||||
BasicInfo {
|
||||
attributes: (file_attributes != INVALID_FILE_ATTRIBUTES).then_some(file_attributes),
|
||||
creation_time: set(creation_time),
|
||||
last_access_time: set(last_access_time),
|
||||
last_write_time: set(last_write_time),
|
||||
change_time: set(last_change_time),
|
||||
},
|
||||
)?;
|
||||
fill(file_info, tree.info(context.id)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_delete(
|
||||
&self,
|
||||
context: &Handle,
|
||||
_file_name: &U16CStr,
|
||||
delete_file: bool,
|
||||
) -> winfsp::Result<()> {
|
||||
if delete_file {
|
||||
self.tree().can_delete(context.id)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_file_size(
|
||||
&self,
|
||||
context: &Handle,
|
||||
new_size: u64,
|
||||
set_allocation_size: bool,
|
||||
file_info: &mut FileInfo,
|
||||
) -> winfsp::Result<()> {
|
||||
let mut tree = self.tree();
|
||||
if set_allocation_size {
|
||||
tree.set_allocation_size(context.id, new_size)?;
|
||||
} else {
|
||||
tree.set_file_size(context.id, new_size)?;
|
||||
}
|
||||
fill(file_info, tree.info(context.id)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(&self, context: &Handle, buffer: &mut [u8], offset: u64) -> winfsp::Result<u32> {
|
||||
Ok(self.tree().read(context.id, offset, buffer)? as u32)
|
||||
}
|
||||
|
||||
fn write(
|
||||
&self,
|
||||
context: &Handle,
|
||||
buffer: &[u8],
|
||||
offset: u64,
|
||||
write_to_eof: bool,
|
||||
constrained_io: bool,
|
||||
file_info: &mut FileInfo,
|
||||
) -> winfsp::Result<u32> {
|
||||
let mut tree = self.tree();
|
||||
let written = tree.write(context.id, offset, buffer, write_to_eof, constrained_io)?;
|
||||
fill(file_info, tree.info(context.id)?);
|
||||
Ok(written as u32)
|
||||
}
|
||||
|
||||
fn get_volume_info(&self, out_volume_info: &mut VolumeInfo) -> winfsp::Result<()> {
|
||||
let tree = self.tree();
|
||||
out_volume_info.total_size = tree.capacity();
|
||||
out_volume_info.free_size = tree.free();
|
||||
out_volume_info.set_volume_label(VOLUME_LABEL);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// The volume holds no reparse points, but it answers for them as NTFS does,
|
||||
// because callers treat "not a reparse point" and "not supported" apart.
|
||||
fn get_reparse_point_by_name(
|
||||
&self,
|
||||
_file_name: &U16CStr,
|
||||
_is_directory: bool,
|
||||
_buffer: &mut [u8],
|
||||
) -> winfsp::Result<u64> {
|
||||
Err(status(STATUS_NOT_A_REPARSE_POINT))
|
||||
}
|
||||
|
||||
fn get_reparse_point(
|
||||
&self,
|
||||
_context: &Handle,
|
||||
_file_name: &U16CStr,
|
||||
_buffer: &mut [u8],
|
||||
) -> winfsp::Result<u64> {
|
||||
Err(status(STATUS_NOT_A_REPARSE_POINT))
|
||||
}
|
||||
|
||||
fn set_reparse_point(
|
||||
&self,
|
||||
_context: &Handle,
|
||||
_file_name: &U16CStr,
|
||||
_buffer: &[u8],
|
||||
) -> winfsp::Result<()> {
|
||||
Err(status(STATUS_ACCESS_DENIED))
|
||||
}
|
||||
|
||||
fn delete_reparse_point(
|
||||
&self,
|
||||
_context: &Handle,
|
||||
_file_name: &U16CStr,
|
||||
_buffer: &[u8],
|
||||
) -> winfsp::Result<()> {
|
||||
Err(status(STATUS_NOT_A_REPARSE_POINT))
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
LuTriangleAlert,
|
||||
} from "react-icons/lu";
|
||||
import { DnsServerField } from "@/components/dns-server-field";
|
||||
import { EphemeralStorageNotice } from "@/components/ephemeral-storage-notice";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { ProfileGlyph } from "@/components/profile-glyph";
|
||||
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
||||
@@ -1088,6 +1089,8 @@ export function CreateProfileDialog({
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{(ephemeral || enablePassword) && <EphemeralStorageNotice />}
|
||||
|
||||
{enablePassword && (
|
||||
<motion.div className="space-y-1.5" {...reveal}>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuTriangleAlert } from "react-icons/lu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface EphemeralStorageStatus {
|
||||
in_memory: boolean;
|
||||
reason: "winfsp_missing" | "winfsp_error" | null;
|
||||
}
|
||||
|
||||
const WINFSP_DOWNLOAD_URL = "https://winfsp.dev/rel/";
|
||||
|
||||
/** Shown when this computer cannot keep ephemeral profiles in memory. */
|
||||
export function EphemeralStorageNotice({ className }: { className?: string }) {
|
||||
const { t } = useTranslation();
|
||||
const [status, setStatus] = useState<EphemeralStorageStatus | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
invoke<EphemeralStorageStatus>("get_ephemeral_storage_status")
|
||||
.then((next) => {
|
||||
if (active) setStatus(next);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error("Failed to read ephemeral storage status", error);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!status || status.in_memory) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="note"
|
||||
className={cn(
|
||||
"flex items-start gap-2 text-xs text-warning-text",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<LuTriangleAlert className="mt-0.5 size-3.5 shrink-0" aria-hidden />
|
||||
<p>
|
||||
{t(
|
||||
status.reason === "winfsp_error"
|
||||
? "ephemeralStorage.winfspError"
|
||||
: "ephemeralStorage.onDisk",
|
||||
)}{" "}
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="h-auto p-0 text-xs"
|
||||
onClick={() => void openUrl(WINFSP_DOWNLOAD_URL)}
|
||||
>
|
||||
{t("ephemeralStorage.download")}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2667,6 +2667,10 @@
|
||||
"name": "stable_deref_trait",
|
||||
"license": "Apache-2.0 OR MIT"
|
||||
},
|
||||
{
|
||||
"name": "static_assertions",
|
||||
"license": "Apache-2.0 OR MIT"
|
||||
},
|
||||
{
|
||||
"name": "strict-num",
|
||||
"license": "MIT"
|
||||
@@ -3155,6 +3159,10 @@
|
||||
"name": "weezl",
|
||||
"license": "Apache-2.0 OR MIT"
|
||||
},
|
||||
{
|
||||
"name": "widestring",
|
||||
"license": "Apache-2.0 OR MIT"
|
||||
},
|
||||
{
|
||||
"name": "winapi",
|
||||
"license": "Apache-2.0 OR MIT"
|
||||
@@ -3231,6 +3239,14 @@
|
||||
"name": "windows_x86_64_msvc",
|
||||
"license": "Apache-2.0 OR MIT"
|
||||
},
|
||||
{
|
||||
"name": "winfsp",
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
{
|
||||
"name": "winfsp-sys",
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
{
|
||||
"name": "winnow",
|
||||
"license": "MIT"
|
||||
|
||||
@@ -4721,5 +4721,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} startet ohne die eigenen Fingerprint-Änderungen",
|
||||
"fingerprintEditsNeedPro": "Deine Änderungen bleiben gespeichert und gelten wieder mit einem aktiven Pro-Abo."
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "Auf diesem Computer schreiben flüchtige Profile ihre Daten auf den Datenträger. Donut löscht sie, wenn der Browser geschlossen wird. Installieren Sie WinFsp, um die Daten im Arbeitsspeicher zu halten.",
|
||||
"winfspError": "WinFsp ist nicht gestartet, daher schreiben flüchtige Profile ihre Daten auf den Datenträger. Donut löscht sie, wenn der Browser geschlossen wird.",
|
||||
"download": "WinFsp herunterladen"
|
||||
}
|
||||
}
|
||||
@@ -4721,5 +4721,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} launches without its custom fingerprint changes",
|
||||
"fingerprintEditsNeedPro": "Your changes are kept and apply again with an active Pro plan."
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "On this computer, ephemeral profiles write their data to disk. Donut erases it when the browser closes. Install WinFsp to keep the data in memory.",
|
||||
"winfspError": "WinFsp did not start, so ephemeral profiles write their data to disk. Donut erases it when the browser closes.",
|
||||
"download": "Download WinFsp"
|
||||
}
|
||||
}
|
||||
@@ -4771,5 +4771,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} se inicia sin sus cambios personalizados de huella digital",
|
||||
"fingerprintEditsNeedPro": "Tus cambios se conservan y vuelven a aplicarse con un plan Pro activo."
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "En este equipo, los perfiles efímeros escriben sus datos en el disco. Donut los borra cuando se cierra el navegador. Instala WinFsp para mantener los datos en memoria.",
|
||||
"winfspError": "WinFsp no se inició, así que los perfiles efímeros escriben sus datos en el disco. Donut los borra cuando se cierra el navegador.",
|
||||
"download": "Descargar WinFsp"
|
||||
}
|
||||
}
|
||||
@@ -4771,5 +4771,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} démarre sans ses modifications d'empreinte personnalisées",
|
||||
"fingerprintEditsNeedPro": "Vos modifications sont conservées et s'appliquent de nouveau avec un forfait Pro actif."
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "Sur cet ordinateur, les profils éphémères écrivent leurs données sur le disque. Donut les efface à la fermeture du navigateur. Installez WinFsp pour garder les données en mémoire.",
|
||||
"winfspError": "WinFsp n'a pas démarré, donc les profils éphémères écrivent leurs données sur le disque. Donut les efface à la fermeture du navigateur.",
|
||||
"download": "Télécharger WinFsp"
|
||||
}
|
||||
}
|
||||
@@ -4721,5 +4721,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} はカスタムのフィンガープリント変更なしで起動します",
|
||||
"fingerprintEditsNeedPro": "変更は保存されており、有効な Pro プランで再び適用されます。"
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "このコンピューターでは、一時的なプロファイルはデータをディスクに書き込みます。ブラウザを閉じると Donut がそのデータを消去します。データをメモリに保持するには WinFsp をインストールしてください。",
|
||||
"winfspError": "WinFsp が起動しなかったため、一時的なプロファイルはデータをディスクに書き込みます。ブラウザを閉じると Donut がそのデータを消去します。",
|
||||
"download": "WinFsp をダウンロード"
|
||||
}
|
||||
}
|
||||
@@ -4721,5 +4721,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}}은(는) 사용자 지정 지문 변경 없이 실행됩니다",
|
||||
"fingerprintEditsNeedPro": "변경 사항은 보관되며 활성 Pro 플랜에서 다시 적용됩니다."
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "이 컴퓨터에서는 임시 프로필이 데이터를 디스크에 기록합니다. 브라우저를 닫으면 Donut이 데이터를 지웁니다. 데이터를 메모리에 유지하려면 WinFsp를 설치하세요.",
|
||||
"winfspError": "WinFsp가 시작되지 않아 임시 프로필이 데이터를 디스크에 기록합니다. 브라우저를 닫으면 Donut이 데이터를 지웁니다.",
|
||||
"download": "WinFsp 다운로드"
|
||||
}
|
||||
}
|
||||
@@ -4771,5 +4771,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} inicia sem as alterações personalizadas de impressão digital",
|
||||
"fingerprintEditsNeedPro": "Suas alterações ficam guardadas e voltam a valer com um plano Pro ativo."
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "Neste computador, os perfis efêmeros gravam seus dados no disco. O Donut os apaga quando o navegador fecha. Instale o WinFsp para manter os dados na memória.",
|
||||
"winfspError": "O WinFsp não iniciou, então os perfis efêmeros gravam seus dados no disco. O Donut os apaga quando o navegador fecha.",
|
||||
"download": "Baixar o WinFsp"
|
||||
}
|
||||
}
|
||||
@@ -4821,5 +4821,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} запускается без ваших изменений отпечатка",
|
||||
"fingerprintEditsNeedPro": "Изменения сохранены и снова применятся с активным тарифом Pro."
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "На этом компьютере временные профили записывают данные на диск. Donut стирает их, когда браузер закрывается. Установите WinFsp, чтобы хранить данные в памяти.",
|
||||
"winfspError": "WinFsp не запустился, поэтому временные профили записывают данные на диск. Donut стирает их, когда браузер закрывается.",
|
||||
"download": "Скачать WinFsp"
|
||||
}
|
||||
}
|
||||
@@ -4721,5 +4721,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} özel parmak izi değişiklikleri olmadan başlatılıyor",
|
||||
"fingerprintEditsNeedPro": "Değişiklikleriniz saklanır ve etkin bir Pro planıyla yeniden uygulanır."
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "Bu bilgisayarda geçici profiller verilerini diske yazar. Donut, tarayıcı kapandığında bu verileri siler. Verileri bellekte tutmak için WinFsp'yi yükleyin.",
|
||||
"winfspError": "WinFsp başlamadı, bu yüzden geçici profiller verilerini diske yazar. Donut, tarayıcı kapandığında bu verileri siler.",
|
||||
"download": "WinFsp'yi indir"
|
||||
}
|
||||
}
|
||||
@@ -4721,5 +4721,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} khởi chạy mà không có các thay đổi vân tay tùy chỉnh",
|
||||
"fingerprintEditsNeedPro": "Các thay đổi của bạn được giữ lại và sẽ áp dụng lại khi có gói Pro đang hoạt động."
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "Trên máy tính này, hồ sơ tạm thời ghi dữ liệu vào ổ đĩa. Donut xóa dữ liệu đó khi trình duyệt đóng. Hãy cài WinFsp để giữ dữ liệu trong bộ nhớ.",
|
||||
"winfspError": "WinFsp không khởi động được, nên hồ sơ tạm thời ghi dữ liệu vào ổ đĩa. Donut xóa dữ liệu đó khi trình duyệt đóng.",
|
||||
"download": "Tải WinFsp"
|
||||
}
|
||||
}
|
||||
@@ -4721,5 +4721,10 @@
|
||||
"launchNotice": {
|
||||
"fingerprintEditsNotApplied": "{{name}} 将在不含自定义指纹更改的情况下启动",
|
||||
"fingerprintEditsNeedPro": "你的更改已保留,在 Pro 套餐有效时会再次生效。"
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"onDisk": "在这台电脑上,临时配置文件会把数据写入磁盘。浏览器关闭时 Donut 会清除这些数据。安装 WinFsp 可将数据保留在内存中。",
|
||||
"winfspError": "WinFsp 未能启动,因此临时配置文件会把数据写入磁盘。浏览器关闭时 Donut 会清除这些数据。",
|
||||
"download": "下载 WinFsp"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user