Skip to content

Commit 1e2a320

Browse files
ralyodioclaude
andauthored
fix(search): actually move stock-engine profiles to pit.world (#143)
3.17.0 only replaced an engine the launcher had set itself, so a profile on one of Chromium's stock engines (Ecosia from the choice screen, Google, Bing) kept it, and .tron-search was still written as pit, so it never retried. pit.world then never showed in chrome://settings/searchEngines either, since Chromium only adds the default to that list. - A one-time pass (.tron-search-stock) also replaces a stock engine nobody edited (prepopulate_id > 0, safe_for_autoreplace). Edited engines, engines the user added, and `tron search <x>` are still left alone. - Clear default_search_provider.guid and rewrite the mirrored copy: Chromium otherwise re-selects the old engine from Web Data by that guid. - Skip while the profile is open (SingletonLock -> live pid): Chromium rewrites Preferences on exit and the edit would be lost. Marks are written only after the edit actually lands. Checked against a copy of a real Ecosia profile on Chromium 152: pit.world becomes the default and is added to the keywords table. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4837b76 commit 1e2a320

2 files changed

Lines changed: 111 additions & 10 deletions

File tree

‎apps/desktop/launcher/tronbrowser‎

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,26 @@ SEARCH_PREV="$(cat "$SEARCH_MARK" 2>/dev/null || true)"
496496
SEARCH_PREV="$(printf '%s' "$SEARCH_PREV" | tr -d '[:space:]')"
497497
if [ -z "$SEARCH_PREV" ] && [ -f "$DATA/.tron-search-kagi" ]; then SEARCH_PREV="kagi"; fi
498498

499-
if command -v python3 >/dev/null 2>&1 && [ "$SEARCH_PREV" != "$SEARCH_WANT" ]; then
499+
# 3.17.0 marked the move to pit.world done even where it left the engine alone,
500+
# so profiles on one of Chromium's stock engines (Ecosia, Google, Bing ...) never
501+
# moved. One more pass replaces a stock engine nobody edited; .tron-search-stock
502+
# records that it ran.
503+
SEARCH_STOCK_MARK="$DATA/.tron-search-stock"
504+
SEARCH_STOCK=0
505+
if [ "$SEARCH_WANT" = pit ] && [ "$SEARCH_EXPLICIT" = 0 ] && [ ! -f "$SEARCH_STOCK_MARK" ]; then SEARCH_STOCK=1; fi
506+
507+
# Chromium rewrites Preferences from memory when it exits, so an edit made while
508+
# this profile is open is silently lost. Leave it, and the marks, for a launch
509+
# that finds the browser closed.
510+
profile_in_use() {
511+
local lock pid
512+
lock="$(readlink "$DATA/SingletonLock" 2>/dev/null)" || return 1
513+
pid="${lock##*-}"
514+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
515+
kill -0 "$pid" 2>/dev/null
516+
}
517+
518+
if command -v python3 >/dev/null 2>&1 && { [ "$SEARCH_PREV" != "$SEARCH_WANT" ] || [ "$SEARCH_STOCK" = 1 ]; } && ! profile_in_use; then
500519
SEARCH_PREV_URL=""
501520
if [ -n "$SEARCH_PREV" ]; then
502521
SEARCH_PREV_URL="$(search_engine_spec "$SEARCH_PREV" 2>/dev/null | cut -d'|' -f3 || true)"
@@ -505,12 +524,14 @@ if command -v python3 >/dev/null 2>&1 && [ "$SEARCH_PREV" != "$SEARCH_WANT" ]; t
505524
TB_ENGINE="$(search_engine_spec "$SEARCH_WANT")" \
506525
TB_PREV_URL="$SEARCH_PREV_URL" \
507526
TB_FORCE="$SEARCH_EXPLICIT" \
508-
python3 - <<'PY' || true
527+
TB_STOCK="$SEARCH_STOCK" \
528+
python3 - <<'PY' && SEARCH_APPLIED=1 || SEARCH_APPLIED=0
509529
import json, os, sys, tempfile
510530
p = os.environ["TB_PREFS"]
511531
name, keyword, url = os.environ["TB_ENGINE"].split("|")
512532
prev_url = os.environ.get("TB_PREV_URL", "")
513533
force = os.environ.get("TB_FORCE") == "1"
534+
stock = os.environ.get("TB_STOCK") == "1"
514535
os.makedirs(os.path.dirname(p), exist_ok=True)
515536
# An existing file that does not parse is LEFT ALONE. Replacing it with a fresh
516537
# dict is how a profile gets wiped: this file holds every setting in the profile,
@@ -522,7 +543,7 @@ if os.path.exists(p):
522543
d = json.load(f)
523544
except Exception:
524545
sys.stderr.write("TronBrowser: %s did not parse — leaving it untouched.\n" % p)
525-
raise SystemExit(0)
546+
raise SystemExit(1)
526547
else:
527548
d = {}
528549
@@ -532,24 +553,38 @@ else:
532553
# `tron search` is the one case that outranks whatever is currently set.
533554
cur = d.get("default_search_provider_data", {}).get("template_url_data") or {}
534555
cur_url = cur.get("url", "")
535-
if force or not cur_url or (prev_url and cur_url == prev_url):
536-
d.setdefault("default_search_provider_data", {})["template_url_data"] = {
556+
# Chromium's own stock engines (prepopulate_id > 0 and never edited, so still
557+
# safe_for_autoreplace) are not a choice anyone made in this browser, so the
558+
# move to pit.world replaces those too.
559+
is_stock = int(cur.get("prepopulate_id") or 0) > 0 and cur.get("safe_for_autoreplace") is True
560+
if force or not cur_url or (prev_url and cur_url == prev_url) or (stock and is_stock):
561+
engine = {
537562
"short_name": name,
538563
"keyword": keyword,
539564
"url": url,
540565
"favicon_url": "https://" + keyword.split("/")[0] + "/favicon.ico",
541566
"safe_for_autoreplace": False,
542567
}
568+
dsp = d.setdefault("default_search_provider_data", {})
569+
dsp["template_url_data"] = engine
570+
# The mirror and the guid still point at the old engine, and Chromium would
571+
# pick it back out of Web Data by that guid.
572+
if "mirrored_template_url_data" in dsp:
573+
dsp["mirrored_template_url_data"] = dict(engine)
574+
(d.get("default_search_provider") or {}).pop("guid", None)
543575
# Preferences is the profile. A truncated write loses every setting in it,
544576
# so land it as a rename rather than in place.
545577
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p))
546578
with os.fdopen(fd, "w") as f:
547579
json.dump(d, f)
548580
os.replace(tmp, p)
549581
PY
550-
mkdir -p "$DATA"
551-
printf '%s\n' "$SEARCH_WANT" > "$SEARCH_MARK" 2>/dev/null || true
552-
rm -f "$DATA/.tron-search-kagi" 2>/dev/null || true
582+
if [ "$SEARCH_APPLIED" = 1 ]; then
583+
mkdir -p "$DATA"
584+
printf '%s\n' "$SEARCH_WANT" > "$SEARCH_MARK" 2>/dev/null || true
585+
rm -f "$DATA/.tron-search-kagi" 2>/dev/null || true
586+
if [ "$SEARCH_WANT" = pit ]; then : > "$SEARCH_STOCK_MARK" 2>/dev/null || true; fi
587+
fi
553588
fi
554589

555590
# Show the feed (ai-sidebar New Tab page) on startup. We can't pass a startup URL —

‎apps/desktop/test/launcher.test.ts‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
// change to flag handling has to survive something before it ships.
55

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

234+
/** A profile whose default is one of Chromium's stock engines, as the choice screen leaves it. */
235+
function seedStock(opts: { marker?: string; edited?: boolean } = {}): string {
236+
const home = seed({ marker: opts.marker });
237+
const ecosia = {
238+
short_name: 'Ecosia',
239+
keyword: 'ecosia.org',
240+
url: 'https://www.ecosia.org/search?q={searchTerms}',
241+
prepopulate_id: 101,
242+
safe_for_autoreplace: !opts.edited,
243+
synced_guid: '485bf7d3-0215-45af-87dc-538868000101',
244+
};
245+
writeFileSync(
246+
prefsPath(home),
247+
JSON.stringify({
248+
default_search_provider: { guid: ecosia.synced_guid, reset_occurred: false },
249+
default_search_provider_data: { template_url_data: ecosia, mirrored_template_url_data: ecosia },
250+
bookmark_bar: { show_on_all_tabs: true },
251+
}),
252+
);
253+
return home;
254+
}
255+
256+
it('moves a profile 3.17.0 marked as done but left on a stock engine (Ecosia)', () => {
257+
// 3.17.0 wrote .tron-search=pit without touching a stock engine, so the
258+
// switch never happened and never retried.
259+
const home = seedStock({ marker: 'pit' });
260+
run([], { home });
261+
const json = JSON.parse(readFileSync(prefsPath(home), 'utf8'));
262+
expect(dse(home).url).toBe('https://pit.world/search?q={searchTerms}');
263+
expect(json.default_search_provider_data.mirrored_template_url_data.url).toBe('https://pit.world/search?q={searchTerms}');
264+
// Chromium would otherwise pull Ecosia back out of Web Data by this guid.
265+
expect(json.default_search_provider.guid).toBeUndefined();
266+
expect(json.bookmark_bar?.show_on_all_tabs).toBe(true);
267+
expect(existsSync(join(home, 'profile', '.tron-search-stock'))).toBe(true);
268+
});
269+
270+
it('keeps a stock engine the user edited', () => {
271+
const home = seedStock({ marker: 'pit', edited: true });
272+
run([], { home });
273+
expect(dse(home).url).toBe('https://www.ecosia.org/search?q={searchTerms}');
274+
});
275+
276+
it('does the stock pass once, so a later choice of a stock engine sticks', () => {
277+
const home = seedStock({ marker: 'pit' });
278+
run([], { home });
279+
const p = prefsPath(home);
280+
const json = JSON.parse(readFileSync(p, 'utf8'));
281+
json.default_search_provider_data.template_url_data = { url: 'https://www.bing.com/search?q={searchTerms}', prepopulate_id: 3, safe_for_autoreplace: true };
282+
writeFileSync(p, JSON.stringify(json));
283+
run([], { home });
284+
expect(dse(home).url).toBe('https://www.bing.com/search?q={searchTerms}');
285+
});
286+
287+
it('leaves an open profile alone and applies on the next launch', () => {
288+
// Chromium rewrites Preferences when it exits, so an edit made under it is lost.
289+
const home = seedStock({ marker: 'pit' });
290+
const lock = join(home, 'profile', 'SingletonLock');
291+
symlinkSync(`${hostname()}-${process.pid}`, lock);
292+
run([], { home });
293+
expect(dse(home).url).toBe('https://www.ecosia.org/search?q={searchTerms}');
294+
expect(existsSync(join(home, 'profile', '.tron-search-stock'))).toBe(false);
295+
unlinkSync(lock);
296+
run([], { home });
297+
expect(dse(home).url).toBe('https://pit.world/search?q={searchTerms}');
298+
});
299+
234300
it('keeps the rest of Preferences when it rewrites the engine', () => {
235301
const home = seed({ url: KAGI_URL, legacyKagi: true });
236302
run([], { home });

0 commit comments

Comments
 (0)