#!/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_port(host, port, timeout=30, poll_interval=1): """Wait for port to become available (not in use)""" print(f"Waiting for port {port} on {host} to be ready...") start_time = time.time() while time.time() - start_time < timeout: try: # Try to connect - if successful, port is available sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(2) result = sock.connect_ex((host, port)) sock.close() if result == 0: print(f"Port {port} is ready") return True else: print(f"Port {port} not responding (error {result}), waiting...") time.sleep(poll_interval) except socket.error as e: print(f"Socket error: {e}, retrying...") time.sleep(poll_interval) print(f"Timeout waiting for port {port} to be ready after {timeout}s") return False 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 [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/.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 port to be ready before submitting compile job if not wait_for_port(mvshost, RDRPORT, timeout=30): print(f"Error: Card reader port {RDRPORT} not available") sys.exit(1) # Step 2: Submit JCL job exit_code = submit_jcl(job, mvshost) sys.exit(exit_code)