Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
node_modules/
node_modules
dist/
build/
out/
Expand Down
89 changes: 72 additions & 17 deletions apps/desktop/launcher/tronbrowser
Original file line number Diff line number Diff line change
Expand Up @@ -170,19 +170,42 @@ fi
# "Always prompt for install". Pre-seed it in the profile's Local State so the
# bundled chromium-web-store can install extensions. Best-effort (needs python3).
if command -v python3 >/dev/null 2>&1; then
TB_LOCAL_STATE="$DATA/Local State" python3 - <<'PY' 2>/dev/null || true
import json, os
TB_LOCAL_STATE="$DATA/Local State" python3 - <<'PY' || true
import json, os, sys, tempfile
p = os.environ["TB_LOCAL_STATE"]
os.makedirs(os.path.dirname(p), exist_ok=True)
try:
d = json.load(open(p)) if os.path.exists(p) else {}
except Exception:
# An existing file that does not parse is LEFT ALONE. Replacing it with a fresh
# dict is how a profile gets wiped: this file holds every setting in the profile,
# so a stub is indistinguishable from a factory reset — and the usual reason it
# does not parse is a truncating write like the one this block used to do.
if os.path.exists(p):
try:
with open(p) as f:
d = json.load(f)
except Exception:
sys.stderr.write("TronBrowser: %s did not parse — leaving it untouched.\n" % p)
raise SystemExit(0)
else:
d = {}
exp = d.setdefault("browser", {}).setdefault("enabled_labs_experiments", [])
flag = "extension-mime-request-handling@2"
if flag not in exp:
exp.append(flag)
json.dump(d, open(p, "w"))
# Land it as a rename. This file is the profile; a truncated in-place write
# loses every setting in it, and the next launch then reads it as unparseable.
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p))
try:
with os.fdopen(fd, "w") as f:
json.dump(d, f)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, p)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
raise
PY
fi

Expand Down Expand Up @@ -240,16 +263,25 @@ if command -v python3 >/dev/null 2>&1 && [ "$SEARCH_PREV" != "$SEARCH_WANT" ]; t
TB_ENGINE="$(search_engine_spec "$SEARCH_WANT")" \
TB_PREV_URL="$SEARCH_PREV_URL" \
TB_FORCE="$SEARCH_EXPLICIT" \
python3 - <<'PY' 2>/dev/null || true
import json, os, tempfile
python3 - <<'PY' || true
import json, os, sys, tempfile
p = os.environ["TB_PREFS"]
name, keyword, url = os.environ["TB_ENGINE"].split("|")
prev_url = os.environ.get("TB_PREV_URL", "")
force = os.environ.get("TB_FORCE") == "1"
os.makedirs(os.path.dirname(p), exist_ok=True)
try:
d = json.load(open(p)) if os.path.exists(p) else {}
except Exception:
# An existing file that does not parse is LEFT ALONE. Replacing it with a fresh
# dict is how a profile gets wiped: this file holds every setting in the profile,
# so a stub is indistinguishable from a factory reset — and the usual reason it
# does not parse is a truncating write like the one this block used to do.
if os.path.exists(p):
try:
with open(p) as f:
d = json.load(f)
except Exception:
sys.stderr.write("TronBrowser: %s did not parse — leaving it untouched.\n" % p)
raise SystemExit(0)
else:
d = {}

# Only ever overwrite an engine that is absent or that we put there ourselves.
Expand Down Expand Up @@ -284,16 +316,39 @@ fi
# goes through the new-tab path, which honors the chrome_url_overrides feed. Once
# per profile, then respect the user's chrome://settings/onStartup choice.
if command -v python3 >/dev/null 2>&1 && [ ! -f "$DATA/.tron-startup-ntp" ]; then
TB_PREFS="$DATA/Default/Preferences" python3 - <<'PY' 2>/dev/null || true
import json, os
TB_PREFS="$DATA/Default/Preferences" python3 - <<'PY' || true
import json, os, sys, tempfile
p = os.environ["TB_PREFS"]
os.makedirs(os.path.dirname(p), exist_ok=True)
try:
d = json.load(open(p)) if os.path.exists(p) else {}
except Exception:
# An existing file that does not parse is LEFT ALONE. Replacing it with a fresh
# dict is how a profile gets wiped: this file holds every setting in the profile,
# so a stub is indistinguishable from a factory reset — and the usual reason it
# does not parse is a truncating write like the one this block used to do.
if os.path.exists(p):
try:
with open(p) as f:
d = json.load(f)
except Exception:
sys.stderr.write("TronBrowser: %s did not parse — leaving it untouched.\n" % p)
raise SystemExit(0)
else:
d = {}
d.setdefault("session", {})["restore_on_startup"] = 5 # 5 = open the New Tab page
json.dump(d, open(p, "w"))
# Land it as a rename. This file is the profile; a truncated in-place write
# loses every setting in it, and the next launch then reads it as unparseable.
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p))
try:
with os.fdopen(fd, "w") as f:
json.dump(d, f)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, p)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
raise
PY
mkdir -p "$DATA"; : > "$DATA/.tron-startup-ntp"
fi
Expand Down
65 changes: 65 additions & 0 deletions apps/desktop/test/launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,71 @@ describe('omnibox search engine', () => {
});
});

// --- Damaged profile ---------------------------------------------------------
// Three blocks in the launcher edit Chromium's JSON state files. Every one of
// them used to do `except Exception: d = {}` and then write the result — so a
// file that failed to parse was REPLACED by a stub holding only the key that
// block cared about. Preferences is the whole profile, which made that a silent
// factory reset, and the usual reason it failed to parse was the non-atomic
// write the same block did on the previous launch. These tests pin the rule:
// a state file we cannot read is a file we do not touch.

const localStatePath = (home: string) => join(home, 'profile', 'Local State');

/** A profile whose JSON state files are present but corrupt. */
function seedCorrupt(which: 'prefs' | 'localstate'): { home: string; path: string } {
const home = mkdtempSync(join(tmpdir(), 'tron-launcher-'));
homes.push(home);
const path = which === 'prefs' ? prefsPath(home) : localStatePath(home);
mkdirSync(dirname(path), { recursive: true });
// What a truncated write leaves behind: valid JSON's opening, then nothing.
writeFileSync(path, '{"default_search_provider_data": {"template_ur');
return { home, path };
}

describe('damaged profile', () => {
it('leaves an unparseable Preferences exactly as it found it', () => {
const { home, path } = seedCorrupt('prefs');
const before = readFileSync(path, 'utf8');
run([], { home });
expect(readFileSync(path, 'utf8')).toBe(before);
});

it('leaves an unparseable Local State exactly as it found it', () => {
const { home, path } = seedCorrupt('localstate');
const before = readFileSync(path, 'utf8');
run([], { home });
expect(readFileSync(path, 'utf8')).toBe(before);
});

it('says which file it refused to touch, rather than failing silently', () => {
const { home } = seedCorrupt('prefs');
const { stderr } = run([], { home });
expect(stderr).toContain('did not parse');
});

it('still launches the browser when a state file is unreadable', () => {
// Refusing to write must not become refusing to start.
const { home } = seedCorrupt('prefs');
const { argv } = run([], { home });
expect(valueOf(argv, '--user-data-dir')).toEqual([join(home, 'profile')]);
});

it('keeps unrelated settings when it sets the startup page', () => {
// The restore_on_startup block rewrites the whole file to change one key.
const home = mkdtempSync(join(tmpdir(), 'tron-launcher-'));
homes.push(home);
const path = prefsPath(home);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, JSON.stringify({ bookmark_bar: { show_on_all_tabs: true }, extensions: { settings: { abc: 1 } } }));
run([], { home });
const json = JSON.parse(readFileSync(path, 'utf8'));
expect(json.session?.restore_on_startup).toBe(5);
expect(json.bookmark_bar?.show_on_all_tabs).toBe(true);
expect(json.extensions?.settings?.abc).toBe(1);
});
});

describe('engine reporting', () => {
it('names the engine it is about to run', () => {
const { stderr } = run([], { version: 'Chromium 141.0.0.0' });
Expand Down
Loading