604 lines
23 KiB
Python
Executable File
604 lines
23 KiB
Python
Executable File
"""
|
|
FileCabinet CS - Batch Export Automation (Step 2)
|
|
=================================================
|
|
Reads a list of drawer IDs from a file, then for each drawer:
|
|
1. Navigates to it in the main window (type ID -> Go)
|
|
2. Opens File > Send To > File
|
|
3. Clicks "Select ->" (grabs all documents via the tree root)
|
|
4. Clicks OK
|
|
5. Waits for the "Exporting Documents" progress dialog to appear and finish
|
|
6. Logs success/failure and moves to the next drawer
|
|
|
|
Key safety features:
|
|
- RESUME: completed drawer IDs are appended to a progress log. On restart,
|
|
already-completed drawers are skipped. A multi-hour run that dies partway
|
|
can simply be re-run and it picks up where it left off.
|
|
- LOGGING: every action and error is timestamped to a log file.
|
|
- DEFENSIVE: unexpected states are caught, logged, screenshotted, and the
|
|
run continues with the next drawer rather than crashing.
|
|
|
|
PREREQUISITES:
|
|
- Run from 32-bit Python (matches FileCabinet CS's architecture)
|
|
- FileCabinet CS already open, pointed at the correct data location
|
|
- Export options/checkboxes and destination already set MANUALLY once
|
|
(they persist between exports within a session)
|
|
- Neither FileCabinet nor this script running elevated (they must match)
|
|
"""
|
|
|
|
import time
|
|
import sys
|
|
import os
|
|
|
|
from pywinauto import Desktop, Application
|
|
from pywinauto import timings
|
|
|
|
from fccs_config import parse_args, load_config, make_logger, load_lines
|
|
|
|
|
|
# Per-drawer outcomes.
|
|
STATUS_SUCCESS = "success" # exported cleanly -> completed.txt
|
|
STATUS_CRASHED = "crashed" # FCCS converter crash -> crashed.txt (skip on rerun)
|
|
STATUS_FAILED = "failed" # navigation/timeout/other -> retried next run
|
|
|
|
# Substrings that mark the FCCS crash error dialog body (case-insensitive),
|
|
# used to distinguish a converter crash from other "FileCabinet CS" prompts.
|
|
_ERROR_SIGNATURES = ("failed", "access violation", "convert")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HELPERS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def load_completed(path):
|
|
"""Read the set of already-completed drawer IDs for resume support."""
|
|
if not os.path.exists(path):
|
|
return set()
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return {line.strip() for line in f if line.strip()}
|
|
|
|
|
|
def mark_completed(path, drawer_id):
|
|
"""Append a drawer ID to the completed log (resume tracking)."""
|
|
with open(path, "a", encoding="utf-8") as f:
|
|
f.write(drawer_id + "\n")
|
|
|
|
|
|
def mark_crashed(path, drawer_id):
|
|
"""Append a drawer ID to the crashed log (skipped on future runs)."""
|
|
with open(path, "a", encoding="utf-8") as f:
|
|
f.write(drawer_id + "\n")
|
|
|
|
|
|
def take_screenshot(window, drawer_id, tag, screenshot_dir, log):
|
|
"""Save a screenshot of a window for later diagnosis of a failure."""
|
|
from datetime import datetime
|
|
try:
|
|
os.makedirs(screenshot_dir, exist_ok=True)
|
|
fname = os.path.join(
|
|
screenshot_dir,
|
|
f"{drawer_id}_{tag}_{datetime.now():%Y%m%d_%H%M%S}.png"
|
|
)
|
|
window.capture_as_image().save(fname)
|
|
log(f" Screenshot saved: {fname}")
|
|
except Exception as e:
|
|
log(f" Could not capture screenshot: {e}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CONNECTION
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def connect_main(ctrl, log):
|
|
"""Find and connect to the running FileCabinet CS main window."""
|
|
target = None
|
|
for w in Desktop(backend="win32").windows():
|
|
if w.class_name() == ctrl["main_class"]:
|
|
target = w
|
|
break
|
|
if target is None:
|
|
log("ERROR: FileCabinet CS window not found. Is it open?")
|
|
sys.exit(1)
|
|
|
|
app = Application(backend="win32").connect(handle=target.handle)
|
|
main = app.window(handle=target.handle)
|
|
log(f"Connected to: {main.window_text()}")
|
|
return app, main
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PER-DRAWER EXPORT
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def find_search_box(main, ctrl, timeouts, log):
|
|
"""Locate the drawer search box reliably using the Go button as an anchor.
|
|
|
|
The main window has several Edit controls (notably the data-location box
|
|
inside a ComboBox). Their best_match names ("Edit", "Edit0"...) are
|
|
unstable and the placeholder title only exists in a fresh session, so we
|
|
anchor off the Go button instead: the search box sits on the same row
|
|
(same top Y) as Go, immediately to its left. Call once before the drawer
|
|
loop — the returned wrapper stays valid for the window's lifetime.
|
|
"""
|
|
go = main.child_window(title=ctrl["go_button_title"], class_name="Button")
|
|
go.wait("visible ready", timeout=timeouts["nav"])
|
|
go_rect = go.rectangle()
|
|
|
|
edits = main.children(class_name="Edit")
|
|
if not edits:
|
|
edits = main.descendants(class_name="Edit")
|
|
|
|
tolerance = 10
|
|
candidates = [
|
|
e for e in edits
|
|
if abs(e.rectangle().top - go_rect.top) <= tolerance
|
|
]
|
|
if not candidates:
|
|
raise RuntimeError(
|
|
"Could not find the drawer search box (no Edit control on the "
|
|
"Go button's row). FCCS may not be on the main drawer view."
|
|
)
|
|
|
|
# If several share the row, pick the one whose right edge is closest to
|
|
# (and left of) the Go button — that's the box directly beside Go.
|
|
left_of_go = [e for e in candidates if e.rectangle().right <= go_rect.left]
|
|
pool = left_of_go if left_of_go else candidates
|
|
box = max(pool, key=lambda e: e.rectangle().right)
|
|
|
|
r = box.rectangle()
|
|
log(f" Search box located at {(r.left, r.top, r.right, r.bottom)} "
|
|
f"(Go button top={go_rect.top})")
|
|
return box
|
|
|
|
|
|
def navigate_to_drawer(main, search_box, drawer_id, ctrl, timeouts, log):
|
|
"""Type the drawer ID into the search box and click Go."""
|
|
# search_box is a control wrapper (from find_search_box), not a
|
|
# WindowSpecification, so it has no .wait() — it was already confirmed
|
|
# present/visible when located. set_edit_text works directly on it.
|
|
search_box.set_edit_text("")
|
|
search_box.set_edit_text(drawer_id)
|
|
time.sleep(timeouts["settle"])
|
|
|
|
main.child_window(title=ctrl["go_button_title"], class_name="Button").click()
|
|
|
|
deadline = time.time() + timeouts["nav"]
|
|
# Whatever drawer your are actively on becomes the actual name of the App Window. We are checking that again to make sure we found the drawer.
|
|
while time.time() < deadline:
|
|
title = main.window_text()
|
|
if f"[{drawer_id}" in title:
|
|
log(f" Navigated to drawer {drawer_id}: {title}")
|
|
return True
|
|
time.sleep(0.3)
|
|
|
|
log(f" WARNING: window title did not confirm drawer {drawer_id} "
|
|
f"(title is: {main.window_text()!r})")
|
|
return False
|
|
|
|
|
|
def open_send_to_file(main, app, ctrl, timeouts):
|
|
"""Open File > Send To > File and return the dialog window object."""
|
|
main.menu_select("File->Send To->File")
|
|
|
|
dlg = app.window(title=ctrl["dialog_title"], class_name=ctrl["dialog_class"])
|
|
dlg.wait("visible ready", timeout=timeouts["dialog"])
|
|
return dlg
|
|
|
|
|
|
def read_manifest(dlg, drawer_id, log):
|
|
"""Read the selected documents from the ListView into clean rows.
|
|
|
|
The ListView (`List1`) has columns Drawer ID, Page Title (the document
|
|
name) and Application. `texts()` returns a flat, row-major dump led by the
|
|
control's own name ('List1'), which is why the raw capture looked messy.
|
|
|
|
This parses it into one row per document and drops the redundant Drawer ID
|
|
column (always equal to drawer_id) and any empty columns, leaving the
|
|
document name (and Application) per line. Returns a list of rows (each a
|
|
list of cell strings), [] if empty, or None if it couldn't be read.
|
|
"""
|
|
try:
|
|
lv = dlg.child_window(title="List1", class_name="SysListView32")
|
|
n_rows = lv.item_count()
|
|
except Exception as e:
|
|
log(f" WARNING: could not read manifest list: {e}")
|
|
return None
|
|
|
|
if n_rows == 0:
|
|
log(" WARNING: ListView is empty after Select")
|
|
return []
|
|
|
|
try:
|
|
n_cols = lv.column_count()
|
|
except Exception:
|
|
n_cols = 0
|
|
if not n_cols or n_cols < 1:
|
|
n_cols = 3 # observed columns: Drawer ID, Page Title, Application
|
|
|
|
# Read every cell. Primary: get_item(row, col). Fallback: reshape texts().
|
|
try:
|
|
grid = [[lv.get_item(r, c).text() for c in range(n_cols)]
|
|
for r in range(n_rows)]
|
|
except Exception as e:
|
|
log(f" NOTE: get_item failed ({e}); falling back to texts().")
|
|
raw = lv.texts()
|
|
if raw and raw[0] == lv.window_text():
|
|
raw = raw[1:] # drop the control's own name ('List1')
|
|
grid = [raw[i:i + n_cols] for i in range(0, len(raw), n_cols)]
|
|
grid = [row for row in grid if len(row) == n_cols]
|
|
|
|
# Keep only columns that carry real, non-redundant info.
|
|
keep_cols = []
|
|
for c in range(n_cols):
|
|
vals = [(row[c].strip() if c < len(row) else "") for row in grid]
|
|
if all(v == drawer_id for v in vals):
|
|
continue # redundant Drawer ID column
|
|
if all(v == "" for v in vals):
|
|
continue # empty column
|
|
keep_cols.append(c)
|
|
|
|
rows = [[(row[c] if c < len(row) else "") for c in keep_cols]
|
|
for row in grid]
|
|
log(f" Manifest captured: {len(rows)} documents selected")
|
|
return rows
|
|
|
|
|
|
def save_manifest(drawer_id, items, manifest_dir, log):
|
|
"""Save the list of expected export items to a manifest file."""
|
|
os.makedirs(manifest_dir, exist_ok=True)
|
|
path = os.path.join(manifest_dir, f"{drawer_id}.txt")
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
for row in items:
|
|
if isinstance(row, (list, tuple)):
|
|
f.write("\t".join(str(c) for c in row) + "\n")
|
|
else:
|
|
f.write(str(row) + "\n")
|
|
log(f" Manifest saved: {path}")
|
|
|
|
|
|
def perform_export(dlg, drawer_id, ctrl, timeouts, log):
|
|
"""Click Select -> then OK to start the export. Returns manifest rows."""
|
|
select_btn = dlg.child_window(title=ctrl["select_btn_title"], class_name="Button")
|
|
select_btn.wait("visible ready enabled", timeout=timeouts["dialog"])
|
|
select_btn.click()
|
|
|
|
ok_btn = dlg.child_window(title=ctrl["ok_btn_title"], class_name="Button")
|
|
ok_btn.wait("visible ready enabled", timeout=timeouts["select"])
|
|
|
|
manifest = read_manifest(dlg, drawer_id, log)
|
|
|
|
ok_btn.click()
|
|
return manifest
|
|
|
|
|
|
def dismiss_any_dialog(app, ctrl, log):
|
|
"""Try to find and dismiss any stale dialogs left from a previous failure."""
|
|
# Dismiss a lingering converter-crash error dialog (OK)
|
|
w = _find_error_dialog(ctrl)
|
|
if w:
|
|
try:
|
|
_click_dialog_button(w, "OK", log)
|
|
log(" Dismissed stale crash error dialog.")
|
|
except Exception:
|
|
pass
|
|
|
|
# Dismiss "Send to" file saved confirmation dialog (OK)
|
|
w = _find_desktop_dialog(ctrl["saved_title"])
|
|
if w:
|
|
try:
|
|
_click_dialog_button(w, "OK", log)
|
|
log(" Dismissed stale confirmation dialog.")
|
|
except Exception:
|
|
pass
|
|
|
|
# Dismiss Send To File Location dialog (Cancel)
|
|
w = _find_desktop_dialog(ctrl["dialog_title"])
|
|
if w:
|
|
try:
|
|
_click_dialog_button(w, "Cancel", log)
|
|
log(" Dismissed stale Send To File dialog.")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _find_desktop_dialog(title_match):
|
|
"""Search all top-level windows for a visible #32770 dialog matching title."""
|
|
for w in Desktop(backend="win32").windows():
|
|
try:
|
|
if w.class_name() == "#32770" and w.is_visible() and title_match in w.window_text():
|
|
return w
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
|
|
def _click_dialog_button(dialog, button_title, log):
|
|
"""Connect to a desktop dialog and click a button on it."""
|
|
app = Application(backend="win32").connect(handle=dialog.handle)
|
|
dlg = app.window(handle=dialog.handle)
|
|
dlg.child_window(title=button_title, class_name="Button").click()
|
|
dlg.wait_not("visible", timeout=10)
|
|
|
|
|
|
def _find_error_dialog(ctrl):
|
|
"""Find the FCCS converter-crash error dialog, if present.
|
|
|
|
Matches a visible #32770 titled 'FileCabinet CS' whose body text carries a
|
|
crash signature (failed / access violation / convert), so we don't confuse
|
|
it with other 'FileCabinet CS' prompts. Returns the window or None.
|
|
"""
|
|
try:
|
|
windows = Desktop(backend="win32").windows()
|
|
except Exception:
|
|
return None
|
|
for w in windows:
|
|
try:
|
|
if w.class_name() != ctrl["error_class"] or not w.is_visible():
|
|
continue
|
|
if ctrl["error_title"] not in w.window_text():
|
|
continue
|
|
body = " ".join(s.window_text()
|
|
for s in w.children(class_name="Static")).lower()
|
|
if any(sig in body for sig in _ERROR_SIGNATURES):
|
|
return w
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
|
|
def _read_progress_doc(app, ctrl):
|
|
"""Read the document name from the 'Exporting Documents' progress dialog."""
|
|
try:
|
|
progress = app.window(title=ctrl["progress_title"],
|
|
class_name=ctrl["progress_class"])
|
|
for st in progress.children(class_name="Static"):
|
|
t = st.window_text()
|
|
if t and "document" in t.lower():
|
|
return t.strip()
|
|
except Exception:
|
|
pass
|
|
return "(unknown document)"
|
|
|
|
|
|
def wait_for_export(app, drawer_id, ctrl, timeouts, screenshot_dir, log):
|
|
"""
|
|
Wait for the export lifecycle to complete, watching for two outcomes:
|
|
- '"Send to" file saved' confirmation dialog -> STATUS_SUCCESS
|
|
- 'FileCabinet CS' converter-crash error -> STATUS_CRASHED
|
|
A drawer that shows neither before the deadline -> STATUS_FAILED.
|
|
"""
|
|
progress = app.window(title=ctrl["progress_title"], class_name=ctrl["progress_class"])
|
|
|
|
try:
|
|
progress.wait("visible", timeout=timeouts["progress_appear"])
|
|
log(" Export in progress...")
|
|
except timings.TimeoutError:
|
|
log(" NOTE: progress dialog not seen; checking for confirmation dialog "
|
|
"(drawer may have exported very quickly).")
|
|
|
|
# Poll the entire desktop each cycle for either the success dialog or a
|
|
# converter-crash error. app.window() can miss dialogs it doesn't own.
|
|
deadline = time.time() + timeouts["progress_finish"]
|
|
saved_win = None
|
|
while time.time() < deadline:
|
|
# Read the current document BEFORE checking for the crash, so if the
|
|
# error dialog has already replaced the progress text we still logged it.
|
|
current_doc = _read_progress_doc(app, ctrl)
|
|
|
|
err_win = _find_error_dialog(ctrl)
|
|
if err_win is not None:
|
|
log(f" CRASH: FCCS converter failed on {current_doc}. "
|
|
f"This aborts the drawer export.")
|
|
take_screenshot(err_win, drawer_id, "crash", screenshot_dir, log)
|
|
try:
|
|
_click_dialog_button(err_win, "OK", log)
|
|
log(" Crash dialog dismissed; FCCS returned to home screen.")
|
|
except Exception as e:
|
|
log(f" WARNING: could not dismiss crash dialog: {e}")
|
|
return STATUS_CRASHED
|
|
|
|
saved_win = _find_desktop_dialog(ctrl["saved_title"])
|
|
if saved_win is not None:
|
|
break
|
|
time.sleep(2)
|
|
|
|
if saved_win is None:
|
|
log(f" ERROR: no confirmation or crash within {timeouts['progress_finish']}s.")
|
|
return STATUS_FAILED
|
|
|
|
log(f" Confirmation dialog found: {saved_win.window_text()!r}")
|
|
|
|
try:
|
|
_click_dialog_button(saved_win, "OK", log)
|
|
log(" Confirmation dismissed.")
|
|
except Exception as e:
|
|
log(f" WARNING: could not dismiss confirmation dialog: {e}")
|
|
log(" Export succeeded but dialog remains — will be dismissed next iteration.")
|
|
|
|
log(" Export complete.")
|
|
return STATUS_SUCCESS
|
|
|
|
|
|
def export_drawer(app, main, search_box, drawer_id, ctrl, timeouts,
|
|
screenshot_dir, manifest_dir, log):
|
|
"""Full per-drawer sequence. Returns a STATUS_* outcome."""
|
|
log(f"Drawer {drawer_id}: starting")
|
|
|
|
# 0. Dismiss any stale dialogs left from a previous failure
|
|
dismiss_any_dialog(app, ctrl, log)
|
|
|
|
# 1. Navigate
|
|
if not navigate_to_drawer(main, search_box, drawer_id, ctrl, timeouts, log):
|
|
take_screenshot(main, drawer_id, "nav_fail", screenshot_dir, log)
|
|
return STATUS_FAILED
|
|
|
|
# 2. Open Send To File dialog
|
|
try:
|
|
dlg = open_send_to_file(main, app, ctrl, timeouts)
|
|
except Exception as e:
|
|
log(f" ERROR opening Send To File dialog: {e}")
|
|
take_screenshot(main, drawer_id, "dialog_fail", screenshot_dir, log)
|
|
return STATUS_FAILED
|
|
|
|
# 3 & 4. Select -> and OK (captures manifest before clicking OK)
|
|
try:
|
|
manifest = perform_export(dlg, drawer_id, ctrl, timeouts, log)
|
|
if manifest is not None:
|
|
save_manifest(drawer_id, manifest, manifest_dir, log)
|
|
except Exception as e:
|
|
log(f" ERROR during export selection: {e}")
|
|
take_screenshot(dlg, drawer_id, "export_fail", screenshot_dir, log)
|
|
try:
|
|
dlg.child_window(title="Cancel", class_name="Button").click()
|
|
except Exception:
|
|
pass
|
|
return STATUS_FAILED
|
|
|
|
# 5. Monitor progress (success / crash / timeout)
|
|
status = wait_for_export(app, drawer_id, ctrl, timeouts, screenshot_dir, log)
|
|
if status == STATUS_CRASHED:
|
|
return STATUS_CRASHED
|
|
if status != STATUS_SUCCESS:
|
|
take_screenshot(main, drawer_id, "progress_fail", screenshot_dir, log)
|
|
return STATUS_FAILED
|
|
|
|
time.sleep(timeouts["settle"])
|
|
return STATUS_SUCCESS
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MAIN LOOP
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main():
|
|
args = parse_args()
|
|
cfg = load_config(args.config)
|
|
log = make_logger(cfg.get("paths", "log_file"))
|
|
|
|
# Build lookup dicts from config
|
|
paths = {
|
|
"drawer_id_file": cfg.get("paths", "drawer_id_file"),
|
|
"completed_file": cfg.get("paths", "completed_file"),
|
|
"ignore_file": cfg.get("paths", "ignore_file"),
|
|
"crashed_file": cfg.get("paths", "crashed_file"),
|
|
"screenshot_dir": cfg.get("paths", "screenshot_dir"),
|
|
"manifest_dir": cfg.get("paths", "manifest_dir"),
|
|
}
|
|
timeouts = {
|
|
"nav": cfg.getfloat("timeouts", "nav_timeout"),
|
|
"dialog": cfg.getfloat("timeouts", "dialog_timeout"),
|
|
"select": cfg.getfloat("timeouts", "select_timeout"),
|
|
"progress_appear": cfg.getfloat("timeouts", "progress_appear"),
|
|
"progress_finish": cfg.getfloat("timeouts", "progress_finish"),
|
|
"settle": cfg.getfloat("timeouts", "settle"),
|
|
"confirm": cfg.getfloat("timeouts", "confirm_timeout"),
|
|
}
|
|
ctrl = {
|
|
"main_class": cfg.get("controls", "main_class"),
|
|
"drawer_box_title": cfg.get("controls", "drawer_box_title"),
|
|
"go_button_title": cfg.get("controls", "go_button_title"),
|
|
"dialog_title": cfg.get("controls", "dialog_title"),
|
|
"dialog_class": cfg.get("controls", "dialog_class"),
|
|
"select_btn_title": cfg.get("controls", "select_btn_title"),
|
|
"ok_btn_title": cfg.get("controls", "ok_btn_title"),
|
|
"progress_title": cfg.get("controls", "progress_title"),
|
|
"progress_class": cfg.get("controls", "progress_class"),
|
|
"saved_title": cfg.get("controls", "saved_title"),
|
|
"saved_class": cfg.get("controls", "saved_class"),
|
|
"saved_ok_title": cfg.get("controls", "saved_ok_title"),
|
|
"error_title": cfg.get("controls", "error_title"),
|
|
"error_class": cfg.get("controls", "error_class"),
|
|
}
|
|
|
|
log("=" * 60)
|
|
log("FileCabinet CS batch export - run starting")
|
|
log("=" * 60)
|
|
|
|
all_ids = load_lines(paths["drawer_id_file"])
|
|
if not all_ids:
|
|
log(f"ERROR: no drawer IDs found in {paths['drawer_id_file']}")
|
|
sys.exit(1)
|
|
|
|
completed = load_completed(paths["completed_file"])
|
|
ignored = set(load_lines(paths["ignore_file"]))
|
|
crashed = set(load_lines(paths["crashed_file"]))
|
|
|
|
pending = [d for d in all_ids
|
|
if d not in completed
|
|
and d not in ignored
|
|
and d not in crashed]
|
|
log(f"Total drawers in list : {len(all_ids)}")
|
|
log(f"Already completed : {len(completed)}")
|
|
log(f"Ignored (skipped) : {len(ignored & set(all_ids))}")
|
|
log(f"Crashed (skipped) : {len(crashed & set(all_ids))}")
|
|
log(f"Remaining to process : {len(pending)}")
|
|
|
|
if not pending:
|
|
log("Nothing to do - all drawers already completed.")
|
|
return
|
|
|
|
app, main_win = connect_main(ctrl, log)
|
|
|
|
try:
|
|
search_box = find_search_box(main_win, ctrl, timeouts, log)
|
|
except Exception as e:
|
|
log(f"ERROR: {e}")
|
|
sys.exit(1)
|
|
|
|
success_count = 0
|
|
fail_count = 0
|
|
crash_count = 0
|
|
failed_ids = []
|
|
crashed_ids = []
|
|
|
|
for i, drawer_id in enumerate(pending, start=1):
|
|
log(f"--- [{i}/{len(pending)}] Drawer {drawer_id} ---")
|
|
try:
|
|
status = export_drawer(app, main_win, search_box, drawer_id, ctrl,
|
|
timeouts, paths["screenshot_dir"],
|
|
paths["manifest_dir"], log)
|
|
except Exception as e:
|
|
log(f" UNEXPECTED ERROR on drawer {drawer_id}: {e}")
|
|
try:
|
|
take_screenshot(main_win, drawer_id, "unexpected",
|
|
paths["screenshot_dir"], log)
|
|
except Exception:
|
|
pass
|
|
status = STATUS_FAILED
|
|
|
|
if status == STATUS_SUCCESS:
|
|
mark_completed(paths["completed_file"], drawer_id)
|
|
success_count += 1
|
|
log(f" Drawer {drawer_id} DONE ({success_count} ok / "
|
|
f"{fail_count} failed / {crash_count} crashed so far)")
|
|
elif status == STATUS_CRASHED:
|
|
mark_crashed(paths["crashed_file"], drawer_id)
|
|
crash_count += 1
|
|
crashed_ids.append(drawer_id)
|
|
log(f" Drawer {drawer_id} CRASHED - added to crashed list; will be "
|
|
f"skipped on re-run. Handle manually (export excluding the "
|
|
f"poison document).")
|
|
else:
|
|
fail_count += 1
|
|
failed_ids.append(drawer_id)
|
|
log(f" Drawer {drawer_id} FAILED - will NOT be marked complete. "
|
|
f"Re-run later to retry.")
|
|
|
|
log("=" * 60)
|
|
log("Run finished.")
|
|
log(f" Succeeded : {success_count}")
|
|
log(f" Failed : {fail_count}")
|
|
log(f" Crashed : {crash_count}")
|
|
if failed_ids:
|
|
log(f" Failed drawer IDs: {', '.join(failed_ids)}")
|
|
log(" These were NOT marked complete; re-running the script "
|
|
"will retry them.")
|
|
if crashed_ids:
|
|
log(f" Crashed drawer IDs: {', '.join(crashed_ids)}")
|
|
log(f" These were added to {paths['crashed_file']} and will be skipped "
|
|
"on re-run. Export them manually, excluding the crashing document.")
|
|
log("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|