fix: prevent lock of old profiles

This commit is contained in:
zhom committed 2026-09-23 16:53:28 +04:00
1 parent bf2e2e0ba7
commit 5aca43c0b3
7 files changed
+138 -11

No files matched your search

+3 -3
View File
@@ -1720,7 +1720,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.31.1"
version = "0.31.2"
dependencies = [
"aes",
"aes-gcm",
@@ -5029,9 +5029,9 @@ dependencies = [
[[package]]
name = "process-wrap"
version = "9.1.0"
version = "9.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55"
checksum = "f95f6259ebd0640685b7cd26d635861f39208df0f89b2b61ff5cf76310fa54f6"
dependencies = [
"futures",
"indexmap 2.14.2",
+103
View File
@@ -1521,3 +1521,106 @@ test("an interactive launch continues the last session once the identity travels
await app.close();
}
});
test("a payload profile an older version stored launches and is stored as an identity", async () => {
assert.ok(process.env.WAYFERN_TEST_TOKEN, "WAYFERN_TEST_TOKEN is required");
const localWayfernVersion = cachedFixtureVersion(
process.env.DONUT_E2E_PROJECT_ROOT,
);
const app = appFromEnvironment("browser-legacy-payload", {
seedVersionCache: localWayfernVersion ?? false,
wayfernTermsAccepted: false,
});
let browserPid;
try {
const prepared = await prepareWayfern(
app,
process.env.DONUT_E2E_PROJECT_ROOT,
);
if (!app.session) await app.start();
await app.invoke("accept_wayfern_terms");
const major = Number.parseInt(prepared.version.split(".")[0], 10);
if (major < 152) {
console.log(
`[donut-e2e] Wayfern ${prepared.version} converts no stored payload; skipping the legacy launch`,
);
return;
}
const profile = await createRealProfile(
app,
prepared.version,
"Stored By 0.30",
);
// The file as v0.30 wrote it: the whole device payload, no identity, and
// none of the fields added since. Its raw JSON never equals the profile it
// parses to, and comparing the two raw once refused every launch of such
// a profile as WAYFERN_BROWSER_BUSY.
const metadataFile = path.join(
app.dataRoot,
"data",
"profiles",
profile.id,
"metadata.json",
);
const stored = JSON.parse(await readFile(metadataFile, "utf8"));
// Set, so listing the profiles never rewrites the file in the new shape.
assert.equal(typeof stored.host_os, "string");
delete stored.temporary;
for (const key of [
"webrtc_mode",
"persona",
"camera_file",
"camera_crop",
"restore_session",
"identity_overrides",
"location",
]) {
delete stored.wayfern_config[key];
}
stored.wayfern_config = JSON.parse(
await readFile(process.env.DONUT_E2E_LEGACY_CONFIG, "utf8"),
);
await writeFile(metadataFile, JSON.stringify(stored, null, 2));
const legacy = (await app.invoke("list_browser_profiles")).find(
(p) => p.id === profile.id,
);
assert.equal(legacy.wayfern_config.identity_id, null);
const launched = await app.invoke("launch_browser_profile", {
profile: legacy,
});
assert.ok(launched.process_id);
browserPid = launched.process_id;
const converted = (await app.invoke("list_browser_profiles")).find(
(p) => p.id === profile.id,
);
assert.equal(
typeof converted.wayfern_config.identity_id,
"string",
"the launch must store the payload profile as an identity",
);
assert.equal(
converted.wayfern_config.fingerprint,
undefined,
"a converted profile must store no device payload",
);
await app.invoke("kill_browser_profile", { profile: converted });
await waitForProcessExit(app, browserPid);
await app.invoke("delete_profile", { profileId: profile.id });
} catch (error) {
await app.capture("failure");
throw error;
} finally {
if (app.session && browserPid && processExists(browserPid)) {
const profile = (
await app.invoke("list_browser_profiles").catch(() => [])
).find((item) => item.process_id === browserPid);
if (profile)
await app.invoke("kill_browser_profile", { profile }).catch(() => {});
}
await app.close();
}
});
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.31.1",
"version": "0.31.2",
"type": "module",
"scripts": {
"predev": "pnpm licenses:generate",
+1 -1
View File
@@ -1738,7 +1738,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.31.1"
version = "0.31.2"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.31.1"
version = "0.31.2"
description = "Simple Yet Powerful Anti-Detect Browser"
authors = ["zhom@github"]
edition = "2021"
+28 -4
View File
@@ -31,6 +31,15 @@ fn atomic_write_locked(path: &Path, data: &[u8]) -> std::io::Result<()> {
fs::rename(&tmp, path)
}
/// Whether a metadata.json still holds `expected`, compared as this version
/// reads it. A file an older version wrote lacks every field added since, so
/// its raw JSON never equals the profile it parses to, and a raw compare
/// reports a change on every call.
fn holds_profile(content: &str, expected: &BrowserProfile) -> Result<bool, serde_json::Error> {
let stored: BrowserProfile = serde_json::from_str(content)?;
Ok(serde_json::to_value(&stored)? == serde_json::to_value(expected)?)
}
/// Collapse an empty proxy/VPN id to `None`.
///
/// REST and MCP clients send `""` to detach a proxy or VPN, since omitting the
@@ -484,12 +493,9 @@ impl ProfileManager {
.join(profile.id.to_string())
.join("metadata.json");
let json = serde_json::to_string_pretty(profile)?;
let expected_json = serde_json::to_string_pretty(expected)?;
let _serialized = METADATA_WRITES.lock().unwrap_or_else(|e| e.into_inner());
let current = fs::read_to_string(&profile_file)?;
if serde_json::from_str::<serde_json::Value>(&current)?
!= serde_json::from_str::<serde_json::Value>(&expected_json)?
{
if !holds_profile(&current, expected)? {
return Ok(false);
}
atomic_write_locked(&profile_file, json.as_bytes())?;
@@ -2087,6 +2093,24 @@ mod tests {
assert!(merged.identity_baseline.is_none());
}
#[test]
fn a_file_an_older_version_wrote_holds_the_profile_it_reads_as() {
// No `temporary` and none of the newer Wayfern fields, as v0.30 wrote it.
let legacy = r#"{
"id": "7b0f7c9e-3f55-4c1b-9d7e-0f2f1f0c5a11",
"name": "Old",
"browser": "wayfern",
"version": "152.0.7977.64",
"wayfern_config": { "fingerprint": "{}", "os": "macos", "identity_id": null }
}"#;
let expected: BrowserProfile = serde_json::from_str(legacy).unwrap();
assert!(holds_profile(legacy, &expected).unwrap());
let mut changed = expected.clone();
changed.wayfern_config.as_mut().unwrap().identity_id = Some("id-1".to_string());
assert!(!holds_profile(legacy, &changed).unwrap());
}
fn create_test_profile_manager() -> (&'static ProfileManager, TempDir) {
let temp_dir = TempDir::new().unwrap();
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut",
"version": "0.31.1",
"version": "0.31.2",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",