Add ECW/HTTP planner scripts, browser crash recovery, and Planner error-overlay handling
- Add ecw_download.py and onedrive_planner_http.py for ECW and HTTP-based Planner access - Add build_exe.bat for packaging - Restart Chrome automatically on crash mid-run in planner_download.py - Detect and recover from Planner "something went wrong" error overlays - Improve board-load waiting, sort-descending menu matching, and screenshot timestamp rendering in chrome.py/onedrive_planner.py
This commit is contained in:
1 parent
c2dec96b1f
commit
291507c2f7
6 files changed
+2134
-49
No files matched your search
+48
-7
@@ -397,10 +397,33 @@ def _is_image_mostly_black(path: Path, *, threshold: int = 18) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _load_timestamp_font(size: int = 22):
|
||||
"""Return a PIL ImageFont at the requested size, falling back to the built-in bitmap font."""
|
||||
try:
|
||||
from PIL import ImageFont
|
||||
|
||||
candidates = [
|
||||
"C:/Windows/Fonts/segoeui.ttf",
|
||||
"C:/Windows/Fonts/arial.ttf",
|
||||
"C:/Windows/Fonts/calibri.ttf",
|
||||
"C:/Windows/Fonts/verdana.ttf",
|
||||
]
|
||||
for path in candidates:
|
||||
try:
|
||||
return ImageFont.truetype(path, size)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from PIL import ImageFont
|
||||
return ImageFont.load_default()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _capture_browser_with_timestamp_bar(driver: webdriver.Chrome, path: Path) -> bool:
|
||||
"""
|
||||
Fallback when desktop capture is black: maximized browser PNG plus a taskbar-style clock strip.
|
||||
"""
|
||||
"""Browser viewport PNG with a timestamp bar showing machine time."""
|
||||
try:
|
||||
import io
|
||||
|
||||
@@ -409,18 +432,36 @@ def _capture_browser_with_timestamp_bar(driver: webdriver.Chrome, path: Path) ->
|
||||
prepare_browser_for_screenshot(driver)
|
||||
png = driver.get_screenshot_as_png()
|
||||
browser = Image.open(io.BytesIO(png)).convert("RGB")
|
||||
bar_h = 48
|
||||
canvas = Image.new("RGB", (browser.width, browser.height + bar_h), (30, 30, 30))
|
||||
bar_h = 36
|
||||
canvas = Image.new("RGB", (browser.width, browser.height + bar_h), (24, 24, 24))
|
||||
canvas.paste(browser, (0, 0))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
stamp = datetime.now().strftime("%I:%M %p %m/%d/%Y").lstrip("0")
|
||||
draw.text((max(browser.width - 260, 8), browser.height + 14), stamp, fill=(255, 255, 255))
|
||||
stamp = datetime.now().strftime("%I:%M:%S %p %m/%d/%Y").lstrip("0")
|
||||
font = _load_timestamp_font(22)
|
||||
y = browser.height + (bar_h - 22) // 2
|
||||
if font:
|
||||
draw.text((12, y), stamp, fill=(220, 220, 220), font=font)
|
||||
else:
|
||||
draw.text((12, y), stamp, fill=(220, 220, 220))
|
||||
canvas.save(str(path), "PNG")
|
||||
return path.is_file() and path.stat().st_size > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def capture_browser_screenshot(driver: webdriver.Chrome, path: Path) -> bool:
|
||||
"""Capture only the Chromium browser viewport — no dependency on the host desktop."""
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_step("Browser screenshot", path=str(path))
|
||||
if _capture_browser_with_timestamp_bar(driver, path):
|
||||
log_step("Browser screenshot saved", path=str(path))
|
||||
return True
|
||||
driver.save_screenshot(str(path))
|
||||
log_step("Viewport screenshot saved", path=str(path))
|
||||
return path.is_file()
|
||||
|
||||
|
||||
def capture_fullscreen_screenshot(driver: webdriver.Chrome, path: Path) -> bool:
|
||||
"""
|
||||
Full-screen capture with system taskbar visible when an interactive desktop exists.
|
||||
|
||||
+185
-41
@@ -19,6 +19,7 @@ from pathlib import Path
|
||||
|
||||
import pyotp
|
||||
from chrome import DOWNLOADS_DIR as _CHROME_DOWNLOADS_DIR
|
||||
from chrome import capture_browser_screenshot
|
||||
from chrome import capture_fullscreen_screenshot
|
||||
from config import DEFAULT_ONEDRIVE_URL, DEFAULT_PLANNER_URL, PROJECT_ROOT, TODAY_ISO_DATE
|
||||
from debug_log import write_agent_session_debug as _agent_session_log
|
||||
@@ -367,11 +368,15 @@ def _on_planner_plan_board(driver: WebDriver) -> bool:
|
||||
|
||||
|
||||
def _plan_load_error_present(driver: WebDriver) -> bool:
|
||||
"""True when Planner shows 'Couldn't open your plan' (URL may still be /plan/)."""
|
||||
"""True when Planner shows any error overlay that blocks normal operation."""
|
||||
phrases = (
|
||||
"couldn't open your plan",
|
||||
"could not open your plan",
|
||||
"try again in a few minutes",
|
||||
"something went wrong",
|
||||
"something's not right",
|
||||
"we couldn't load",
|
||||
"please try again",
|
||||
)
|
||||
try:
|
||||
blob = (driver.find_element(By.TAG_NAME, "body").text or "").lower()
|
||||
@@ -380,6 +385,33 @@ def _plan_load_error_present(driver: WebDriver) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _recover_planner_error(driver: WebDriver) -> bool:
|
||||
"""Detect and dismiss a Planner error overlay ('Something went wrong', etc).
|
||||
|
||||
Tries clicking the 'Try again' button first; falls back to a full page refresh.
|
||||
Returns True if an error was detected (regardless of whether recovery succeeded).
|
||||
"""
|
||||
if not _plan_load_error_present(driver):
|
||||
return False
|
||||
log_step("Planner error overlay detected — attempting recovery")
|
||||
if _click_plan_try_again(driver):
|
||||
time.sleep(4)
|
||||
if not _plan_load_error_present(driver):
|
||||
log_step("Planner error overlay cleared via Try again")
|
||||
return True
|
||||
# Fallback: full page refresh
|
||||
try:
|
||||
log_step("Refreshing page to clear Planner error overlay")
|
||||
driver.refresh()
|
||||
time.sleep(6)
|
||||
driver.switch_to.default_content()
|
||||
_switch_to_planner_context(driver)
|
||||
log_step("Page refreshed — error overlay cleared")
|
||||
except Exception as exc:
|
||||
log_step("Refresh failed during error recovery", error=str(exc)[:120])
|
||||
return True
|
||||
|
||||
|
||||
def _plan_page_ready(driver: WebDriver) -> bool:
|
||||
"""Plan UI is usable — not the error page."""
|
||||
if _plan_load_error_present(driver):
|
||||
@@ -1157,6 +1189,7 @@ def open_my_plans_shared(driver: WebDriver) -> None:
|
||||
|
||||
This matches your current UI flow and avoids search-based plan opening.
|
||||
"""
|
||||
_recover_planner_error(driver)
|
||||
log_step("Navigate: My plans -> Shared")
|
||||
log_url(driver, "before")
|
||||
driver.switch_to.default_content()
|
||||
@@ -1374,7 +1407,26 @@ def _open_facility_from_shared_list(driver: WebDriver, facility_name: str) -> No
|
||||
_type_facility_filter(driver, term)
|
||||
if _filter_shows_no_results(driver):
|
||||
log_step("Filter returned no results", query=term[:60], facility=facility_name)
|
||||
continue
|
||||
# A Planner error overlay may be blocking the search UI — try to recover
|
||||
if _recover_planner_error(driver):
|
||||
log_step("Recovered from error overlay during filter search", facility=facility_name)
|
||||
# Re-open My plans -> Shared after the page refreshed
|
||||
_click_any(driver, _MY_PLANS_NAV_LOCATORS, timeout=WAIT_LONG)
|
||||
time.sleep(2)
|
||||
shared_locators = [
|
||||
(By.XPATH, "//*[self::button or self::div or self::span][normalize-space()='Shared']"),
|
||||
(By.XPATH, "//*[@role='tab' and normalize-space()='Shared']"),
|
||||
]
|
||||
try:
|
||||
_click_any(driver, shared_locators, timeout=WAIT_LONG)
|
||||
time.sleep(2)
|
||||
except TimeoutException:
|
||||
pass
|
||||
_type_facility_filter(driver, term)
|
||||
if _filter_shows_no_results(driver):
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
rows = _shared_list_data_rows(driver)
|
||||
log_step(
|
||||
"Filter results",
|
||||
@@ -1611,6 +1663,53 @@ def _ensure_board_view(driver: WebDriver) -> None:
|
||||
log_step("Board tab not found (continuing on current view)")
|
||||
|
||||
|
||||
def _wait_for_board_loaded(driver: WebDriver, timeout: int = 20) -> None:
|
||||
"""Switch to Board tab and wait until board content is visible before screenshotting."""
|
||||
board_locators = [
|
||||
(By.XPATH, "//*[@role='tab' and normalize-space()='Board']"),
|
||||
(By.XPATH, "//*[self::button or self::div][normalize-space()='Board']"),
|
||||
]
|
||||
|
||||
# Click the Board tab (ignore if already on board or tab not found)
|
||||
try:
|
||||
tab = _first_clickable(driver, board_locators, timeout=8)
|
||||
# Only click if the tab isn't already selected
|
||||
selected = (tab.get_attribute("aria-selected") or "").lower()
|
||||
if selected != "true":
|
||||
log_step("Switch to Board view")
|
||||
_js_click(driver, tab)
|
||||
else:
|
||||
log_step("Board tab already active")
|
||||
except TimeoutException:
|
||||
log_step("Board tab not found (continuing on current view)")
|
||||
|
||||
# Poll until board content is visible (task cards, bucket columns, or Add task)
|
||||
content_xpaths = [
|
||||
"//button[@data-testid='attachment-indicator']",
|
||||
"//*[@role='checkbox']",
|
||||
"//label[@role='checkbox']",
|
||||
"//*[contains(@data-testid,'TaskCard') or contains(@class,'TaskCard')]",
|
||||
"//*[@role='list' and ancestor::*[@role='group']]",
|
||||
"//*[normalize-space()='Add task' and ancestor::*[@role='group']]",
|
||||
"//*[normalize-space()='Add task']",
|
||||
]
|
||||
|
||||
log_step("Waiting for board content to load")
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
for xpath in content_xpaths:
|
||||
try:
|
||||
for el in driver.find_elements(By.XPATH, xpath)[:6]:
|
||||
if el.is_displayed():
|
||||
log_step("Board content visible — ready for screenshot")
|
||||
return
|
||||
except Exception:
|
||||
continue
|
||||
time.sleep(0.5)
|
||||
|
||||
log_step("Board content wait timed out — proceeding with screenshot anyway")
|
||||
|
||||
|
||||
def _ensure_grid_view(driver: WebDriver) -> None:
|
||||
grid_locators = [
|
||||
(By.CSS_SELECTOR, "button#gridViewPivot"),
|
||||
@@ -1642,13 +1741,13 @@ def _is_status_sorted_descending(header) -> bool:
|
||||
|
||||
|
||||
def _sort_descending_menu_selected(driver: WebDriver) -> bool:
|
||||
"""True when the column header menu shows Sort Descending as active."""
|
||||
"""True when the column header menu shows Sort descending as active."""
|
||||
probes = [
|
||||
"//button[@name='Sort Descending' and @aria-checked='true']",
|
||||
"//button[@name='Sort Descending' and contains(@class,'is-checked')]",
|
||||
"//button[@name='Sort Descending']//*[@data-icon-name='CheckMark']",
|
||||
"//button[@name='Sort descending' and @aria-checked='true']",
|
||||
"//button[@name='Sort descending' and contains(@class,'is-checked')]",
|
||||
"//button[@name='Sort descending']//*[@data-icon-name='CheckMark']",
|
||||
(
|
||||
"//button[@name='Sort Descending']"
|
||||
"//button[@name='Sort descending']"
|
||||
"/ancestor::li[contains(@class,'is-checked') or contains(@class,'is-selected')]"
|
||||
),
|
||||
]
|
||||
@@ -1692,13 +1791,18 @@ def _ensure_status_sort_descending(driver: WebDriver) -> None:
|
||||
return
|
||||
|
||||
sort_locators = [
|
||||
# Exact match — actual Planner DOM uses lowercase 'd'
|
||||
(By.XPATH, "//button[@name='Sort descending' and @role='menuitem']"),
|
||||
(By.XPATH, "//ul[contains(@class,'ContextualMenu')]//button[normalize-space()='Sort descending']"),
|
||||
(By.XPATH, "//button[@role='menuitem' and normalize-space()='Sort descending']"),
|
||||
(By.XPATH, "//span[normalize-space()='Sort descending']/ancestor::button[@role='menuitem']"),
|
||||
# Uppercase fallback (in case Planner localises differently)
|
||||
(By.XPATH, "//button[@name='Sort Descending' and @role='menuitem']"),
|
||||
(
|
||||
By.XPATH,
|
||||
"//ul[contains(@class,'ContextualMenu')]"
|
||||
"//button[normalize-space()='Sort Descending']",
|
||||
),
|
||||
(By.XPATH, "//button[@role='menuitem' and normalize-space()='Sort Descending']"),
|
||||
# Broad contains — catches any capitalisation
|
||||
(By.XPATH, "//*[@role='menuitem' and contains(normalize-space(),'Sort descending')]"),
|
||||
(By.XPATH, "//*[@role='menuitem' and contains(normalize-space(),'Sort Descending')]"),
|
||||
(By.XPATH, "//li[contains(@class,'ms-ContextualMenu-item')]//button[contains(normalize-space(),'escending')]"),
|
||||
]
|
||||
try:
|
||||
_click_any(driver, sort_locators, timeout=5)
|
||||
@@ -1712,8 +1816,30 @@ def _ensure_status_sort_descending(driver: WebDriver) -> None:
|
||||
except TimeoutException:
|
||||
log_step("Status column sort descending clicked")
|
||||
except TimeoutException:
|
||||
log_step("Sort Descending menu item not found (continuing on current sort)")
|
||||
_dismiss_open_menu(driver)
|
||||
log_step("Sort descending menu item not found via XPath — trying JS click")
|
||||
clicked = False
|
||||
try:
|
||||
clicked = driver.execute_script(
|
||||
"""
|
||||
var items = Array.from(document.querySelectorAll(
|
||||
'[role="menuitem"], [role="menuitem"] button, button[role="menuitem"]'
|
||||
));
|
||||
var target = items.find(function(el) {
|
||||
var t = (el.innerText || el.textContent || '').trim().toLowerCase();
|
||||
return t === 'sort descending';
|
||||
});
|
||||
if (target) { target.click(); return true; }
|
||||
return false;
|
||||
"""
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if clicked:
|
||||
time.sleep(1)
|
||||
log_step("Sort descending clicked via JS")
|
||||
else:
|
||||
log_step("Sort descending not found (continuing on current sort)")
|
||||
_dismiss_open_menu(driver)
|
||||
|
||||
|
||||
def _grid_probes_present(driver: WebDriver) -> bool:
|
||||
@@ -1902,11 +2028,14 @@ def _scroll_grid_down(driver: WebDriver, pixels: int = 450) -> None:
|
||||
time.sleep(0.4)
|
||||
|
||||
|
||||
def _scan_grid_not_started_titles(driver: WebDriver, *, max_scrolls: int = 100) -> list[str]:
|
||||
"""Scroll the virtualized grid and collect Not started task titles."""
|
||||
def _scan_grid_not_started_titles(driver: WebDriver, *, max_scrolls: int = 100) -> list[tuple[str, int]]:
|
||||
"""Scroll the virtualized grid and collect (title, aria-rowindex) for Not started tasks.
|
||||
|
||||
Uses aria-rowindex as the unique key so duplicate task names are all captured.
|
||||
"""
|
||||
_scroll_grid_to_top(driver)
|
||||
seen: set[str] = set()
|
||||
titles: list[str] = []
|
||||
seen_indices: set[int] = set()
|
||||
tasks: list[tuple[str, int]] = []
|
||||
stagnant = 0
|
||||
for _ in range(max_scrolls):
|
||||
new_this_pass = 0
|
||||
@@ -1914,14 +2043,15 @@ def _scan_grid_not_started_titles(driver: WebDriver, *, max_scrolls: int = 100)
|
||||
status = _grid_row_status(row)
|
||||
if status != "not started":
|
||||
continue
|
||||
idx_raw = row.get_attribute("aria-rowindex") or ""
|
||||
row_idx = int(idx_raw) if idx_raw.isdigit() else -1
|
||||
if row_idx < 1 or row_idx in seen_indices:
|
||||
continue
|
||||
title = _grid_row_task_name(row).strip()
|
||||
if not title or _is_weak_task_title_key(title.lower()):
|
||||
continue
|
||||
key = title.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
titles.append(title)
|
||||
seen_indices.add(row_idx)
|
||||
tasks.append((title, row_idx))
|
||||
new_this_pass += 1
|
||||
if new_this_pass == 0:
|
||||
stagnant += 1
|
||||
@@ -1931,8 +2061,22 @@ def _scan_grid_not_started_titles(driver: WebDriver, *, max_scrolls: int = 100)
|
||||
stagnant = 0
|
||||
_scroll_grid_down(driver)
|
||||
_scroll_grid_to_top(driver)
|
||||
log_step("Grid not-started tasks scanned", count=len(titles))
|
||||
return titles
|
||||
log_step("Grid not-started tasks scanned", count=len(tasks))
|
||||
return tasks
|
||||
|
||||
|
||||
def _find_grid_row_by_index(driver: WebDriver, row_index: int, *, max_scrolls: int = 100):
|
||||
"""Find a grid row by its aria-rowindex — stable even when multiple rows share the same title."""
|
||||
if row_index < 1:
|
||||
return None
|
||||
_scroll_grid_to_top(driver)
|
||||
for _ in range(max_scrolls):
|
||||
for row in _grid_visible_data_rows(driver):
|
||||
idx_raw = row.get_attribute("aria-rowindex") or ""
|
||||
if idx_raw.isdigit() and int(idx_raw) == row_index:
|
||||
return row
|
||||
_scroll_grid_down(driver)
|
||||
return None
|
||||
|
||||
|
||||
def _find_grid_row_by_title(driver: WebDriver, title: str, *, max_scrolls: int = 100):
|
||||
@@ -6047,13 +6191,13 @@ def save_board_screenshot(
|
||||
safe = _sanitize_folder_name(facility_name)
|
||||
taken_at = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
path = shots_dir / f"{safe}_{taken_at}.png"
|
||||
capture_fullscreen_screenshot(driver, path)
|
||||
capture_browser_screenshot(driver, path)
|
||||
log_step(
|
||||
"Board screenshot saved",
|
||||
facility=facility_name,
|
||||
tasks=task_count,
|
||||
path=str(path),
|
||||
fullscreen="true",
|
||||
fullscreen="false",
|
||||
view="board",
|
||||
)
|
||||
if task_count:
|
||||
@@ -6114,6 +6258,7 @@ def process_facility(
|
||||
try:
|
||||
open_my_plans_shared(driver)
|
||||
_open_facility_from_shared_list(driver, facility_name)
|
||||
_wait_for_board_loaded(driver)
|
||||
save_board_screenshot(driver, facility_name)
|
||||
_ensure_grid_view(driver)
|
||||
_ensure_status_sort_descending(driver)
|
||||
@@ -6135,8 +6280,8 @@ def process_facility(
|
||||
log_step("Grid not visible — waiting for grid to settle", facility=facility_name)
|
||||
time.sleep(6)
|
||||
|
||||
task_titles = _scan_grid_not_started_titles(driver)
|
||||
task_count = len(task_titles)
|
||||
task_entries = _scan_grid_not_started_titles(driver)
|
||||
task_count = len(task_entries)
|
||||
log_step(
|
||||
"Scan tasks on grid",
|
||||
task_count=task_count,
|
||||
@@ -6147,19 +6292,21 @@ def process_facility(
|
||||
return FacilityRunResult()
|
||||
|
||||
download_dir_ready = False
|
||||
processed_titles: set[str] = set()
|
||||
processed_indices: set[int] = set()
|
||||
|
||||
for task_num, grid_title in enumerate(task_titles, start=1):
|
||||
for task_num, (grid_title, row_index) in enumerate(task_entries, start=1):
|
||||
title_key = grid_title.strip().lower()
|
||||
if title_key in processed_titles:
|
||||
if row_index in processed_indices:
|
||||
continue
|
||||
_close_task_panel(driver)
|
||||
task_title = ""
|
||||
try:
|
||||
row = _find_grid_row_by_title(driver, grid_title)
|
||||
# Locate by row index first (handles duplicate task names correctly),
|
||||
# fall back to title search if the row has no aria-rowindex.
|
||||
row = _find_grid_row_by_index(driver, row_index) or _find_grid_row_by_title(driver, grid_title)
|
||||
if row is None:
|
||||
log_step("Grid row not found", title=grid_title[:60], facility=facility_name)
|
||||
processed_titles.add(title_key)
|
||||
processed_indices.add(row_index)
|
||||
continue
|
||||
|
||||
row_status = _grid_row_status(row)
|
||||
@@ -6170,7 +6317,7 @@ def process_facility(
|
||||
status=row_status or "",
|
||||
facility=facility_name,
|
||||
)
|
||||
processed_titles.add(title_key)
|
||||
processed_indices.add(row_index)
|
||||
continue
|
||||
|
||||
log_step(
|
||||
@@ -6180,7 +6327,7 @@ def process_facility(
|
||||
facility=facility_name,
|
||||
)
|
||||
# Re-find row right before click — virtualized grid can shift rows.
|
||||
row = _find_grid_row_by_title(driver, grid_title)
|
||||
row = _find_grid_row_by_index(driver, row_index) or _find_grid_row_by_title(driver, grid_title)
|
||||
if row is None or not _open_grid_row_details(driver, row):
|
||||
log_step("Grid task open failed", title=grid_title[:60], facility=facility_name)
|
||||
continue
|
||||
@@ -6188,7 +6335,7 @@ def process_facility(
|
||||
# Grid Status column already filtered to Not started — go straight
|
||||
# to Attachments tab (no panel status gate).
|
||||
task_title = _clean_grid_task_title(grid_title)
|
||||
processed_titles.add(task_title.strip().lower() or title_key)
|
||||
processed_indices.add(row_index)
|
||||
log_step("Task opened from grid", title=task_title[:60])
|
||||
time.sleep(PANEL_TAB_LOAD_DELAY)
|
||||
|
||||
@@ -6280,10 +6427,7 @@ def process_facility(
|
||||
)
|
||||
_log.warning("Task %d failed: %s", task_num, exc)
|
||||
_close_task_panel(driver)
|
||||
if task_title:
|
||||
processed_titles.add(task_title.strip().lower())
|
||||
elif title_key:
|
||||
processed_titles.add(title_key)
|
||||
processed_indices.add(row_index)
|
||||
|
||||
_remove_empty_directory(dest_folder)
|
||||
log_step(
|
||||
|
||||
+77
-1
@@ -58,6 +58,64 @@ from debug_log import write_debug as _agent_log
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_browser_dead(exc: Exception) -> bool:
|
||||
"""True when Chrome itself has crashed and the WebDriver session is gone."""
|
||||
err = str(exc).lower()
|
||||
return any(phrase in err for phrase in (
|
||||
"invalid session id",
|
||||
"session deleted",
|
||||
"browser has closed the connection",
|
||||
"chrome not reachable",
|
||||
"cannot connect to chrome",
|
||||
"no such window",
|
||||
))
|
||||
|
||||
|
||||
def _restart_browser(cfg: "AppConfig") -> "webdriver.Chrome | None":
|
||||
"""Kill Chrome, relaunch it, and re-login to Planner. Returns new driver on success."""
|
||||
log_step("Browser crash detected — restarting Chrome")
|
||||
_log.warning("Restarting Chrome after crash...")
|
||||
try:
|
||||
stop_chrome_browser()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(4)
|
||||
try:
|
||||
if not start_chrome_debug(
|
||||
cfg.proxy_ip, cfg.proxy_port, cfg.proxy_username, cfg.proxy_password
|
||||
):
|
||||
log_step("Chrome restart failed — start_chrome_debug returned False")
|
||||
return None
|
||||
new_driver = attach_browser(create_download_dir=False)
|
||||
if new_driver is None:
|
||||
log_step("Chrome restart failed — attach_browser returned None")
|
||||
return None
|
||||
consolidate_browser_to_single_tab(new_driver)
|
||||
if cfg.proxy_username and cfg.proxy_password:
|
||||
dismiss_proxy_auth_popup(
|
||||
new_driver,
|
||||
cfg.proxy_username.strip(),
|
||||
str(cfg.proxy_password),
|
||||
wait_secs=8.0,
|
||||
max_attempts=3,
|
||||
)
|
||||
time.sleep(2)
|
||||
login_and_open_planner(
|
||||
new_driver,
|
||||
cfg.onedrive_username,
|
||||
cfg.onedrive_password,
|
||||
cfg.microsoft_totp_secret,
|
||||
planner_url=cfg.planner_url,
|
||||
)
|
||||
log_step("Chrome restarted and Planner re-opened after crash")
|
||||
_log.info("Chrome restarted successfully after crash.")
|
||||
return new_driver
|
||||
except Exception as exc:
|
||||
log_step("Browser restart failed", error=str(exc)[:200])
|
||||
_log.error("Browser restart failed: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _show_error_dialog(title: str, message: str) -> None:
|
||||
try:
|
||||
root = tk.Tk()
|
||||
@@ -624,7 +682,10 @@ def run_download_workflow(
|
||||
log_step("Facility failed", facility=fac, error=err_msg[:200], status=status)
|
||||
_log.error("%s: %s", fac, err_msg)
|
||||
if screenshot_on_error and driver is not None:
|
||||
save_failure_screenshot(driver, fac, failure_log_dir=failure_log_dir)
|
||||
try:
|
||||
save_failure_screenshot(driver, fac, failure_log_dir=failure_log_dir)
|
||||
except Exception:
|
||||
pass
|
||||
failures.append((fac, err_msg, status))
|
||||
results[fac] = {
|
||||
"original": [],
|
||||
@@ -644,6 +705,21 @@ def run_download_workflow(
|
||||
completed_facilities=sorted(completed),
|
||||
)
|
||||
|
||||
# Chrome crashed — restart it so remaining facilities can still run
|
||||
if _is_browser_dead(exc):
|
||||
_log.warning(
|
||||
"Browser crash detected after '%s' — restarting Chrome to continue run",
|
||||
fac,
|
||||
)
|
||||
new_driver = _restart_browser(cfg)
|
||||
if new_driver is not None:
|
||||
driver = new_driver
|
||||
else:
|
||||
_log.error(
|
||||
"Could not restart browser after crash — aborting remaining facilities"
|
||||
)
|
||||
break
|
||||
|
||||
summary_path = (
|
||||
PROJECT_ROOT / "Output" / TODAY_ISO_DATE / "Summary"
|
||||
/ f"Download_Summary_{TODAY_ISO_DATE}.xlsx"
|
||||
|
||||
Reference in new issue
Block a user