cdpilot 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cdpilot.js +4 -0
- package/package.json +1 -1
- package/src/cdpilot.py +289 -70
package/bin/cdpilot.js
CHANGED
|
@@ -329,6 +329,10 @@ function showHelp() {
|
|
|
329
329
|
captcha-check Detect CAPTCHA on active page (JSON output)
|
|
330
330
|
captcha-wait [s] Pause until user solves CAPTCHA (default 300s)
|
|
331
331
|
|
|
332
|
+
RELIABILITY
|
|
333
|
+
browser [name] Show or set preferred browser (chrome|brave|chromium|edge|vivaldi|auto)
|
|
334
|
+
health JSON status: alive, port, tabs, browser, today's crashes
|
|
335
|
+
|
|
332
336
|
PROJECTS
|
|
333
337
|
projects List all project browser instances
|
|
334
338
|
project-stop <id> Stop a specific project's browser
|
package/package.json
CHANGED
package/src/cdpilot.py
CHANGED
|
@@ -14,7 +14,7 @@ Environment:
|
|
|
14
14
|
CDPILOT_PROFILE Isolated browser profile directory
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
__version__ = "0.4.
|
|
17
|
+
__version__ = "0.4.2"
|
|
18
18
|
|
|
19
19
|
import asyncio
|
|
20
20
|
import json
|
|
@@ -1227,44 +1227,110 @@ async def navigate_collect(ws_url, url, network=False, console=False, glow=True)
|
|
|
1227
1227
|
|
|
1228
1228
|
# ─── Helper Functions ───
|
|
1229
1229
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1230
|
+
# ─── Browser preference & detection ───────────────────────────────────────────
|
|
1231
|
+
# Map of canonical browser names to platform-specific binary candidates.
|
|
1232
|
+
# Order matters within each list: first existing path wins.
|
|
1233
|
+
BROWSER_BINARIES = {
|
|
1234
|
+
'brave': {'Darwin': ["/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"],
|
|
1235
|
+
'Linux': ["/usr/bin/brave-browser", "/usr/bin/brave"],
|
|
1236
|
+
'Windows': [os.path.expandvars(r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe")]},
|
|
1237
|
+
'chrome': {'Darwin': ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
|
|
1238
|
+
'Linux': ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"],
|
|
1239
|
+
'Windows': [os.path.expandvars(r"C:\Program Files\Google\Chrome\Application\chrome.exe"),
|
|
1240
|
+
os.path.expandvars(r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe")]},
|
|
1241
|
+
'vivaldi': {'Darwin': ["/Applications/Vivaldi.app/Contents/MacOS/Vivaldi"],
|
|
1242
|
+
'Linux': ["/usr/bin/vivaldi", "/usr/bin/vivaldi-stable"],
|
|
1243
|
+
'Windows': [os.path.expandvars(r"C:\Program Files\Vivaldi\Application\vivaldi.exe")]},
|
|
1244
|
+
'edge': {'Darwin': ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
|
|
1245
|
+
'Linux': ["/usr/bin/microsoft-edge"],
|
|
1246
|
+
'Windows': [os.path.expandvars(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe")]},
|
|
1247
|
+
'chromium': {'Darwin': ["/Applications/Chromium.app/Contents/MacOS/Chromium"],
|
|
1248
|
+
'Linux': ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium"],
|
|
1249
|
+
'Windows': [os.path.expandvars(r"C:\Program Files\Chromium\Application\chromium.exe")]},
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
BROWSER_CONFIG_FILE = os.path.join(CDPILOT_HOME, 'browser.json')
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _macos_major():
|
|
1256
|
+
"""Return macOS major version (e.g. 26) or None on other platforms."""
|
|
1257
|
+
if platform.system() != 'Darwin':
|
|
1258
|
+
return None
|
|
1259
|
+
try:
|
|
1260
|
+
return int(platform.mac_ver()[0].split('.')[0])
|
|
1261
|
+
except (ValueError, IndexError):
|
|
1260
1262
|
return None
|
|
1261
1263
|
|
|
1262
|
-
|
|
1264
|
+
|
|
1265
|
+
def _resolve_browser_name(name):
|
|
1266
|
+
"""Resolve a browser name to its binary path. Returns None if not installed."""
|
|
1267
|
+
name = (name or '').lower().strip()
|
|
1268
|
+
if name not in BROWSER_BINARIES:
|
|
1269
|
+
return None
|
|
1270
|
+
candidates = BROWSER_BINARIES[name].get(platform.system(), [])
|
|
1271
|
+
for path in candidates:
|
|
1263
1272
|
if os.path.exists(path):
|
|
1264
1273
|
return path
|
|
1265
1274
|
return None
|
|
1266
1275
|
|
|
1267
1276
|
|
|
1277
|
+
def get_browser_preference():
|
|
1278
|
+
"""Read user's persisted browser preference. Returns name or 'auto'."""
|
|
1279
|
+
if os.path.exists(BROWSER_CONFIG_FILE):
|
|
1280
|
+
try:
|
|
1281
|
+
with open(BROWSER_CONFIG_FILE) as f:
|
|
1282
|
+
return (json.load(f).get('browser') or 'auto').lower()
|
|
1283
|
+
except (OSError, ValueError):
|
|
1284
|
+
pass
|
|
1285
|
+
return 'auto'
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def _auto_browser_priority():
|
|
1289
|
+
"""Default detection order. macOS 26 (Tahoe) demotes Brave because the
|
|
1290
|
+
current Brave 1.89 build crashes deterministically at ~7min uptime
|
|
1291
|
+
(SIGTRAP in ThreadPoolForegroundWorker, observed across 9+ dumps).
|
|
1292
|
+
Vivaldi takes priority because it's Chromium-based AND honors
|
|
1293
|
+
--load-extension (Chrome silently drops unpacked extensions in 147+).
|
|
1294
|
+
"""
|
|
1295
|
+
if _macos_major() and _macos_major() >= 26:
|
|
1296
|
+
return ['vivaldi', 'brave', 'chrome', 'chromium', 'edge']
|
|
1297
|
+
return ['brave', 'chrome', 'vivaldi', 'chromium', 'edge']
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _find_browser():
|
|
1301
|
+
"""Locate the browser binary. Priority:
|
|
1302
|
+
1. CHROME_BIN env var (full path, backward-compatible)
|
|
1303
|
+
2. ~/.cdpilot/browser.json preference (set via `cdpilot browser <name>`)
|
|
1304
|
+
3. Auto-detection per _auto_browser_priority()
|
|
1305
|
+
"""
|
|
1306
|
+
# 1) PATH lookup for common command names (Linux/Brew installs)
|
|
1307
|
+
for b in ["brave-browser", "vivaldi", "google-chrome", "chromium-browser", "chromium"]:
|
|
1308
|
+
found = shutil.which(b)
|
|
1309
|
+
if found:
|
|
1310
|
+
# Only use PATH match as fallback — preference still wins below.
|
|
1311
|
+
path_match = found
|
|
1312
|
+
break
|
|
1313
|
+
else:
|
|
1314
|
+
path_match = None
|
|
1315
|
+
|
|
1316
|
+
# 2) User preference from config (overrides auto)
|
|
1317
|
+
pref = get_browser_preference()
|
|
1318
|
+
if pref and pref != 'auto':
|
|
1319
|
+
resolved = _resolve_browser_name(pref)
|
|
1320
|
+
if resolved:
|
|
1321
|
+
return resolved
|
|
1322
|
+
# Configured browser not installed — fall through to auto with a warning.
|
|
1323
|
+
sys.stderr.write(f"⚠️ Configured browser '{pref}' not found, falling back to auto-detect.\n")
|
|
1324
|
+
|
|
1325
|
+
# 3) Auto-detection in priority order
|
|
1326
|
+
for name in _auto_browser_priority():
|
|
1327
|
+
resolved = _resolve_browser_name(name)
|
|
1328
|
+
if resolved:
|
|
1329
|
+
return resolved
|
|
1330
|
+
|
|
1331
|
+
return path_match # Last resort
|
|
1332
|
+
|
|
1333
|
+
|
|
1268
1334
|
def _is_port_in_use(port):
|
|
1269
1335
|
"""Check if a port is actively in use."""
|
|
1270
1336
|
return not _is_port_free(port)
|
|
@@ -1503,10 +1569,37 @@ def cmd_launch():
|
|
|
1503
1569
|
proj_label = f' [{PROJECT_ID}]' if PROJECT_ID else ''
|
|
1504
1570
|
print(f'Launching browser (isolated session, port {CDP_PORT}){proj_label}...')
|
|
1505
1571
|
|
|
1572
|
+
# ─── Stability fixes (2026-04-21) ───
|
|
1573
|
+
# Brave on Apple Silicon crashed with SIGTRAP/EXC_BREAKPOINT deterministically
|
|
1574
|
+
# at ~7 min uptime on ThreadPoolForegroundWorker. Two changes:
|
|
1575
|
+
# 1) `--disable-gpu-compositing` removed — conflicts with Metal accel on
|
|
1576
|
+
# M-series. Browser falls back to GPU-accel rendering which is the
|
|
1577
|
+
# Chromium default and matches what websites expect.
|
|
1578
|
+
# 2) `--disable-features=...` added — catches Brave-specific background
|
|
1579
|
+
# tasks that individual `--disable-brave-*` flags don't fully kill
|
|
1580
|
+
# (Rewards ad scanner, Sync heartbeat, News fetcher).
|
|
1581
|
+
# Per-browser profile isolation. Brave-written profiles include keys that
|
|
1582
|
+
# confuse Vivaldi/Chrome (search_engines.json missing, prefs schema diffs),
|
|
1583
|
+
# so each browser gets its own subdir. Brave keeps the original path for
|
|
1584
|
+
# backward compatibility — existing users don't lose state on upgrade.
|
|
1585
|
+
browser_basename = os.path.basename(CHROME_BIN).lower()
|
|
1586
|
+
if 'brave' in browser_basename:
|
|
1587
|
+
profile_dir = PROFILE_DIR
|
|
1588
|
+
elif 'vivaldi' in browser_basename:
|
|
1589
|
+
profile_dir = PROFILE_DIR + '-vivaldi'
|
|
1590
|
+
elif 'edge' in browser_basename or 'msedge' in browser_basename:
|
|
1591
|
+
profile_dir = PROFILE_DIR + '-edge'
|
|
1592
|
+
elif 'chromium' in browser_basename:
|
|
1593
|
+
profile_dir = PROFILE_DIR + '-chromium'
|
|
1594
|
+
else:
|
|
1595
|
+
# Default: chrome and unknown chromium-based browsers
|
|
1596
|
+
profile_dir = PROFILE_DIR + '-chrome'
|
|
1597
|
+
os.makedirs(profile_dir, exist_ok=True)
|
|
1598
|
+
|
|
1506
1599
|
chrome_args = [
|
|
1507
1600
|
CHROME_BIN,
|
|
1508
1601
|
f'--remote-debugging-port={CDP_PORT}',
|
|
1509
|
-
f'--user-data-dir={
|
|
1602
|
+
f'--user-data-dir={profile_dir}',
|
|
1510
1603
|
'--remote-allow-origins=*',
|
|
1511
1604
|
'--disable-fre', '--no-default-browser-check', '--no-first-run',
|
|
1512
1605
|
# ─── Brave-specific features (harmless on other Chromium builds) ───
|
|
@@ -1521,6 +1614,14 @@ def cmd_launch():
|
|
|
1521
1614
|
'--disable-tor',
|
|
1522
1615
|
'--disable-ipfs',
|
|
1523
1616
|
'--disable-brave-extension',
|
|
1617
|
+
# Feature-level disables — catch background workers the
|
|
1618
|
+
# single-purpose flags above miss. Comma-joined string = one arg.
|
|
1619
|
+
'--disable-features=' + ','.join([
|
|
1620
|
+
'BraveRewards', 'BraveAds', 'BraveSync', 'BraveNewsToday',
|
|
1621
|
+
'BraveVPN', 'BraveWalletBubble', 'SpeedReader', 'Tor',
|
|
1622
|
+
'IPFSCompanion', 'Translate', 'OptimizationGuideModelDownloading',
|
|
1623
|
+
'InterestFeedContentSuggestions', 'CalculateNativeWinOcclusion',
|
|
1624
|
+
]),
|
|
1524
1625
|
# ─── Chromium performance flags ───
|
|
1525
1626
|
'--disable-background-networking',
|
|
1526
1627
|
'--disable-background-timer-throttling',
|
|
@@ -1542,7 +1643,9 @@ def cmd_launch():
|
|
|
1542
1643
|
'--safebrowsing-disable-auto-update',
|
|
1543
1644
|
'--password-store=basic',
|
|
1544
1645
|
# ─── GPU / rendering ───
|
|
1545
|
-
|
|
1646
|
+
# --disable-gpu-compositing REMOVED: caused EXC_BREAKPOINT on Apple
|
|
1647
|
+
# Silicon Brave 147+ after ~7min. Let the browser use its default
|
|
1648
|
+
# (Metal-accelerated) compositor.
|
|
1546
1649
|
'--disable-smooth-scrolling',
|
|
1547
1650
|
'--new-window', 'about:blank',
|
|
1548
1651
|
]
|
|
@@ -2225,52 +2328,71 @@ def cmd_session_close(session_id=None):
|
|
|
2225
2328
|
|
|
2226
2329
|
|
|
2227
2330
|
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
|
|
2331
|
+
"""List installed extensions (packed via Chrome Web Store + dev mode unpacked).
|
|
2233
2332
|
|
|
2234
|
-
|
|
2333
|
+
Two sources are checked independently — neither absence should hide the other.
|
|
2334
|
+
Previously an early-return on missing Default/Extensions/ silently swallowed
|
|
2335
|
+
dev-mode entries written by `ext-install`.
|
|
2336
|
+
"""
|
|
2337
|
+
# 1) Packed (CRX) extensions live under Default/Extensions and are described
|
|
2338
|
+
# in Default/Preferences. Only present after the user installs from the
|
|
2339
|
+
# Chrome Web Store; ext-install does NOT write here.
|
|
2340
|
+
ext_dir = os.path.join(PROFILE_DIR, "Default", "Extensions")
|
|
2341
|
+
packed_ids = []
|
|
2235
2342
|
ext_names = {}
|
|
2236
|
-
if os.path.
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2343
|
+
if os.path.isdir(ext_dir):
|
|
2344
|
+
prefs_path = os.path.join(PROFILE_DIR, "Default", "Preferences")
|
|
2345
|
+
if os.path.exists(prefs_path):
|
|
2346
|
+
try:
|
|
2347
|
+
with open(prefs_path) as f:
|
|
2348
|
+
prefs = json.load(f)
|
|
2349
|
+
settings = prefs.get("extensions", {}).get("settings", {})
|
|
2350
|
+
for ext_id, info in settings.items():
|
|
2351
|
+
manifest = info.get("manifest", {})
|
|
2352
|
+
ext_names[ext_id] = {
|
|
2353
|
+
"name": manifest.get("name", ext_id),
|
|
2354
|
+
"version": manifest.get("version", "?"),
|
|
2355
|
+
"enabled": info.get("state", 1) == 1,
|
|
2356
|
+
}
|
|
2357
|
+
except Exception:
|
|
2358
|
+
pass
|
|
2359
|
+
packed_ids = [d for d in os.listdir(ext_dir) if not d.startswith(".")]
|
|
2360
|
+
|
|
2361
|
+
# 2) Dev-mode (unpacked) extensions registered by `ext-install` —
|
|
2362
|
+
# loaded into the browser via --load-extension on each launch.
|
|
2363
|
+
dev_exts = get_dev_extensions()
|
|
2251
2364
|
|
|
2252
|
-
|
|
2253
|
-
if not ext_ids:
|
|
2365
|
+
if not packed_ids and not dev_exts:
|
|
2254
2366
|
print("No extensions installed.")
|
|
2255
2367
|
return
|
|
2256
2368
|
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2369
|
+
if packed_ids:
|
|
2370
|
+
for ext_id in sorted(packed_ids):
|
|
2371
|
+
info = ext_names.get(ext_id, {})
|
|
2372
|
+
name = info.get("name", ext_id)
|
|
2373
|
+
version = info.get("version", "?")
|
|
2374
|
+
enabled = info.get("enabled", True)
|
|
2375
|
+
status = "✅" if enabled else "⏸️"
|
|
2376
|
+
print(f" {status} {name} (v{version})")
|
|
2377
|
+
print(f" ID: {ext_id}")
|
|
2378
|
+
print(f"\n{len(packed_ids)} packed extension{'s' if len(packed_ids) != 1 else ''}")
|
|
2265
2379
|
|
|
2266
|
-
print(f"\n{len(ext_ids)} extensions")
|
|
2267
|
-
|
|
2268
|
-
dev_exts = get_dev_extensions()
|
|
2269
2380
|
if dev_exts:
|
|
2270
|
-
|
|
2381
|
+
if packed_ids:
|
|
2382
|
+
print()
|
|
2383
|
+
print(f"Dev Mode Extensions ({len(dev_exts)}):")
|
|
2271
2384
|
for i, path in enumerate(dev_exts):
|
|
2272
2385
|
exists = '✅' if os.path.isdir(path) else '❌ (directory not found)'
|
|
2273
|
-
|
|
2386
|
+
# Try to read manifest for friendlier output
|
|
2387
|
+
label = os.path.basename(path.rstrip('/'))
|
|
2388
|
+
try:
|
|
2389
|
+
with open(os.path.join(path, 'manifest.json')) as f:
|
|
2390
|
+
mf = json.load(f)
|
|
2391
|
+
label = f"{mf.get('name', label)} (v{mf.get('version', '?')})"
|
|
2392
|
+
except Exception:
|
|
2393
|
+
pass
|
|
2394
|
+
print(f" {exists} [{i}] {label}")
|
|
2395
|
+
print(f" {path}")
|
|
2274
2396
|
|
|
2275
2397
|
|
|
2276
2398
|
def cmd_ext_install(source):
|
|
@@ -2589,6 +2711,101 @@ def cmd_headless(state=None):
|
|
|
2589
2711
|
print('Restart browser (stop → launch).')
|
|
2590
2712
|
|
|
2591
2713
|
|
|
2714
|
+
def cmd_browser(name=None):
|
|
2715
|
+
"""Show or set the preferred browser (chrome|brave|chromium|edge|vivaldi|auto).
|
|
2716
|
+
|
|
2717
|
+
The choice is persisted in ~/.cdpilot/browser.json and applies to all
|
|
2718
|
+
projects. `auto` (default) picks per-platform priority — on macOS 26
|
|
2719
|
+
Vivaldi is preferred over Brave due to a known Brave 1.89/macOS 26
|
|
2720
|
+
crash. `CHROME_BIN` env var still overrides if set.
|
|
2721
|
+
|
|
2722
|
+
Usage:
|
|
2723
|
+
cdpilot browser # show current + which is detected
|
|
2724
|
+
cdpilot browser vivaldi # force Vivaldi
|
|
2725
|
+
cdpilot browser auto # restore platform default
|
|
2726
|
+
"""
|
|
2727
|
+
if name is None or name.lower() == 'status':
|
|
2728
|
+
pref = get_browser_preference()
|
|
2729
|
+
resolved = _resolve_browser_name(pref) if pref != 'auto' else None
|
|
2730
|
+
auto_resolved = _find_browser()
|
|
2731
|
+
installed = [n for n in BROWSER_BINARIES if _resolve_browser_name(n)]
|
|
2732
|
+
print(f"Preference: {pref}")
|
|
2733
|
+
if pref != 'auto':
|
|
2734
|
+
print(f" resolved: {resolved or '(not found — falling back to auto)'}")
|
|
2735
|
+
print(f"Auto-pick: {auto_resolved or '(none)'}")
|
|
2736
|
+
print(f"Installed: {', '.join(installed) or '(none detected)'}")
|
|
2737
|
+
if _macos_major() and _macos_major() >= 26:
|
|
2738
|
+
print(f"Note: macOS {_macos_major()} demotes Brave in auto-pick (Brave 1.89 crashes ~7min).")
|
|
2739
|
+
return
|
|
2740
|
+
|
|
2741
|
+
name = name.lower().strip()
|
|
2742
|
+
if name not in BROWSER_BINARIES and name != 'auto':
|
|
2743
|
+
valid = ', '.join(['auto'] + list(BROWSER_BINARIES.keys()))
|
|
2744
|
+
print(f"Invalid browser: {name}. Valid: {valid}", file=sys.stderr)
|
|
2745
|
+
sys.exit(1)
|
|
2746
|
+
|
|
2747
|
+
if name != 'auto' and not _resolve_browser_name(name):
|
|
2748
|
+
print(f"⚠️ Browser '{name}' not installed on this system.", file=sys.stderr)
|
|
2749
|
+
candidates = BROWSER_BINARIES[name].get(platform.system(), [])
|
|
2750
|
+
if candidates:
|
|
2751
|
+
print(f" Looked at: {candidates[0]}", file=sys.stderr)
|
|
2752
|
+
sys.exit(1)
|
|
2753
|
+
|
|
2754
|
+
os.makedirs(os.path.dirname(BROWSER_CONFIG_FILE), exist_ok=True)
|
|
2755
|
+
with open(BROWSER_CONFIG_FILE, 'w') as f:
|
|
2756
|
+
json.dump({'browser': name}, f)
|
|
2757
|
+
print(f"Browser preference: {name}")
|
|
2758
|
+
print('Restart the browser (`cdpilot stop` then any command) for the change to take effect.')
|
|
2759
|
+
|
|
2760
|
+
|
|
2761
|
+
def cmd_health():
|
|
2762
|
+
"""Print JSON health summary of the cdpilot browser session.
|
|
2763
|
+
|
|
2764
|
+
Output keys:
|
|
2765
|
+
alive — bool, CDP /json/version reachable
|
|
2766
|
+
port — int, current CDP port
|
|
2767
|
+
project_id — str|null, project identifier (multi-instance)
|
|
2768
|
+
tabs — int, count of page targets (when alive)
|
|
2769
|
+
browser — str|null, version string from /json/version
|
|
2770
|
+
crashes_today — int, Brave crash dump count from macOS today
|
|
2771
|
+
stealth — bool, current stealth config
|
|
2772
|
+
uptime_warning — str|null, hint when browser is alive but very old
|
|
2773
|
+
|
|
2774
|
+
Exit codes: 0 = alive, 2 = down. Designed for shell watchdog loops:
|
|
2775
|
+
`until cdpilot health >/dev/null; do cdpilot launch; sleep 2; done`
|
|
2776
|
+
"""
|
|
2777
|
+
import datetime as _dt
|
|
2778
|
+
import glob as _glob
|
|
2779
|
+
|
|
2780
|
+
info = {
|
|
2781
|
+
'alive': False,
|
|
2782
|
+
'port': CDP_PORT,
|
|
2783
|
+
'project_id': PROJECT_ID,
|
|
2784
|
+
'tabs': 0,
|
|
2785
|
+
'browser': None,
|
|
2786
|
+
'crashes_today': 0,
|
|
2787
|
+
'stealth': get_stealth_config(),
|
|
2788
|
+
'uptime_warning': None,
|
|
2789
|
+
}
|
|
2790
|
+
ver = cdp_get('/json/version')
|
|
2791
|
+
if ver:
|
|
2792
|
+
info['alive'] = True
|
|
2793
|
+
info['browser'] = ver.get('Browser') or ver.get('browser') or ''
|
|
2794
|
+
targets = cdp_get('/json') or []
|
|
2795
|
+
info['tabs'] = sum(1 for t in targets if t.get('type') == 'page')
|
|
2796
|
+
|
|
2797
|
+
# Today's crash count from macOS DiagnosticReports (Brave only).
|
|
2798
|
+
if platform.system() == 'Darwin':
|
|
2799
|
+
today = _dt.date.today().strftime('%Y-%m-%d')
|
|
2800
|
+
pattern = os.path.expanduser(f'~/Library/Logs/DiagnosticReports/Brave Browser-{today}-*.ips')
|
|
2801
|
+
info['crashes_today'] = len(_glob.glob(pattern))
|
|
2802
|
+
if info['crashes_today'] >= 3:
|
|
2803
|
+
info['uptime_warning'] = f"{info['crashes_today']} Brave crashes today — consider `cdpilot stop` then relaunch"
|
|
2804
|
+
|
|
2805
|
+
print(json.dumps(info, ensure_ascii=False))
|
|
2806
|
+
sys.exit(0 if info['alive'] else 2)
|
|
2807
|
+
|
|
2808
|
+
|
|
2592
2809
|
def cmd_stealth(state=None):
|
|
2593
2810
|
"""Toggle stealth fingerprint patches.
|
|
2594
2811
|
|
|
@@ -4756,6 +4973,8 @@ if __name__ == "__main__":
|
|
|
4756
4973
|
'proxy': lambda: cmd_proxy(args[0] if args else None),
|
|
4757
4974
|
'headless': lambda: cmd_headless(args[0] if args else None),
|
|
4758
4975
|
'stealth': lambda: cmd_stealth(args[0] if args else None),
|
|
4976
|
+
'browser': lambda: cmd_browser(args[0] if args else None),
|
|
4977
|
+
'health': cmd_health,
|
|
4759
4978
|
'session': cmd_session,
|
|
4760
4979
|
'sessions': cmd_sessions,
|
|
4761
4980
|
'session-close': lambda: cmd_session_close(args[0] if args else None),
|