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
87 changes: 87 additions & 0 deletions apps/desktop/launcher/tronbrowser
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,47 @@ MSG
fi
fi

# --- Which engine is actually running? --------------------------------------
# TronBrowser ships no engine. It drives whatever Ungoogled Chromium is on the
# system, and the Flatpak that usually provides it updates itself — so a Flathub
# push can change every user's browser overnight while TronBrowser's own history
# shows nothing at all. A regression in scrolling, video or compositing then
# arrives as a TronBrowser bug against a release that touched none of it.
# Record the engine on every launch, and say so out loud when it changes, so the
# first question anyone asks — "what actually changed?" — has an answer here.
ENGINE_VERSION=""
ENGINE_SOURCE=""
if [ "$BROWSER" = "flatpak" ]; then
ENGINE_VERSION="$(flatpak info "$FLATPAK_APP" 2>/dev/null | sed -n 's/^[[:space:]]*Version:[[:space:]]*//p' | head -n1)"
ENGINE_SOURCE="flatpak $FLATPAK_APP $(flatpak info --show-commit "$FLATPAK_APP" 2>/dev/null | cut -c1-12)"
elif [ -n "$MAC_APP" ]; then
# Don't ask the inner binary — run from a shell it SIGABRTs (see the launch
# block below). The bundle records the version anyway.
ENGINE_VERSION="$(defaults read "$MAC_APP/Contents/Info" CFBundleShortVersionString 2>/dev/null || true)"
ENGINE_SOURCE="$MAC_APP"
else
ENGINE_VERSION="$("$BROWSER" --version 2>/dev/null | head -n1)"
ENGINE_SOURCE="$BROWSER"
fi
ENGINE="${ENGINE_VERSION:-unknown} — $ENGINE_SOURCE"
echo "TronBrowser: engine $ENGINE" >&2

# Tor mode wipes its profile on every launch, so a marker kept there would always
# read as a change. Only the normal profile tracks this.
if [ "$TOR" != "1" ]; then
mkdir -p "$DATA"
ENGINE_MARK="$DATA/.tron-engine"
ENGINE_PREV="$(cat "$ENGINE_MARK" 2>/dev/null || true)"
if [ -n "$ENGINE_PREV" ] && [ "$ENGINE_PREV" != "$ENGINE" ]; then
echo "TronBrowser: the browser engine CHANGED since your last launch." >&2
echo " was: $ENGINE_PREV" >&2
echo " now: $ENGINE" >&2
echo " TronBrowser did not do this — your system updated Chromium underneath it." >&2
echo " If the browser now feels different (scrolling, video, GPU), start here." >&2
fi
printf '%s\n' "$ENGINE" > "$ENGINE_MARK" 2>/dev/null || true
fi

# Keep Manifest V2 working so uBlock Origin (MV2) loads — recent Chromium disables
# MV2 by default. (Ungoogled keeps MV2, but force it off everywhere to be safe.)
MV2_KEEP="ExtensionManifestV2Disabled,ExtensionManifestV2Unsupported,ExtensionManifestV2DeprecationWarning,ExtensionManifestV2DeprecationUnsupported"
Expand Down Expand Up @@ -308,6 +349,52 @@ if [ -n "${TRON_AUTOMATION_PORT:-}" ]; then
esac
fi

# --- The caller's flags win over ours ---------------------------------------
# Our flags land on the command line before "$@", and Chromium honors the FIRST
# occurrence of a switch. So `tron --user-data-dir=/tmp/scratch` was silently
# ignored — the browser opened the default profile instead of the one asked for,
# and a duplicated --user-data-dir can fault the process outright. Drop any
# switch of ours that the caller also passed, leaving theirs as the only one.
#
# --enable-features / --disable-features merge rather than being replaced: they
# are comma-separated lists, and dropping ours wholesale would quietly re-enable
# the Manifest V2 kill switch (uBlock Origin stops loading) just because the
# caller wanted one unrelated feature. Rotating through "$@" to pull those two
# out is the same idiom --tor is stripped with above.
#
# This runs BEFORE the Tor block on purpose. Tor's proxy flags are a guarantee,
# not a preference — appending them after this leaves them first on the command
# line, where a caller-supplied --proxy-server cannot displace them.
USER_SWITCHES=" "
USER_ENABLE=""
USER_DISABLE=""
_argc=$#
_i=0
while [ "$_i" -lt "$_argc" ]; do
_a="$1"; shift; _i=$((_i + 1))
case "$_a" in
--enable-features=*)
USER_ENABLE="${USER_ENABLE:+$USER_ENABLE,}${_a#*=}"; continue ;;
--disable-features=*)
USER_DISABLE="${USER_DISABLE:+$USER_DISABLE,}${_a#*=}"; continue ;;
--*=*) USER_SWITCHES="$USER_SWITCHES${_a%%=*} " ;;
--*) USER_SWITCHES="$USER_SWITCHES$_a " ;;
esac
set -- "$@" "$_a"
done

_kept=""
for _f in $FLAGS; do
case "$_f" in
--enable-features=*) _f="$_f${USER_ENABLE:+,$USER_ENABLE}" ;;
--disable-features=*) _f="$_f${USER_DISABLE:+,$USER_DISABLE}" ;;
--*=*) case "$USER_SWITCHES" in *" ${_f%%=*} "*) continue ;; esac ;;
--*) case "$USER_SWITCHES" in *" $_f "*) continue ;; esac ;;
esac
_kept="${_kept:+$_kept }$_f"
done
FLAGS="$_kept"

# --- Start the Tor daemon (only in --tor mode) ---------------------------
# Resolve a bundled `tor` next to the launcher, else one on PATH. Start it on a
# loopback SOCKS port, wait for the circuit to bootstrap, then add the proxy
Expand Down
142 changes: 142 additions & 0 deletions apps/desktop/test/launcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// The launcher shim decides what Chromium runs and with which flags, and it is
// the one piece of TronBrowser with no compiler and no types behind it. These
// tests run the real script against a stub browser that records its argv, so a
// 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 } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';

const HERE = dirname(fileURLToPath(import.meta.url));
const LAUNCHER = join(HERE, '..', 'launcher', 'tronbrowser');

type Run = { argv: string[]; stderr: string; home: string };

const homes: string[] = [];

/**
* Run the launcher with a stub browser and return the argv it was handed.
*
* The script is copied somewhere else first, on purpose: it resolves helpers
* relative to its own directory, and a copy has no tron-tor-helper beside it,
* so the background Tor helper is skipped instead of binding a port under test.
*/
function run(args: string[], opts: { version?: string; home?: string } = {}): Run {
const home = opts.home ?? mkdtempSync(join(tmpdir(), 'tron-launcher-'));
if (!opts.home) homes.push(home);

const dir = join(home, 'bin');
mkdirSync(dir, { recursive: true });
const script = join(dir, 'tronbrowser');
copyFileSync(LAUNCHER, script);
chmodSync(script, 0o755);

// Named "ungoogled-chromium" so the launcher's de-googled check stays quiet.
const argvOut = join(home, 'argv.txt');
const browser = join(dir, 'ungoogled-chromium');
writeFileSync(
browser,
[
'#!/bin/sh',
`if [ "\${1:-}" = "--version" ]; then echo "${opts.version ?? 'Chromium 100.0.0.0'}"; exit 0; fi`,
`: > "${argvOut}"`,
`for a in "$@"; do printf '%s\\n' "$a" >> "${argvOut}"; done`,
'exit 0',
].join('\n'),
{ mode: 0o755 },
);

// TRONBROWSER_VERBOSE keeps the launcher from routing the browser's stderr
// into its log file, so the engine lines reach us here.
const result = spawnSync('sh', [script, ...args], {
encoding: 'utf8',
env: {
PATH: process.env.PATH ?? '/usr/bin:/bin',
HOME: home,
TRONBROWSER_BROWSER: browser,
TRONBROWSER_DATA: join(home, 'profile'),
TRONBROWSER_VERBOSE: '1',
},
});
if (result.status !== 0) {
throw new Error(`launcher exited ${result.status}\n${result.stderr ?? ''}`);
}

return {
argv: existsSync(argvOut) ? readFileSync(argvOut, 'utf8').split('\n').filter(Boolean) : [],
stderr: result.stderr ?? '',
home,
};
}

const valueOf = (argv: string[], name: string): string[] =>
argv.filter((a) => a.startsWith(`${name}=`)).map((a) => a.slice(name.length + 1));

afterEach(() => {
homes.length = 0;
});

describe('launcher flags', () => {
it('passes its own profile when the caller asks for nothing', () => {
const { argv, home } = run([]);
expect(valueOf(argv, '--user-data-dir')).toEqual([join(home, 'profile')]);
});

it('lets the caller replace a switch instead of duplicating it', () => {
// Chromium honors the FIRST --user-data-dir, so a duplicate meant the
// caller's was ignored and the default profile opened instead.
const { argv } = run(['--user-data-dir=/tmp/tron-scratch']);
expect(valueOf(argv, '--user-data-dir')).toEqual(['/tmp/tron-scratch']);
});

it('merges --enable-features rather than dropping its own', () => {
const { argv } = run(['--enable-features=WebGPU']);
const enabled = valueOf(argv, '--enable-features');
expect(enabled).toHaveLength(1);
expect(enabled[0]!.split(',')).toEqual(expect.arrayContaining(['EnableTabMuting', 'WebGPU']));
});

it('merges --disable-features, keeping the Manifest V2 kill switch off', () => {
// Dropping ours here would stop uBlock Origin (MV2) from loading.
const { argv } = run(['--disable-features=Foo']);
const disabled = valueOf(argv, '--disable-features');
expect(disabled).toHaveLength(1);
expect(disabled[0]!.split(',')).toEqual(
expect.arrayContaining(['ExtensionManifestV2Disabled', 'Translate', 'Foo']),
);
});

it('leaves URLs and other arguments alone', () => {
const { argv } = run(['https://tronbrowser.dev', '--incognito']);
expect(argv).toContain('https://tronbrowser.dev');
expect(argv).toContain('--incognito');
});
});

describe('engine reporting', () => {
it('names the engine it is about to run', () => {
const { stderr } = run([], { version: 'Chromium 141.0.0.0' });
expect(stderr).toContain('TronBrowser: engine Chromium 141.0.0.0');
});

it('says so when the engine changed underneath it', () => {
// The case this exists for: a Flatpak updates itself overnight and the
// browser feels different, with nothing in TronBrowser's history to blame.
const first = run([], { version: 'Chromium 141.0.0.0' });
expect(first.stderr).not.toContain('CHANGED');

const second = run([], { version: 'Chromium 149.0.0.0', home: first.home });
expect(second.stderr).toContain('the browser engine CHANGED since your last launch');
expect(second.stderr).toContain('was: Chromium 141.0.0.0');
expect(second.stderr).toContain('now: Chromium 149.0.0.0');
});

it('stays quiet when the engine is the same as last time', () => {
const first = run([], { version: 'Chromium 141.0.0.0' });
const second = run([], { version: 'Chromium 141.0.0.0', home: first.home });
expect(second.stderr).not.toContain('CHANGED');
});
});
Loading