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
51 changes: 43 additions & 8 deletions apps/desktop/launcher/tronbrowser
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,26 @@ SEARCH_PREV="$(cat "$SEARCH_MARK" 2>/dev/null || true)"
SEARCH_PREV="$(printf '%s' "$SEARCH_PREV" | tr -d '[:space:]')"
if [ -z "$SEARCH_PREV" ] && [ -f "$DATA/.tron-search-kagi" ]; then SEARCH_PREV="kagi"; fi

if command -v python3 >/dev/null 2>&1 && [ "$SEARCH_PREV" != "$SEARCH_WANT" ]; then
# 3.17.0 marked the move to pit.world done even where it left the engine alone,
# so profiles on one of Chromium's stock engines (Ecosia, Google, Bing ...) never
# moved. One more pass replaces a stock engine nobody edited; .tron-search-stock
# records that it ran.
SEARCH_STOCK_MARK="$DATA/.tron-search-stock"
SEARCH_STOCK=0
if [ "$SEARCH_WANT" = pit ] && [ "$SEARCH_EXPLICIT" = 0 ] && [ ! -f "$SEARCH_STOCK_MARK" ]; then SEARCH_STOCK=1; fi

# Chromium rewrites Preferences from memory when it exits, so an edit made while
# this profile is open is silently lost. Leave it, and the marks, for a launch
# that finds the browser closed.
profile_in_use() {
local lock pid
lock="$(readlink "$DATA/SingletonLock" 2>/dev/null)" || return 1
pid="${lock##*-}"
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
kill -0 "$pid" 2>/dev/null
}

if command -v python3 >/dev/null 2>&1 && { [ "$SEARCH_PREV" != "$SEARCH_WANT" ] || [ "$SEARCH_STOCK" = 1 ]; } && ! profile_in_use; then
SEARCH_PREV_URL=""
if [ -n "$SEARCH_PREV" ]; then
SEARCH_PREV_URL="$(search_engine_spec "$SEARCH_PREV" 2>/dev/null | cut -d'|' -f3 || true)"
Expand All @@ -505,12 +524,14 @@ 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' || true
TB_STOCK="$SEARCH_STOCK" \
python3 - <<'PY' && SEARCH_APPLIED=1 || SEARCH_APPLIED=0
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"
stock = os.environ.get("TB_STOCK") == "1"
os.makedirs(os.path.dirname(p), exist_ok=True)
# 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,
Expand All @@ -522,7 +543,7 @@ if os.path.exists(p):
d = json.load(f)
except Exception:
sys.stderr.write("TronBrowser: %s did not parse — leaving it untouched.\n" % p)
raise SystemExit(0)
raise SystemExit(1)
else:
d = {}

Expand All @@ -532,24 +553,38 @@ else:
# `tron search` is the one case that outranks whatever is currently set.
cur = d.get("default_search_provider_data", {}).get("template_url_data") or {}
cur_url = cur.get("url", "")
if force or not cur_url or (prev_url and cur_url == prev_url):
d.setdefault("default_search_provider_data", {})["template_url_data"] = {
# Chromium's own stock engines (prepopulate_id > 0 and never edited, so still
# safe_for_autoreplace) are not a choice anyone made in this browser, so the
# move to pit.world replaces those too.
is_stock = int(cur.get("prepopulate_id") or 0) > 0 and cur.get("safe_for_autoreplace") is True
if force or not cur_url or (prev_url and cur_url == prev_url) or (stock and is_stock):
engine = {
"short_name": name,
"keyword": keyword,
"url": url,
"favicon_url": "https://" + keyword.split("/")[0] + "/favicon.ico",
"safe_for_autoreplace": False,
}
dsp = d.setdefault("default_search_provider_data", {})
dsp["template_url_data"] = engine
# The mirror and the guid still point at the old engine, and Chromium would
# pick it back out of Web Data by that guid.
if "mirrored_template_url_data" in dsp:
dsp["mirrored_template_url_data"] = dict(engine)
(d.get("default_search_provider") or {}).pop("guid", None)
# Preferences is the profile. A truncated write loses every setting in it,
# so land it as a rename rather than in place.
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p))
with os.fdopen(fd, "w") as f:
json.dump(d, f)
os.replace(tmp, p)
PY
mkdir -p "$DATA"
printf '%s\n' "$SEARCH_WANT" > "$SEARCH_MARK" 2>/dev/null || true
rm -f "$DATA/.tron-search-kagi" 2>/dev/null || true
if [ "$SEARCH_APPLIED" = 1 ]; then
mkdir -p "$DATA"
printf '%s\n' "$SEARCH_WANT" > "$SEARCH_MARK" 2>/dev/null || true
rm -f "$DATA/.tron-search-kagi" 2>/dev/null || true
if [ "$SEARCH_WANT" = pit ]; then : > "$SEARCH_STOCK_MARK" 2>/dev/null || true; fi
fi
fi

# Show the feed (ai-sidebar New Tab page) on startup. We can't pass a startup URL —
Expand Down
70 changes: 68 additions & 2 deletions apps/desktop/test/launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
// change to flag handling has to survive something before it ships.

import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync, readFileSync, chmodSync, existsSync, symlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync, readFileSync, chmodSync, existsSync, symlinkSync, unlinkSync } from 'node:fs';
import { hostname, tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
Expand Down Expand Up @@ -231,6 +231,72 @@ describe('omnibox search engine', () => {
expect(dse(first.home).url).toBe(chosen);
});

/** A profile whose default is one of Chromium's stock engines, as the choice screen leaves it. */
function seedStock(opts: { marker?: string; edited?: boolean } = {}): string {
const home = seed({ marker: opts.marker });
const ecosia = {
short_name: 'Ecosia',
keyword: 'ecosia.org',
url: 'https://www.ecosia.org/search?q={searchTerms}',
prepopulate_id: 101,
safe_for_autoreplace: !opts.edited,
synced_guid: '485bf7d3-0215-45af-87dc-538868000101',
};
writeFileSync(
prefsPath(home),
JSON.stringify({
default_search_provider: { guid: ecosia.synced_guid, reset_occurred: false },
default_search_provider_data: { template_url_data: ecosia, mirrored_template_url_data: ecosia },
bookmark_bar: { show_on_all_tabs: true },
}),
);
return home;
}

it('moves a profile 3.17.0 marked as done but left on a stock engine (Ecosia)', () => {
// 3.17.0 wrote .tron-search=pit without touching a stock engine, so the
// switch never happened and never retried.
const home = seedStock({ marker: 'pit' });
run([], { home });
const json = JSON.parse(readFileSync(prefsPath(home), 'utf8'));
expect(dse(home).url).toBe('https://pit.world/search?q={searchTerms}');
expect(json.default_search_provider_data.mirrored_template_url_data.url).toBe('https://pit.world/search?q={searchTerms}');
// Chromium would otherwise pull Ecosia back out of Web Data by this guid.
expect(json.default_search_provider.guid).toBeUndefined();
expect(json.bookmark_bar?.show_on_all_tabs).toBe(true);
expect(existsSync(join(home, 'profile', '.tron-search-stock'))).toBe(true);
});

it('keeps a stock engine the user edited', () => {
const home = seedStock({ marker: 'pit', edited: true });
run([], { home });
expect(dse(home).url).toBe('https://www.ecosia.org/search?q={searchTerms}');
});

it('does the stock pass once, so a later choice of a stock engine sticks', () => {
const home = seedStock({ marker: 'pit' });
run([], { home });
const p = prefsPath(home);
const json = JSON.parse(readFileSync(p, 'utf8'));
json.default_search_provider_data.template_url_data = { url: 'https://www.bing.com/search?q={searchTerms}', prepopulate_id: 3, safe_for_autoreplace: true };
writeFileSync(p, JSON.stringify(json));
run([], { home });
expect(dse(home).url).toBe('https://www.bing.com/search?q={searchTerms}');
});

it('leaves an open profile alone and applies on the next launch', () => {
// Chromium rewrites Preferences when it exits, so an edit made under it is lost.
const home = seedStock({ marker: 'pit' });
const lock = join(home, 'profile', 'SingletonLock');
symlinkSync(`${hostname()}-${process.pid}`, lock);
run([], { home });
expect(dse(home).url).toBe('https://www.ecosia.org/search?q={searchTerms}');
expect(existsSync(join(home, 'profile', '.tron-search-stock'))).toBe(false);
unlinkSync(lock);
run([], { home });
expect(dse(home).url).toBe('https://pit.world/search?q={searchTerms}');
});

it('keeps the rest of Preferences when it rewrites the engine', () => {
const home = seed({ url: KAGI_URL, legacyKagi: true });
run([], { home });
Expand Down
Loading