Initial commit: plan, spec, tickets and dashboard skill templates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
49f8f49b65
39 files changed
+10736
No files matched your search
@@ -0,0 +1,171 @@
|
||||
"""Drive the Run tab's 7-box checkpoint tracker.
|
||||
kind: 'active' (box cp is now running) | 'done' (box cp finished) |
|
||||
'stuck' (box cp overran its max time)."""
|
||||
self.send_to_js("cpstate", {"kind": kind, "cp": int(cp)})
|
||||
|
||||
def _on_automation_line(self, line, level):
|
||||
"""Called (from the worker thread) for every printed line."""
|
||||
# Watchdog checkpoint state marker -> 7-box tracker in the Run tab
|
||||
# (raw line still stored in the DB log below).
|
||||
if "[CPSTATE]" in line:
|
||||
try:
|
||||
kind, cp = line.split("[CPSTATE]", 1)[1].split()
|
||||
self._cpstate(kind, cp)
|
||||
# Feed the parent-side stall watchdog. The automation runs in a
|
||||
# CHILD process, so the in-process performer.WD_STATE the old
|
||||
# watchdog read is ALWAYS empty here (it never fired — a stuck
|
||||
# checkpoint never restarted). Track the active checkpoint + its
|
||||
# start time from the STREAMED [CPSTATE] line instead.
|
||||
if kind == "active":
|
||||
self._wd_cp = int(cp)
|
||||
self._wd_since = time.time()
|
||||
except Exception:
|
||||
pass
|
||||
low = line.lower()
|
||||
ltype = "info"
|
||||
if "[error]" in low or "[fatal]" in low or "traceback" in low or "exception" in low:
|
||||
ltype = "error"
|
||||
elif "warning" in low or "[warn" in low:
|
||||
ltype = "warn"
|
||||
elif "processed successfully" in low or "[all done]" in low or "success" in low:
|
||||
ltype = "success"
|
||||
|
||||
# The FULL raw technical line always goes to the per-batch DB log AND to
|
||||
# the DB-independent file log (the file works even when the DB is down).
|
||||
if self._batch_log is not None:
|
||||
self._batch_log.log(ltype, line)
|
||||
if self._run_log_file is not None:
|
||||
try:
|
||||
self._run_log_file.write(line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# The on-screen log gets the plain-English translation only; raw lines
|
||||
# stay off the screen — except untranslated ERRORS, which are shown
|
||||
# as-is (a failure must never be invisible to the user).
|
||||
shown = friendly_log_line(line)
|
||||
if shown is not None:
|
||||
if shown[0] != self._last_friendly: # collapse repeated polling lines
|
||||
self._log(shown[0], shown[1])
|
||||
self._last_friendly = shown[0]
|
||||
elif ltype == "error" and not _is_error_noise(low):
|
||||
# An untranslated real error still shows (a failure must never be
|
||||
# invisible) — but raw traceback/COM noise is filtered out.
|
||||
self._log(line, "error")
|
||||
self._last_friendly = None
|
||||
|
||||
# advance the progress bar / step list on known markers
|
||||
for marker, step_idx, pct in STEP_MARKERS:
|
||||
if marker in low:
|
||||
if step_idx > self._cur_step:
|
||||
self._cur_step = step_idx
|
||||
if pct > self._max_pct:
|
||||
self._max_pct = pct
|
||||
self._step(STEPS[self._cur_step], self._max_pct)
|
||||
break
|
||||
|
||||
def _resolve_input_path(self, p):
|
||||
"""Drag-drop in QWebEngine often hides the real disk path, so we may only
|
||||
get a bare filename. Resolve it against the known input folders so the
|
||||
automation always receives a real, absolute path."""
|
||||
if p and os.path.isabs(p) and os.path.exists(p):
|
||||
return p
|
||||
base = os.path.basename(p) if p else ""
|
||||
for folder in (INPUT_DIR, _ROOT):
|
||||
cand = os.path.join(folder, base) if base else ""
|
||||
if cand and os.path.exists(cand):
|
||||
return cand
|
||||
return p # unresolved — caller will report a clear error
|
||||
|
||||
def _run_multi_subprocess(self, excel_path, emp):
|
||||
"""Run performer.run_multi in a SEPARATE PROCESS (its own MAIN thread,
|
||||
no Qt) and stream its stdout back through this run's log pipeline.
|
||||
|
||||
Why a subprocess: pywinauto/UIA reads are reliable on a process MAIN
|
||||
thread but flaky on a Qt BACKGROUND thread (STA-without-a-message-pump
|
||||
for cross-process UIA; forcing MTA dead-locks against the Qt main
|
||||
thread's UIA object). The reference tool works because it is a separate
|
||||
process with no Qt — this reproduces that. Every child stdout line is
|
||||
re-printed here so it flows through _StreamToBridge → _on_automation_line
|
||||
exactly as an in-process print did (friendly log, [CPSTATE] tracker,
|
||||
per-batch DB log, file log). Returns the run_multi summary dict.
|
||||
"""
|
||||
import subprocess
|
||||
import time as _time
|
||||
proj_root = os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__))))
|
||||
|
||||
# The child writes ALL its output to THIS file and we tail it live. A
|
||||
# --windowed PyInstaller child's stdout PIPE is block-buffered/unreliable
|
||||
# (v1.1.5: nothing streamed back), but a line-buffered file always
|
||||
# flushes per line and can be read while the process runs.
|
||||
try:
|
||||
os.makedirs(LOGS_DIR, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
child_log = os.path.join(
|
||||
LOGS_DIR,
|
||||
f"auto_child_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
f"_{self.batch_id or 0}.log")
|
||||
try:
|
||||
open(child_log, "w", encoding="utf-8").close() # start empty
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
cmd = [sys.executable, "--run-automation"]
|
||||
else:
|
||||
cmd = [sys.executable, os.path.join(proj_root, "run_app.py"),
|
||||
"--run-automation"]
|
||||
cmd += ["--excel", str(excel_path),
|
||||
"--batch", str(self.batch_id or ""),
|
||||
"--emp", str(emp or ""),
|
||||
"--log", child_log,
|
||||
# Parent-death watchdog: the child exits itself the instant this
|
||||
# GUI process dies (crash, taskkill, or a closeEvent we couldn't
|
||||
# run) so automation can never keep driving Tebra orphaned.
|
||||
"--parent-pid", str(os.getpid())]
|
||||
|
||||
env = dict(os.environ)
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0) \
|
||||
if sys.platform.startswith("win") else 0
|
||||
|
||||
print(f"[Automation] Launching automation in its own process "
|
||||
f"(reliable UIA) — batch {self.batch_id} …")
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
cwd=proj_root, env=env, creationflags=creationflags)
|
||||
except Exception as e:
|
||||
print(f"[Automation] Could not launch automation process: {e}")
|
||||
return {"total": 0, "ok": [],
|
||||
"failed": [("Automation", f"launch failed: {e}")],
|
||||
"stopped": False}
|
||||
self._auto_proc = proc
|
||||
|
||||
summary = {}
|
||||
got_summary = False
|
||||
|
||||
def _handle(line):
|
||||
nonlocal summary, got_summary
|
||||
if line.startswith("__WF_SUMMARY__"):
|
||||
try:
|
||||
summary = json.loads(line[len("__WF_SUMMARY__"):]) or {}
|
||||
got_summary = True
|
||||
except Exception:
|
||||
pass
|
||||
elif line.strip():
|
||||
print(line) # → _StreamToBridge tee → _on_automation_line
|
||||
|
||||
# Tail the child log until the process exits, then drain the rest.
|
||||
try:
|
||||
with open(child_log, "r", encoding="utf-8", errors="replace") as f:
|
||||
while True:
|
||||
pos = f.tell()
|
||||
line = f.readline()
|
||||
if line and line.endswith("\n"):
|
||||
_handle(line.rstrip("\r\n"))
|
||||
elif line: # partial line (no newline yet)
|
||||
if proc.poll() is not None:
|
||||
_handle(line.rstrip("\r\n"))
|
||||
Reference in new issue
Block a user