1414 CDPILOT_PROFILE Isolated browser profile directory
1515"""
1616
17- __version__ = "0.4.0 "
17+ __version__ = "0.4.1 "
1818
1919import asyncio
2020import 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
22272246def 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'\n Dev 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
22762314def 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+
25922678def 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 ),
0 commit comments