""" JCL Executor - executes parsed JCL steps. Phase 1: sequential execution, COND on return codes, DD mapping to files, SORT mapped to external sort utility. """ import os import re import subprocess import sys import tempfile from pathlib import Path from typing import Optional from parser import Job, JobStep, DDEntry, CondParam, COND_OPS COB_MAIN_DIR = r"D:\360安全浏览器下载\GC32-BDB-SP1-rename-7z-to-exe" class Executor: def __init__(self, root_dir: str, cobol_dir: str, copybook_dir: str): self.root_dir = Path(root_dir).resolve() self.cobol_dir = Path(cobol_dir).resolve() self.copybook_dir = Path(copybook_dir).resolve() self.bin_dir = self.root_dir / "bin" self.bin_dir.mkdir(exist_ok=True) self.last_rc: int = 0 self.step_rcs: dict[str, int] = {} def _cobol_env(self) -> dict[str, str]: """Build environment dict for GnuCOBOL execution.""" env = os.environ.copy() env["COB_MAIN_DIR"] = COB_MAIN_DIR env["COB_CONFIG_DIR"] = os.path.join(COB_MAIN_DIR, "config") env["COB_LIBRARY_PATH"] = os.path.join(COB_MAIN_DIR, "lib", "gnucobol") env["COBCPY"] = str(self.copybook_dir) cobbin = os.path.join(COB_MAIN_DIR, "bin") env["PATH"] = cobbin + os.pathsep + env.get("PATH", "") return env def run(self, job: Job): print(f"\n{'='*60}") print(f"JOB: {job.job_name}") print(f"{'='*60}") for i, step in enumerate(job.steps): self._execute_step(step, i) print(f"\n{'='*60}") print(f"JOB {job.job_name} COMPLETED") print(f"STEPS: {len(job.steps)}, FINAL RC: {self.last_rc}") print(f"{'='*60}") return self.last_rc def _execute_step(self, step: JobStep, idx: int): print(f"\n--- STEP {idx+1}: {step.step_name} (PGM={step.program}) ---") # COND check if step.cond and not self._check_cond(step.cond): print(f" COND: SKIPPED ({step.cond})") return program_upper = step.program.upper() if program_upper == "SORT": rc = self._run_sort(step) else: rc = self._run_cobol(step) self.last_rc = rc self.step_rcs[step.step_name] = rc print(f" RC: {rc}") def _check_cond(self, cond: CondParam) -> bool: """Return True if step should run, False if skipped.""" if cond.step_name: target_rc = self.step_rcs.get(cond.step_name, 0) else: # Check all previous steps for rc in self.step_rcs.values(): if COND_OPS.get(cond.operator, lambda x, y: False)(rc, cond.code): return False return True if COND_OPS.get(cond.operator, lambda x, y: False)(target_rc, cond.code): return False return True def _run_cobol(self, step: JobStep) -> int: """Compile (if needed) and execute a COBOL program.""" cbl_path = self.cobol_dir / f"{step.program}.cbl" exe_path = self.bin_dir / f"{step.program}.exe" # Compile if source newer than binary if not exe_path.exists() or ( cbl_path.exists() and os.path.getmtime(cbl_path) > os.path.getmtime(exe_path) ): print(f" COMPILE: {cbl_path.name}") result = subprocess.run( ["cobc", "-std=ibm", "-x", str(cbl_path), "-o", str(exe_path)], capture_output=True, text=True, env=self._cobol_env(), ) if result.returncode != 0: print(f" COMPILE ERROR (RC={result.returncode}):") print(result.stderr[:500]) return result.returncode # Map DD entries to file paths stdin_file = None stdout_file = None env_map = {} for dd in step.dd_entries: dd_name_upper = dd.dd_name.upper() if dd_name_upper == "SYSOUT": if dd.sysout == "*": stdout_file = ( self.root_dir / "data" / "output" / f"{step.step_name.lower()}_sysout.txt" ) continue if dd_name_upper == "SYSIN" and dd.inline_data: # Write inline data to temp file tmp = tempfile.NamedTemporaryFile( mode="w", delete=False, suffix=".sysin", dir=self.root_dir / "data" / "work" ) for line in dd.inline_data: tmp.write(line + "\n") tmp.close() stdin_file = tmp.name env_map[dd_name_upper] = stdin_file continue if dd.dsn: # Map DSN to file path file_path = self._resolve_dsn(dd.dsn, dd.disp) env_map[dd_name_upper] = str(file_path) # Execute COBOL program print(f" EXECUTE: {exe_path.name}") env = self._cobol_env() env.update(env_map) stdin = None if stdin_file: stdin = open(stdin_file, "r") stdout = None if stdout_file: stdout = open(stdout_file, "w") try: result = subprocess.run( [str(exe_path)], stdin=stdin, stdout=stdout, stderr=subprocess.PIPE, text=True, env=env, cwd=str(self.root_dir), ) if result.stderr: print(f" STDERR: {result.stderr[:200]}") return result.returncode finally: if stdin: stdin.close() if stdout: stdout.close() def _run_sort(self, step: JobStep) -> int: """Execute SORT step (maps to GNU sort or PowerShell Sort-Object).""" sortin = None sortout = None sort_fields = None for dd in step.dd_entries: dd_name_upper = dd.dd_name.upper() if dd_name_upper == "SORTIN" and dd.dsn: sortin = self._resolve_dsn(dd.dsn, dd.disp) elif dd_name_upper == "SORTOUT" and dd.dsn: sortout = self._resolve_dsn(dd.dsn, dd.disp) elif dd_name_upper == "SYSIN" and dd.inline_data: sort_text = " ".join(dd.inline_data).upper() # Parse SORT FIELDS=(start,len,order,...) match = re.search( r"FIELDS=\s*\(([^)]+)\)", sort_text ) if match: sort_fields = match.group(1) if not sortin or not sortout: print(" ERROR: SORT requires SORTIN and SORTOUT DD") return 12 if sort_fields: # Parse sort fields for PowerShell Sort-Object fields = sort_fields.split(",") # fields: start,len,type,order,start2,len2,type2,order2 pscmd = f"Get-Content '{sortin}' | Sort-Object" i = 0 first = True while i + 3 < len(fields): start = int(fields[i].strip()) - 1 # 0-based length = int(fields[i + 1].strip()) order = fields[i + 3].strip() # skip type field (CH, PD, etc.) ascending = order.upper() != "D" if not first: pscmd += "," pscmd += ( f" {{$_.Substring({start},{length})}}" f"{'' if ascending else ' -Descending'}" ) first = False i += 4 pscmd += f" | Set-Content '{sortout}' -Encoding Ascii" else: pscmd = f"Get-Content '{sortin}' | Sort-Object | Set-Content '{sortout}' -Encoding Ascii" print(f" SORT CMD: {pscmd[:100]}...") result = subprocess.run( ["powershell", "-NoProfile", "-Command", pscmd], capture_output=True, text=True, ) if result.returncode != 0: print(f" SORT ERROR: {result.stderr[:200]}") return result.returncode def _resolve_dsn(self, dsn: str, disp: Optional[str] = None) -> Path: """Map z/OS DSN to Windows file path.""" # Handle GDG notation (simplified) dsn = re.sub(r"\(\+?\d+\)", "", dsn).strip(".") # If it's a z/OS DSN (no slashes, has dots as qualifiers), convert dots if "/" not in dsn and "\\" not in dsn: dsn = dsn.replace(".", "/") path = (self.root_dir / dsn.lstrip("/")).resolve() return path