Initial commit: COBOL+JCL credit card billing system with COMP-3, OCCURS, COPY REPLACING, INSPECT, and JCL runner
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user