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
112 changes: 109 additions & 3 deletions apps/web/public/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Usage:
tron trace start|stop Record commands into a .trontrace bundle
tron replay <bundle> Replay a recorded trace against the session
tron upgrade Update to the latest release
tron clean Clear browser caches (keeps bookmarks, logins, history)
tron remove Uninstall TronBrowser (keeps your profile data)
tron version Print the installed version
tron help Show this help
Expand Down Expand Up @@ -249,6 +250,24 @@ case "${1:-}" in
exec "$TORBIN" --SocksPort 127.0.0.1:9071 --DataDirectory "$TOR_DATA" ;;
upgrade|update)
exec sh -c "curl -fsSL '$INSTALL_URL' | sh -s -- upgrade" ;;
clean)
# `tron upgrade` clears these too, once they pass a size threshold — see
# prune_profile_caches in install.sh. This copy is deliberate: cleaning a
# bloated profile is exactly when you don't want to need the network.
# Regenerable caches only; bookmarks, passwords, history and cookies stay.
for _data in "${TRONBROWSER_DATA:-$HOME/.tronbrowser}" "$HOME/TronBrowser"; do
[ -d "$_data/Default" ] || continue
if command -v pgrep >/dev/null 2>&1 && pgrep -f "user-data-dir=$_data" >/dev/null 2>&1; then
echo "TronBrowser is running — quit it first, then run 'tron clean'." >&2
continue
fi
_freed="$(du -sm "$_data/Default/Cache" "$_data/Default/Code Cache" \
"$_data/Default/Service Worker" 2>/dev/null \
| awk '{t += $1} END {print t + 0}')"
[ "${_freed:-0}" -gt 0 ] || continue
rm -rf "$_data/Default/Cache" "$_data/Default/Code Cache" "$_data/Default/Service Worker"
echo "Freed ~${_freed}MB from $_data. Bookmarks, passwords and logins untouched."
done ;;
remove|uninstall)
rm -rf "$APP_DIR"
rm -f "$PREFIX/bin/tron" "$PREFIX/bin/tronbrowser" "$PREFIX/share/applications/tronbrowser.desktop"
Expand Down Expand Up @@ -586,7 +605,83 @@ DESKTOP
esac
}

# Clear the profile's regenerable caches once they get big enough to hurt.
#
# Chromium's disk caches have no ceiling that matters here. Service-worker
# CacheStorage in particular is quota-managed per origin, so a handful of heavy
# sites can carry a profile into the gigabytes on their own. Past roughly a
# gigabyte the cost stops being disk and starts being latency: the CacheStorage
# index is consulted on navigation, and once it is bloated, scrolling and tab
# switching go with it. The failure is gradual and then sudden, which makes it
# read as "the browser broke today" — a profile that had grown to 3.9G, with
# 1.4G of CacheStorage, is what prompted this.
#
# None of these three directories holds bookmarks, passwords, history or
# cookies, so clearing them costs a re-download and nothing else. Upgrade is the
# right moment: it is the one command every user already runs, and the browser
# is usually closed for it.
#
# `$1` = "force" to clear regardless of size (that is `tron clean`).
# TRONBROWSER_CACHE_LIMIT_MB=0 disables the automatic pass entirely.
PROFILE_CACHES="Cache
Code Cache
Service Worker"

dir_mb() {
[ -d "$1" ] || { echo 0; return 0; }
_mb="$(du -sm "$1" 2>/dev/null | awk 'NR==1{print $1}')"
echo "${_mb:-0}"
}

prune_profile_caches() {
_force="${1:-}"
_limit="${TRONBROWSER_CACHE_LIMIT_MB:-1024}"
if [ "$_force" != "force" ] && [ "$_limit" = "0" ]; then
return 0
fi

for _data in "${TRONBROWSER_DATA:-$HOME/.tronbrowser}" "$HOME/TronBrowser"; do
[ -d "$_data/Default" ] || continue

# Never unlink these under a live browser. The profile is memory-mapped, and
# pulling it out from underneath Chromium gives you a corrupt profile rather
# than a clean one.
if command -v pgrep >/dev/null 2>&1 && pgrep -f "user-data-dir=$_data" >/dev/null 2>&1; then
warn "TronBrowser is running — leaving $_data alone. Quit it, then run 'tron clean'."
continue
fi

_total=0
_old_ifs="$IFS"; IFS="
"
for _c in $PROFILE_CACHES; do
_total=$((_total + $(dir_mb "$_data/Default/$_c")))
done
IFS="$_old_ifs"

if [ "$_force" != "force" ] && [ "$_total" -lt "$_limit" ]; then
continue
fi
if [ "$_total" -eq 0 ]; then
continue
fi

info "Clearing ${_total}MB of browser cache from $_data (bookmarks, passwords, history and logins are untouched)."
_old_ifs="$IFS"; IFS="
"
for _c in $PROFILE_CACHES; do
rm -rf "$_data/Default/$_c"
done
IFS="$_old_ifs"
say "Freed ~${_total}MB. Cached assets re-download on demand; push notifications need re-granting."
done
}

do_upgrade() {
# Before anything else, so a running browser is reported while the user is
# still watching, and so the check happens even when already up to date.
prune_profile_caches

if [ ! -f "$VERSION_FILE" ]; then
warn "TronBrowser not installed; installing fresh."
do_install
Expand Down Expand Up @@ -628,22 +723,33 @@ Usage: curl -fsSL $INSTALL_URL | sh [-s -- <command>]
Commands:
install Download and install the latest TronBrowser (default)
upgrade Update an existing install to the latest release
clean Clear the profile's browser caches (keeps bookmarks/logins).
'clean --if-large' only acts past TRONBROWSER_CACHE_LIMIT_MB
remove Uninstall TronBrowser (keeps your profile data)
version Print the installed version
help Show this help

After install, prefer the 'tron' CLI: tron upgrade | tron remove | tron version
After install, prefer the 'tron' CLI: tron upgrade | tron clean | tron remove

Env:
TRONBROWSER_PREFIX install prefix (default: \$HOME/.local)
TRONBROWSER_REPO GitHub repo (default: $REPO)
TRONBROWSER_PREFIX install prefix (default: \$HOME/.local)
TRONBROWSER_REPO GitHub repo (default: $REPO)
TRONBROWSER_CACHE_LIMIT_MB clear profile caches on upgrade once they exceed
this (default: 1024; 0 disables)
EOF
}

cmd="${1:-install}"
case "$cmd" in
install) do_install ;;
upgrade|update) do_upgrade ;;
clean)
# Bare `clean` always clears; --if-large respects TRONBROWSER_CACHE_LIMIT_MB
# and is the pass `tron upgrade` runs for you.
case "${2:-}" in
--if-large) prune_profile_caches ;;
*) prune_profile_caches force ;;
esac ;;
remove|uninstall) do_remove ;;
ensure-tor) ensure_tor ;;
version|--version|-v) do_version ;;
Expand Down
116 changes: 116 additions & 0 deletions apps/web/test/install-clean.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// install.sh's cache cleanup deletes directories out of a user's real profile,
// so it gets tested against a fake one. The rule it has to hold to: regenerable
// caches go, everything the user would miss stays.

import { spawnSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, writeFileSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterAll, describe, expect, it } from 'vitest';

const HERE = dirname(fileURLToPath(import.meta.url));
const INSTALL_SH = join(HERE, '..', 'public', 'install.sh');

const CACHES = ['Cache', 'Code Cache', 'Service Worker'];
const KEEP = ['Bookmarks', 'Cookies', 'History', 'Login Data'];

const roots: string[] = [];

/** A profile whose three cache directories hold roughly `mb` megabytes each. */
function profile(mb: number): { home: string; data: string } {
const home = mkdtempSync(join(tmpdir(), 'tron-clean-'));
roots.push(home);
const data = join(home, '.tronbrowser');
const def = join(data, 'Default');

for (const c of CACHES) {
mkdirSync(join(def, c), { recursive: true });
if (mb > 0) writeFileSync(join(def, c, 'blob'), Buffer.alloc(mb * 1024 * 1024, 1));
}
// The things a user would be upset to lose.
for (const f of KEEP) writeFileSync(join(def, f), 'precious');
mkdirSync(join(def, 'IndexedDB'), { recursive: true });
writeFileSync(join(def, 'IndexedDB', 'data'), 'precious');

return { home, data };
}

function clean(home: string, args: string[], env: Record<string, string> = {}) {
const result = spawnSync('sh', [INSTALL_SH, 'clean', ...args], {
encoding: 'utf8',
env: { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: home, ...env },
});
if (result.status !== 0) {
throw new Error(`install.sh clean exited ${result.status}\n${result.stderr}\n${result.stdout}`);
}
return `${result.stdout}${result.stderr}`;
}

const cachesExist = (data: string) =>
CACHES.map((c) => existsSync(join(data, 'Default', c)));

afterAll(() => {
for (const r of roots) rmSync(r, { recursive: true, force: true });
});

describe('install.sh clean', () => {
it('clears the regenerable caches', () => {
const { home, data } = profile(2);
const out = clean(home, []);
expect(cachesExist(data)).toEqual([false, false, false]);
expect(out).toMatch(/Freed ~\d+MB/);
});

it('keeps everything the user would miss', () => {
const { home, data } = profile(2);
clean(home, []);
for (const f of [...KEEP, 'IndexedDB']) {
expect(existsSync(join(data, 'Default', f)), `${f} should survive`).toBe(true);
}
});

it('leaves a small profile alone with --if-large', () => {
// The automatic pass on upgrade. Clearing a healthy profile every update
// would just cost everyone a re-download for nothing.
const { home, data } = profile(1);
clean(home, ['--if-large']);
expect(cachesExist(data)).toEqual([true, true, true]);
});

it('clears past the limit with --if-large', () => {
const { home, data } = profile(2);
clean(home, ['--if-large'], { TRONBROWSER_CACHE_LIMIT_MB: '4' });
expect(cachesExist(data)).toEqual([false, false, false]);
});

it('honors TRONBROWSER_CACHE_LIMIT_MB=0 as "never automatically"', () => {
const { home, data } = profile(2);
clean(home, ['--if-large'], { TRONBROWSER_CACHE_LIMIT_MB: '0' });
expect(cachesExist(data)).toEqual([true, true, true]);
});

it('still clears on an explicit clean when the automatic pass is disabled', () => {
const { home, data } = profile(2);
clean(home, [], { TRONBROWSER_CACHE_LIMIT_MB: '0' });
expect(cachesExist(data)).toEqual([false, false, false]);
});

it('does nothing when there is no profile', () => {
const home = mkdtempSync(join(tmpdir(), 'tron-clean-empty-'));
roots.push(home);
expect(() => clean(home, [])).not.toThrow();
});

it('respects TRONBROWSER_DATA', () => {
const { home } = profile(2);
const alt = join(home, 'elsewhere');
mkdirSync(join(alt, 'Default', 'Cache'), { recursive: true });
writeFileSync(join(alt, 'Default', 'Cache', 'blob'), Buffer.alloc(2 * 1024 * 1024, 1));

clean(home, [], { TRONBROWSER_DATA: alt });
expect(existsSync(join(alt, 'Default', 'Cache'))).toBe(false);
// The default location wasn't touched, because it wasn't the one named.
expect(existsSync(join(home, '.tronbrowser', 'Default', 'Cache'))).toBe(true);
});
});
Loading