tk5-basic-projects/scripts/submit_job.py

210 lines
7.5 KiB
Python
Executable File

#!/usr/bin/env python3
import sys
import subprocess
import tempfile
import os
import time
import socket
# Force temp files into a folder inside your project (fully owned by you)
custom_temp_dir = os.path.join(os.getcwd(), "tmp")
os.makedirs(custom_temp_dir, exist_ok=True)
tempfile.tempdir = custom_temp_dir
SRCLIB = "src"
JCLLIB = "jcl"
MVSHOST = "oldcomputernerd.com"
RDRPORT = 3505
MVS_PASSWORD = os.environ.get("MVS_BATCH_PASSWORD")
def wait_for_reader(host, port, wait_seconds=10):
"""
Wait for card reader to finish processing previous job.
Hercules keeps the socket in IO[n] open state while JES processes
the submitted job. We need to wait long enough for:
1. JES to read the job from the internal reader
2. Hercules to close the socket completely
3. The port to be ready for a new connection
A simple fixed delay is more reliable than trying to probe the port,
since the port will respond even when Hercules will reject connections.
"""
print(f"Waiting {wait_seconds} seconds for card reader to finish processing...")
for i in range(wait_seconds):
time.sleep(1)
if (i + 1) % 3 == 0:
print(f" {wait_seconds - i - 1} seconds remaining...")
print("Card reader should be ready now")
return True
def create_jcl_payload(local_file, dataset_name, member_name):
with open(local_file, 'r') as f:
sysin = f.readlines()
# PDS member: Use IEBUPDTE
jcl = f"""
//UPLOAD JOB (ACCT),'UPLOAD',
// USER=@05054,PASSWORD={MVS_PASSWORD},
// CLASS=A,MSGCLASS=H,NOTIFY=@05054
//COPY EXEC PGM=IEBUPDTE,PARM=NEW
//SYSPRINT DD SYSOUT=*
//SYSUT1 DD DUMMY
//SYSUT2 DD DSN={dataset_name},DISP=MOD,UNIT=SYSDA,
// DCB=(RECFM=FB,LRECL=80,BLKSIZE=0)
//SYSIN DD *
"""
# Append control statement, source lines, end, and terminator (no leading space on ./)
jcl += f"./ ADD NAME={member_name}\n"
for line in sysin:
line = line.rstrip('\n')
stripped = line.lstrip()
# Skip comment lines that would be interpreted as JCL
if stripped.startswith('//') or stripped.startswith('/*'):
continue
jcl += line[:80].ljust(80) + "\n"
jcl += "./ ENDUP\n"
jcl += "/*\n"
return jcl
def upload_source(local_file, dataset_name, member_name, mvshost=MVSHOST):
"""Upload source code to MVS PDS member"""
# Read the source file
# full path will come from the job runner
# filepath = os.path.join(SRCLIB, local_file)
payload = create_jcl_payload(local_file, dataset_name, member_name)
# Write JCL to temporary file and submit via netcat
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.jcl') as tmpfile:
tmpfile.write(payload)
tmpfile.flush()
tmpfile_path = tmpfile.name
try:
with open(tmpfile_path, 'rb') as f:
result = subprocess.run(
['nc', '-w', '5', mvshost, str(RDRPORT)],
input=f.read(),
check=True,
capture_output=True
)
print(f"Uploaded {local_file} to {dataset_name}({member_name})")
if result.stdout:
print("JES response:", result.stdout.decode(errors='ignore').strip())
return 0
except subprocess.CalledProcessError as e:
print(f"Upload failed: {e}")
print("stderr:", e.stderr.decode(errors='ignore'))
return 1
finally:
# Clean up outside
os.unlink(tmpfile_path)
def submit_jcl(job, mvshost="oldcomputernerd.com"):
"""Submit JCL job from local directory"""
subjcl = os.path.join(JCLLIB, f"{job.upper()}.jcl")
if not os.path.exists(subjcl):
print(f"JCL file {subjcl} not found")
return 1
try:
# Read the JCL file and send via netcat (same approach as upload_source)
with open(subjcl, 'rb') as f:
jcl_data = f.read()
print(f"Submitting {len(jcl_data)} bytes of JCL to {mvshost}:{RDRPORT}")
result = subprocess.run(
['nc', '-w', '5', mvshost, str(RDRPORT)],
input=jcl_data,
check=True,
capture_output=True
)
print(f"Submitted JCL job: {job}")
if result.stdout:
print("JES response:", result.stdout.decode(errors='ignore').strip())
if result.stderr:
print("netcat stderr:", result.stderr.decode(errors='ignore').strip())
if result.returncode != 0:
print(f"WARNING: netcat returned non-zero exit code: {result.returncode}")
return 1
return 0
except subprocess.CalledProcessError as e:
print(f"ERROR: JCL submission failed with exit code {e.returncode}")
print("stderr:", e.stderr.decode(errors='ignore'))
print("stdout:", e.stdout.decode(errors='ignore'))
return 1
except FileNotFoundError as e:
print(f"Error reading JCL file: {e}")
return 1
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: mvs_job.py <local_source_file> [destination_pds] [mvshost]")
print()
print("Arguments:")
print(" local_source_file - Path to source file (required)")
print(" destination_pds - PDS destination as DATASET(MEMBER) (optional)")
print(" Default: @05054.C90.SOURCE(basename)")
print(" mvshost - MVS host (optional, default: oldcomputernerd.com)")
print()
print("Examples:")
print(" mvs_job.py src/sieve11.c")
print(" mvs_job.py src/sieve11.c '@05054.C90.SOURCE(SIEVE11)'")
print(" mvs_job.py src/hello.c '@05054.C90.SOURCE(HELLO)' mainframe.example.com")
print()
print("Notes:")
print(" - JCL file is assumed to be jcl/<basename>.jcl")
print(" - Member name defaults to source filename without extension")
sys.exit(1)
local_file = sys.argv[1]
# Extract base name without extension for defaults
basename = os.path.splitext(os.path.basename(local_file))[0].upper()
valid_host_source_pds_suffixes = ['C', 'ALG', 'ASM', 'BAS', 'COB', 'PAS', 'PL360']
default_suffix = valid_host_source_pds_suffixes[0]
# Parse destination PDS (optional second argument)
if len(sys.argv) > 2 and sys.argv[2]:
destination = sys.argv[2]
# Parse PDS syntax: DATASET(MEMBER)
if '(' in destination and destination.endswith(')'):
dataset_name = destination[:destination.index('(')]
member_name = destination[destination.index('(')+1:-1]
else:
print(f"Error: Invalid PDS syntax '{destination}'. Use format: DATASET(MEMBER)")
sys.exit(1)
else:
# Default destination
dataset_name = f"@05054.SRCLIB.{default_suffix}"
member_name = basename.upper()
# JCL job name defaults to basename
job = basename.upper()
# Optional host override
mvshost = sys.argv[3] if len(sys.argv) > 3 else MVSHOST
print(f"Source: {local_file}")
print(f"Destination: {dataset_name}({member_name})")
print(f"JCL: jcl/{job}.jcl")
print(f"Host: {mvshost}")
print()
# Step 1: Upload source to PDS
if upload_source(local_file, dataset_name, member_name, mvshost) != 0:
sys.exit(1)
# Wait for card reader to finish processing upload job before submitting compile job
# This prevents "device busy or interrupt pending" errors from Hercules
wait_for_reader(mvshost, RDRPORT, wait_seconds=10)
# Step 2: Submit JCL job
exit_code = submit_jcl(job, mvshost)
sys.exit(exit_code)