Files

241 lines
8.4 KiB
Python

"""
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