Skip to content

Commit ae23c6a

Browse files
mehmetnadirclaude
andcommitted
fix(stability): drop --disable-gpu-compositing + Brave feature kill list + health probe
Brave crashed deterministically at ~7min uptime on Apple Silicon (SIGTRAP / EXC_BREAKPOINT in ThreadPoolForegroundWorker, ChromeMain stack across 9 dumps from 2026-04-20 23:31 to 2026-04-21 09:19, all uptime 400-420s). Two-part fix: 1) Removed --disable-gpu-compositing — conflicts with Metal accel on M-series. Letting Brave use its default compositor matches what every real-world site expects. 2) Added --disable-features=BraveRewards,BraveAds,BraveSync, BraveNewsToday,BraveVPN,BraveWalletBubble,SpeedReader,Tor, IPFSCompanion,Translate,OptimizationGuideModelDownloading, InterestFeedContentSuggestions,CalculateNativeWinOcclusion — catches background workers that single-purpose --disable-brave-* flags don't fully kill (likely the assertion-failing tasks). Also fixes cmd_extensions: previously early-returned on missing Default/Extensions/, hiding any dev-mode unpacked extensions registered by `ext-install`. Now lists both sources independently with manifest name + version resolution. New `cdpilot health` command — prints JSON with alive, port, project_id, tabs, browser version, today's Brave crash count from ~/Library/Logs/DiagnosticReports, stealth state, and uptime warning. Exit 0/2 for shell watchdog loops. Bumped to 0.4.1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 912d3ca commit ae23c6a

3 files changed

Lines changed: 128 additions & 41 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cdpilot",
3-
"version": "0.4.0",
3+
"version": "0.4.1",
44
"description": "Zero-dependency browser automation from your terminal. One command, full control.",
55
"bin": {
66
"cdpilot": "./bin/cdpilot.js",

src/cdpilot.py

Lines changed: 125 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
CDPILOT_PROFILE Isolated browser profile directory
1515
"""
1616

17-
__version__ = "0.4.0"
17+
__version__ = "0.4.1"
1818

1919
import asyncio
2020
import json
@@ -1503,6 +1503,15 @@ def cmd_launch():
15031503
proj_label = f' [{PROJECT_ID}]' if PROJECT_ID else ''
15041504
print(f'Launching browser (isolated session, port {CDP_PORT}){proj_label}...')
15051505

1506+
# ─── Stability fixes (2026-04-21) ───
1507+
# Brave on Apple Silicon crashed with SIGTRAP/EXC_BREAKPOINT deterministically
1508+
# at ~7 min uptime on ThreadPoolForegroundWorker. Two changes:
1509+
# 1) `--disable-gpu-compositing` removed — conflicts with Metal accel on
1510+
# M-series. Browser falls back to GPU-accel rendering which is the
1511+
# Chromium default and matches what websites expect.
1512+
# 2) `--disable-features=...` added — catches Brave-specific background
1513+
# tasks that individual `--disable-brave-*` flags don't fully kill
1514+
# (Rewards ad scanner, Sync heartbeat, News fetcher).
15061515
chrome_args = [
15071516
CHROME_BIN,
15081517
f'--remote-debugging-port={CDP_PORT}',
@@ -1521,6 +1530,14 @@ def cmd_launch():
15211530
'--disable-tor',
15221531
'--disable-ipfs',
15231532
'--disable-brave-extension',
1533+
# Feature-level disables — catch background workers the
1534+
# single-purpose flags above miss. Comma-joined string = one arg.
1535+
'--disable-features=' + ','.join([
1536+
'BraveRewards', 'BraveAds', 'BraveSync', 'BraveNewsToday',
1537+
'BraveVPN', 'BraveWalletBubble', 'SpeedReader', 'Tor',
1538+
'IPFSCompanion', 'Translate', 'OptimizationGuideModelDownloading',
1539+
'InterestFeedContentSuggestions', 'CalculateNativeWinOcclusion',
1540+
]),
15241541
# ─── Chromium performance flags ───
15251542
'--disable-background-networking',
15261543
'--disable-background-timer-throttling',
@@ -1542,7 +1559,9 @@ def cmd_launch():
15421559
'--safebrowsing-disable-auto-update',
15431560
'--password-store=basic',
15441561
# ─── GPU / rendering ───
1545-
'--disable-gpu-compositing',
1562+
# --disable-gpu-compositing REMOVED: caused EXC_BREAKPOINT on Apple
1563+
# Silicon Brave 147+ after ~7min. Let the browser use its default
1564+
# (Metal-accelerated) compositor.
15461565
'--disable-smooth-scrolling',
15471566
'--new-window', 'about:blank',
15481567
]
@@ -2225,52 +2244,71 @@ async def _close():
22252244

22262245

22272246
def cmd_extensions():
2228-
"""List installed extensions."""
2229-
ext_dir = os.path.join(PROFILE_DIR, "Default", "Extensions")
2230-
if not os.path.isdir(ext_dir):
2231-
print("No extensions installed.")
2232-
return
2247+
"""List installed extensions (packed via Chrome Web Store + dev mode unpacked).
22332248
2234-
prefs_path = os.path.join(PROFILE_DIR, "Default", "Preferences")
2249+
Two sources are checked independently — neither absence should hide the other.
2250+
Previously an early-return on missing Default/Extensions/ silently swallowed
2251+
dev-mode entries written by `ext-install`.
2252+
"""
2253+
# 1) Packed (CRX) extensions live under Default/Extensions and are described
2254+
# in Default/Preferences. Only present after the user installs from the
2255+
# Chrome Web Store; ext-install does NOT write here.
2256+
ext_dir = os.path.join(PROFILE_DIR, "Default", "Extensions")
2257+
packed_ids = []
22352258
ext_names = {}
2236-
if os.path.exists(prefs_path):
2237-
try:
2238-
with open(prefs_path) as f:
2239-
prefs = json.load(f)
2240-
settings = prefs.get("extensions", {}).get("settings", {})
2241-
for ext_id, info in settings.items():
2242-
manifest = info.get("manifest", {})
2243-
ext_names[ext_id] = {
2244-
"name": manifest.get("name", ext_id),
2245-
"version": manifest.get("version", "?"),
2246-
"enabled": info.get("state", 1) == 1,
2247-
"path": info.get("path", ""),
2248-
}
2249-
except Exception:
2250-
pass
2259+
if os.path.isdir(ext_dir):
2260+
prefs_path = os.path.join(PROFILE_DIR, "Default", "Preferences")
2261+
if os.path.exists(prefs_path):
2262+
try:
2263+
with open(prefs_path) as f:
2264+
prefs = json.load(f)
2265+
settings = prefs.get("extensions", {}).get("settings", {})
2266+
for ext_id, info in settings.items():
2267+
manifest = info.get("manifest", {})
2268+
ext_names[ext_id] = {
2269+
"name": manifest.get("name", ext_id),
2270+
"version": manifest.get("version", "?"),
2271+
"enabled": info.get("state", 1) == 1,
2272+
}
2273+
except Exception:
2274+
pass
2275+
packed_ids = [d for d in os.listdir(ext_dir) if not d.startswith(".")]
2276+
2277+
# 2) Dev-mode (unpacked) extensions registered by `ext-install` —
2278+
# loaded into the browser via --load-extension on each launch.
2279+
dev_exts = get_dev_extensions()
22512280

2252-
ext_ids = [d for d in os.listdir(ext_dir) if not d.startswith(".")]
2253-
if not ext_ids:
2281+
if not packed_ids and not dev_exts:
22542282
print("No extensions installed.")
22552283
return
22562284

2257-
for ext_id in sorted(ext_ids):
2258-
info = ext_names.get(ext_id, {})
2259-
name = info.get("name", ext_id)
2260-
version = info.get("version", "?")
2261-
enabled = info.get("enabled", True)
2262-
status = "✅" if enabled else "⏸️"
2263-
print(f" {status} {name} (v{version})")
2264-
print(f" ID: {ext_id}")
2265-
2266-
print(f"\n{len(ext_ids)} extensions")
2285+
if packed_ids:
2286+
for ext_id in sorted(packed_ids):
2287+
info = ext_names.get(ext_id, {})
2288+
name = info.get("name", ext_id)
2289+
version = info.get("version", "?")
2290+
enabled = info.get("enabled", True)
2291+
status = "✅" if enabled else "⏸️"
2292+
print(f" {status} {name} (v{version})")
2293+
print(f" ID: {ext_id}")
2294+
print(f"\n{len(packed_ids)} packed extension{'s' if len(packed_ids) != 1 else ''}")
22672295

2268-
dev_exts = get_dev_extensions()
22692296
if dev_exts:
2270-
print(f'\nDev Mode Extensions ({len(dev_exts)}):')
2297+
if packed_ids:
2298+
print()
2299+
print(f"Dev Mode Extensions ({len(dev_exts)}):")
22712300
for i, path in enumerate(dev_exts):
22722301
exists = '✅' if os.path.isdir(path) else '❌ (directory not found)'
2273-
print(f' {exists} [{i}] {path}')
2302+
# Try to read manifest for friendlier output
2303+
label = os.path.basename(path.rstrip('/'))
2304+
try:
2305+
with open(os.path.join(path, 'manifest.json')) as f:
2306+
mf = json.load(f)
2307+
label = f"{mf.get('name', label)} (v{mf.get('version', '?')})"
2308+
except Exception:
2309+
pass
2310+
print(f" {exists} [{i}] {label}")
2311+
print(f" {path}")
22742312

22752313

22762314
def cmd_ext_install(source):
@@ -2589,6 +2627,54 @@ def cmd_headless(state=None):
25892627
print('Restart browser (stop → launch).')
25902628

25912629

2630+
def cmd_health():
2631+
"""Print JSON health summary of the cdpilot browser session.
2632+
2633+
Output keys:
2634+
alive — bool, CDP /json/version reachable
2635+
port — int, current CDP port
2636+
project_id — str|null, project identifier (multi-instance)
2637+
tabs — int, count of page targets (when alive)
2638+
browser — str|null, version string from /json/version
2639+
crashes_today — int, Brave crash dump count from macOS today
2640+
stealth — bool, current stealth config
2641+
uptime_warning — str|null, hint when browser is alive but very old
2642+
2643+
Exit codes: 0 = alive, 2 = down. Designed for shell watchdog loops:
2644+
`until cdpilot health >/dev/null; do cdpilot launch; sleep 2; done`
2645+
"""
2646+
import datetime as _dt
2647+
import glob as _glob
2648+
2649+
info = {
2650+
'alive': False,
2651+
'port': CDP_PORT,
2652+
'project_id': PROJECT_ID,
2653+
'tabs': 0,
2654+
'browser': None,
2655+
'crashes_today': 0,
2656+
'stealth': get_stealth_config(),
2657+
'uptime_warning': None,
2658+
}
2659+
ver = cdp_get('/json/version')
2660+
if ver:
2661+
info['alive'] = True
2662+
info['browser'] = ver.get('Browser') or ver.get('browser') or ''
2663+
targets = cdp_get('/json') or []
2664+
info['tabs'] = sum(1 for t in targets if t.get('type') == 'page')
2665+
2666+
# Today's crash count from macOS DiagnosticReports (Brave only).
2667+
if platform.system() == 'Darwin':
2668+
today = _dt.date.today().strftime('%Y-%m-%d')
2669+
pattern = os.path.expanduser(f'~/Library/Logs/DiagnosticReports/Brave Browser-{today}-*.ips')
2670+
info['crashes_today'] = len(_glob.glob(pattern))
2671+
if info['crashes_today'] >= 3:
2672+
info['uptime_warning'] = f"{info['crashes_today']} Brave crashes today — consider `cdpilot stop` then relaunch"
2673+
2674+
print(json.dumps(info, ensure_ascii=False))
2675+
sys.exit(0 if info['alive'] else 2)
2676+
2677+
25922678
def cmd_stealth(state=None):
25932679
"""Toggle stealth fingerprint patches.
25942680
@@ -4756,6 +4842,7 @@ def run(self):
47564842
'proxy': lambda: cmd_proxy(args[0] if args else None),
47574843
'headless': lambda: cmd_headless(args[0] if args else None),
47584844
'stealth': lambda: cmd_stealth(args[0] if args else None),
4845+
'health': cmd_health,
47594846
'session': cmd_session,
47604847
'sessions': cmd_sessions,
47614848
'session-close': lambda: cmd_session_close(args[0] if args else None),

test/test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ console.log('\n cdpilot tests\n');
4040

4141
test('--version prints version', () => {
4242
const out = run('--version');
43-
assert(out.includes('0.4.0'), 'Should print version');
43+
assert(out.includes('0.4.1'), 'Should print version');
4444
});
4545

4646
test('-v prints version', () => {
4747
const out = run('-v');
48-
assert(out.includes('0.4.0'), 'Should print version');
48+
assert(out.includes('0.4.1'), 'Should print version');
4949
});
5050

5151
test('help shows usage', () => {

0 commit comments

Comments
 (0)