diff --git a/poll_job.py b/poll_job.py new file mode 100755 index 0000000..e53069f --- /dev/null +++ b/poll_job.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 + +import sys +import os +import re +import time +import subprocess +import requests +from requests.auth import HTTPBasicAuth + +CONSOLE_URL = os.environ.get("MVS_CONSOLE_URL") +CONSOLE_USER = os.environ.get("MVS_CONSOLE_USER") +CONSOLE_PASS = os.environ.get("MVS_CONSOLE_PASSWORD") +LINODE_HOST = os.environ.get("LINODE_SSH_HOST") +LINODE_PRINTOUT_DIR = os.environ.get("LINODE_PRINTOUT_DIR") + +def get_syslog(): + """Fetch the Hercules syslog via HTTP""" + try: + response = requests.get( + CONSOLE_URL, + auth=HTTPBasicAuth(CONSOLE_USER, CONSOLE_PASS), + timeout=10 + ) + response.raise_for_status() + return response.text + except requests.RequestException as e: + print(f"Failed to fetch syslog: {e}") + return None + +def find_job_number(syslog, jobname): + """Extract job number from $HASP100 message""" + # Pattern: /12.28.02 JOB 257 $HASP100 SIMPLE2 ON READER1 + pattern = rf'/\d+\.\d+\.\d+\s+JOB\s+(\d+)\s+\$HASP100\s+{jobname}\s+ON\s+READER' + match = re.search(pattern, syslog, re.IGNORECASE) + if match: + return match.group(1) + return None + +def check_job_completed(syslog, jobname, job_number): + """Check if a job has completed printing (HASP150 message)""" + # Pattern: /12.28.03 JOB 257 $HASP150 SIMPLE2 ON PRINTER1 + pattern = rf'/\d+\.\d+\.\d+\s+JOB\s+{job_number}\s+\$HASP150\s+{jobname}\s+ON\s+PRINTER' + return re.search(pattern, syslog, re.IGNORECASE) is not None + +def list_pdfs_local(local_dir): + """List PDF files in a local directory (for mounted volumes)""" + import glob + pdf_files = glob.glob(f"{local_dir}/v1403-*.pdf") + # Sort by modification time, newest first + pdf_files.sort(key=os.path.getmtime, reverse=True) + return pdf_files + +def list_pdfs_remote(): + """List PDF files on remote Linode via SSH""" + cmd = f"ssh {LINODE_HOST} ls -t {LINODE_PRINTOUT_DIR}/v1403-*.pdf" + try: + result = subprocess.run( + cmd, + shell=True, + check=True, + capture_output=True, + text=True + ) + return result.stdout.strip().split('\n') + except subprocess.CalledProcessError: + return [] + +def find_pdf_for_job(job_number, jname, local_printout_dir=None): + """Find the PDF matching job number and name""" + pattern = f"v1403-J{job_number}_{jname}-" + + # Try the local directory first (for mounted volumes in CI) + if local_printout_dir and os.path.isdir(str(local_printout_dir)): + pdfs = list_pdfs_local(local_printout_dir) + for pdf_path in pdfs: + if pattern in pdf_path: + return pdf_path + return None + + # Fall back to remote SSH access + pdfs = list_pdfs_remote() + for pdf_path in pdfs: + if pattern in pdf_path: + return pdf_path + return None + +def retrieve_pdf(source_path, local_filename, is_local=False): + """Retrieve PDF either locally (copy) or remotely (SCP)""" + try: + if is_local: + # Local copy from mounted volume + import shutil + shutil.copy2(source_path, local_filename) + print(f"Copied: {local_filename}") + else: + # Remote SCP + cmd = f"scp {LINODE_HOST}:{source_path} {local_filename}" + subprocess.run(cmd, shell=True, check=True) + print(f"Retrieved: {local_filename}") + return True + except (subprocess.CalledProcessError, IOError) as e: + print(f"Failed to retrieve PDF: {e}") + return False + +def poll_for_job(jn, to=300, poll_interval=5): + """Poll the console for job completion and retrieve PDF""" + jobname_upper = jn.upper() + start_time = time.time() + job_number = None + + print(f"Polling for job: {jobname_upper}") + print(f"Timeout: {to}s, Poll interval: {poll_interval}s") + print() + + # Phase 1: Find a job number + print("Phase 1: Looking for job submission ($HASP100)...") + while time.time() - start_time < to: + syslog = get_syslog() + if not syslog: + time.sleep(poll_interval) + continue + + job_number = find_job_number(syslog, jobname_upper) + if job_number: + print(f"Found job number: J{job_number}") + break + + time.sleep(poll_interval) + + if not job_number: + print(f"Timeout: Job {jobname_upper} not found in console after {to}s") + return 1 + + # Phase 2: Wait for completion + print(f"Phase 2: Waiting for job completion ($HASP150)...") + while time.time() - start_time < to: + syslog = get_syslog() + if not syslog: + time.sleep(poll_interval) + continue + + if check_job_completed(syslog, jobname_upper, job_number): + print(f"Job J{job_number} completed and printed!") + break + + time.sleep(poll_interval) + else: + print(f"Timeout: Job J{job_number} did not complete after {to}s") + return 1 + + # Phase 3: Retrieve PDF + print("Phase 3: Retrieving PDF...") + # Give the PDF a moment to be written to disk + time.sleep(2) + + # Check for local mounted directory (CI environment) + local_printout_dir = os.environ.get("LOCAL_PRINTOUT_DIR") + is_local = local_printout_dir and os.path.isdir(local_printout_dir) + + if is_local: + print(f"Using local mounted directory: {local_printout_dir}") + + pdf_path = find_pdf_for_job(job_number, jobname_upper, local_printout_dir) + if not pdf_path: + print(f"Error: PDF not found for J{job_number}_{jobname_upper}") + return 1 + + local_filename = f"{jobname_upper}_J{job_number}.pdf" + if retrieve_pdf(pdf_path, local_filename, is_local): + print(f"Success! Job output saved to: {local_filename}") + return 0 + else: + return 1 + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: poll_job.py [timeout_seconds]") + print() + print("Arguments:") + print(" jobname - Job name to poll for (required)") + print(" timeout_seconds - Maximum time to wait (optional, default: 300)") + print() + print("Example:") + print(" poll_job.py SIMPLE2") + print(" poll_job.py SIMPLE2 600") + sys.exit(1) + + jobname = sys.argv[1] + timeout = int(sys.argv[2]) if len(sys.argv) > 2 else 300 + + sys.exit(poll_for_job(jobname, timeout))