241 lines
8.4 KiB
Python
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
|