Submitting jobs from Rexx in Unix Services

As part of using Slickedit and VScode to work on z/OS I wanted to be able to submit JCL from these tools and look at the output. With a bit of glue code it was pretty easy.

SlickEdit and VScode can use SSH to issue commands in Unix on z/OS.

Submitting the JCL

There are several ways of doing it

  • Using TSO SUBMIT to submit a file. It was not easy to tailor the file. Also you do not get back the Jobid, so you cannot easily retrieve the output.
  • Submitting from Rexx variables to the internal reader. This was OK.You do not get back the Jobid.
  • Using the Rexx submit command.

Submit to the internal reader

You do not get back the job id, so you do not know which is your job in the spool.

/* REXX */
"ALLOC FI(INTRDR) SYSOUT(A) WRITER(INTRDR) REUSE"
JOB.1="//COLIN JOB CLASS=A"
JOB.2="//TEST1 EXEC PGM=IEFBR14,REGION=0M"
JOB.3="//"
"EXECIO * DISKW INTRDR (FINIS STEM JOB."
"FREE FI(INTRDR)"
EXIT

Using the Rexx submit

This worked, and was easy. See the documentation.

For example

mysub.rexx

/* rexx */ 
parse arg input
say "*************************"input
rc=isfcalls('ON')
rc= syscalls('ON')
JOB.0=4
JOB.1="//COLINJ2 JOB "
JOB.2="//S1 EXEC PGM=IEFBR14"
JOB.3="//*"
JOB.4="//"
jobid = submit("JOB.")
say "submitted " jobid

You can tailor the JCL depending on your input parameter. For example invoke a JCL procedure, or explicitly build the JCL. For example pass the name of a member of C code in a PDS, and compile that member.

Retrieving the output

SDSF has some great facilities to retrieve the output. SDSF “owns” the prefix ISF. It uses the isf prefix to identify its variables.

If you use SDSF from a terminal you would use:

  • the STatus command to display the jobs you are interested in
    • You can filter what jobs you want displayed, for example COLIN*, or JOB98765,
  • Select the specific jobid
  • Use the ? line command to display the job output
  • Select the output file and browse it.

I use the Jobid because this should be unique. If you use job name, it is not unique (I have 20 jobs in the spool for the COLINCC job to compile a C program).

Selecting the ST display

isffilter = "JOBID = "jobid  /* isf filter=... */                                                               

/* Access the ST panel */
Address SDSF "ISFEXEC 'ST' ()"
lrc=rc

if lrc<>0 then do error handling.

The above code says filter on the specified jobid. You could specify ISFOWNER, and ISFPREFIX. See the documentation.

If you have filtered by jobname, for example COLINCC, there could be multiple instances, so you need to select the one(s) you are interested in, so you might just a well use the JobId from the start.

If the ISFEXEC ST command was successful, it returns variables: The count of rows, and details on each row. See the list of available columns/field names.

  • isfrows the number of rows
  • JobId.n is the job at the nth row
  • RETCODE.n this could be a number such as 0000 or JCL ERROR
  • PhaseName.n such as “AWAITING OUTPUT” or “EXECUTING”
  • Token.n uniquely identifies the job, file or spool output.
do i=1 to isfrows  /* Loop for all rows */ 
if ( JobId.i = jobid) then /* pick the ones of interest */
do 5 /* loop up to 5 times - while executing */
say "PhaseName" PhaseName.i
say "RETCODE" RETCODE.i
if RETCODE.i ="JCL ERROR" then
do
call "./readspool" TOKEN.i "JESYSMSG.JES2. JESMSGLG.JES2. JESJCL.JES2 "
leave
end
if PhaseName.i ="AWAITING OUTPUT" then /* it completed */
do
call "./readspool" TOKEN.i rest /* Rest is the list of output files */
leave
end
/* if PhaseName.i ="EXECUTING" then */
call sleep 5 /* wait 5 seconds before retry */
end /* do 5 */
end /* For all rows */


To display files of interest… call the external Rexx readspool ( the Rexx program is called readspool without a suffix).

readspool – display the contents of the spool file

Process each data set, if it matches the passed names, then display the contents. In my program a names is DDNAME, StepName and ProcStep concatenation with ‘.’.

SDSD Job Dataset Display panel has columns

COMMAND INPUT ===>              
NP DDNAME StepName ProcStep
JESJCL JES2
JESMSGLG JES2
JESYSMSG JES2
SYSCPRT COMPILE COMPILE
SYSOUT COMPILE COMPILE
SYSPRINT COMPILE BIND
SYSPRINT RUN

To display the JESYSMSG output use “JESYSMSG.JES2.” to look at the output from the C bind use “SYSOUT.COMPILE.BIND”

The token to uniquely identify the job is passed. Use the SDSF parm “?” to get access to the data sets.

/* rexx */ 
rc=isfcalls('ON')
rc= syscalls('ON')
parse arg token rest
Address SDSF "ISFACT ST TOKEN('"token"') PARM(NP ?)( prefix jds_)"
lrc=rc
if lrc<>0 then do error handling.

This gets you into the Job Data Set panel. There are many fields you can use relating to each data set. The (prefix jds_) puts jds_ before each variable so it does not overwrite existing symbols. So to reference the DDNAME.n you specify jds_DDNAME.n

Each data set will have

  • TOKEN.n a token to uniquely identify it.
  • STEPN.n the job step name
  • PROCS.n the procedure step name
/* go through each spool record and see if it in the list we were passed */ 
do jx=1 to jds_DDNAME.0
/* build up the list of matching names */
which = jds_DDNAME.jx||"."||jds_STEPN.jx ||"."||jds_PROCS.jx
if wordpos(which,rest) > 0 then /* was it passed in ? */
do
Address SDSF "ISFACT ST TOKEN('"jds_TOKEN.jx"') PARM(NP SA)"
lrc=rc
if lrc<>0 then do error handling.

/* Read the records from the data set and list them. */
/* The ddname for each allocated data set will be in */
/* the isfddname stem. Since the SA action was done */
/* from JDS, only one data set will be allocated. */
do kx=1 to isfddname.0
ADDRESS MVS "EXECIO * DISKR" isfddname.kx "(STEM line. FINIS"
Say "==="Which"===="
do lx = 1 to line.0
say line.lx
end
Say " "
end
end
return 0

The EXECIO reads from the DDName, into the stem variable line., and the data is then written to the terminal.

Note: When running the script in Unix, it needed ADDRESS MVS “EXECIO * DISKR”. Without it I got

FSUM7332 syntax error: got (, expecting Newline

Which usually indicates a code page problem.

Example output

The command

./mysub.rexx

submitted a simple IEFBR14 job. It produced

*************************                                                                                    
submitted JOB07229
PhaseName AWAITING OUTPUT
RETCODE CC 0000
===JESMSGLG.JES2.====
1 J E S 2 J O B L O G -- S Y S T E M S 0 W 1 -- N O D E
0
07.24.52 JOB07229 ---- FRIDAY, 25 APR 2025 ----
07.24.52 JOB07229 IRR010I USERID COLIN IS ASSIGNED TO THIS JOB.
07.24.52 JOB07229 ICH70001I COLIN LAST ACCESS AT 07:24:13 ON FRIDAY, APRIL 25, 2025
07.24.52 JOB07229 $HASP373 COLINJ2 STARTED - INIT 1 - CLASS A - SYS S0W1
07.24.52 JOB07229 IEF403I COLINJ2 - STARTED - TIME=07.24.52
07.24.52 JOB07229 - -----TIMINGS (MINS.)----
-
07.24.52 JOB07229 -STEPNAME PROCSTEP RC EXCP CONN TCB SRB
S
07.24.52 JOB07229 -S1 00 6 0 .00 .00 .0
0
07.24.52 JOB07229 IEF404I COLINJ2 - ENDED - TIME=07.24.52
07.24.52 JOB07229 -COLINJ2 ENDED. NAME- TOTAL TCB CPU TIME= .00
07.24.52 JOB07229 $HASP395 COLINJ2 ENDED - RC=0000
0------ JES2 JOB STATISTICS ------
- 25 APR 2025 JOB EXECUTION DATE
- 4 CARDS READ
- 41 SYSOUT PRINT RECORDS
- 0 SYSOUT PUNCH RECORDS
- 6 SYSOUT SPOOL KBYTES
- 0.00 MINUTES EXECUTION TIME

With thanks to Rob Scott and Dave Crayford for opening my eyes as to how easy it is, and for help in the basics. A lot of code was generated from SDSF using the RGEN facility.

What now?

Having displayed the job output, you could include a step to delete the job.

How to get a file from z/OS to a different z/OS without using FTP

I have a userid on a z/OS production system, which does not support FTP. To run my tests, I needed to get some files on to this system. Getting the files there was a challange.

The 3270 emulator has support for transferring files. It uses the IND$FILE TSO command to send data packaged as 3270 datastream As far as I can tell, this only works with data sets, not Unix files.

Creating a portable file from a data set.

You can package a data set into a FB Lrecl 80 dataset using the TSO XMIT (TRANSMIT) command.

Create a portable dataset from a Unix file.

On my home system I created a PAX dataset from a file in a Unix directory.

Use cd to get into the directory you want to package. If you specify a file name like /tmp/mypackage, the unpax will store the output in /tmp/mypackage which may not be where you want to store the data.

If you use relative directories such as ‘.’ it will unpax into a relative directory. I used the cd command to get into my working directory

pax -W "seqparms='space=(cyl,(10,10))'" -wzvf  "//'COLIN.ZOWE.PAX'" -x os390  myfile

You need both the single and double quotes around the data set name.

This created a data set with record format FB, and Lrecl 80.

A 360 MB file became a 426 CYL data set.

If you run out of space ( B37-04 abend). Delete the dataset before you reissue the pax command, otherwise the space parameters on the pax command are ignored; and increase the amount of space in the pax command.

I FTPed this down to my Linux machine in binary mode.

Send the file to the remote z/OS over 3270 emulator

Because FTP was not available I had to use the TSO facility IND$FILE. One of the options from the “file” menu was “File Transfer”.

You fill in details of the local file name, the remote data set name, and data set attributes.

In theory you need to be in TSO option 6 – where you can enter TSO commands, but when I tried this I kept getting “input field too small”. I had to exit ISPF and get into native TSO before the command worked.

The transfer rate is very slow. It sends one block at a time, and waits for the acknowledgement. With TCP/IP you can send multiple blocks before waiting for the ack, and use big blocks. For a 300MB file, I achieved 47KB per second with a 16000 block size – so not very high.

With IND$FILE, pick the biggest block size you can. I think it supports a maximum size of 32767. I got 86 KB/second with a 32767 block size with DFT mode.

For a dataset packaged with TSO XMIT

Use the TSO command RECEIVE INDSN(…) to restore the data set.

Un PAX the file to recreate it

On the production system, I use went into Unix, and used the cd command to get to the destination directory.

pax -ppx -rf  "//'COLIN.ZOWE.PAX'"      

Tailoring ISPF on a guest machine

I’ve got access to a “production” z/OS machine, and I want to customise ISPF to include my clists, and ISPF panels. How do I do this?

I covered some of the details in Configuring ISPF for new applications.

The first step

The most useful command is ALTLIB

"altlib activate application(exec) dataset ('COLIN.CLIST')" 

This allows you to insert your data set ‘COLIN.CLIST’ at the front of the search chain for EXEC (REXX) commands.

You can use the command ALTLIB DISPLAY. This gave me

Current search order (by DDNAME) is:
Application-level EXEC DDNAME=SYS00053
System-level EXEC DDNAME=SYSEXEC
System-level CLIST DDNAME=SYSPROC

You can now issue commands from the specified dataset.

Note: The ALTLIB only applies to the current ISPF session. If you have multiple ISPF sessions, you will need to do it in each session.
If you create new sessions ( START ) it will inherit from the the current session.

Can I do this automatically?

I can manually issue the ALTLIB command. If the systems programmer adds the following to the TSO logon procedure

/* REXX */ 
trace r
/* your userid is prefixed to any data set unless you double quote it */
if (sysdsn('CLIST') = 'OK')
then
do
"altlib activate application(exec) dataset (CLIST)"
if (sysdsn("CLIST(USERPROF)") = 'OK' )
then "USERPROF"
end
return 0

It will automatically issue the ALTLIB command, if the userid.CLIST data set exists, and it there is a member USERPROF in the data set, it will execute that.

To configure ISPF applications you can use the LIBDEF command. See Configuring ISPF for new applications.

Creating a CBT file

The CBT package is a collection of useful programs which enhance z/OS or make it easier to use. For example the PDS utility is like ISPF 3.4 on steroids. These programs have been collected for many years. Some are written in assembler (from before the time when C or COBOL were generally available), some are written in Rexx, many are new.

Some customers will accept tools from CBT, when they would not accept programs from Github.

This blog post is a guide to creating a package for inclusion in the CBT.

There is some documentation here. And there is a good article Packaging z/OS Open Source (and other) Software for Electronic Distribution by Lionel B Dyck.

The basic package is a PDS. It has a number to identify it. My package (zWireshark) was allocated the number 1063.

I created a PDS COLIN.FILEnnnn.

You should create the following members

@FILnnn

This is a description of the what the package does.

$CHANGES

This contains a change history

***********************************************************************
* *
* C H A N G E L O G *
* ------------------- *
* *
* DATE DESCRIPTION *
* ---------- ------------------------------------------------------ *
* *
* 2025/04/17 V1.0 First version on CBT *

$README

Introduction and instructions on how to use the package.

$RECEIVE

This has the JCL to unpack the package

//COLINR JOB (CCMVS),RECEIVE,                             
// NOTIFY=&SYSUID,
// CLASS=B,MSGCLASS=X,COND=(1,LT)
//*
//* CREATE NECESSARY PARTITIONED DATASETS
//* FOR ZWIRESHARK PACKAGE.
//*
//* (RENAME DATASETS AS PER YOUR INSTALLATION)
//*
//RECEIVE EXEC PGM=IKJEFT01
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
RECEIVE INDS('COLIN.CBT509.FILEnnnn(XMITJCLC)')
DSN('COLIN.ZWIRESHA.JCL')
RECEIVE INDS('COLIN.CBT509.FILEnnnn(XMITLOAD)')
DSN('COLIN.ZWIRESHA.LOADLIB')
/*
//

Your package content

You need to add the files for your package. The files will be record format FB with record length 80. If your file is not in this format you can use the TSO TRANSMIT (XMIT) command to make a portable member from your dataset. See MAKEXMIT below.

MAKEXMIT

This has the JCL I used to create the members of the package

//COLINX   JOB 1,MSGCLASS=H                                    
//S1 EXEC PGM=IKJEFT01,REGION=0M
//SYSPRINT DD SYSOUT=*
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
xmit a.a dsn('colin.ZWIRESHA.LOAD') OUTFILE(XMITL)
xmit a.a dsn('COLIN.C.ZWIRESHA') OUTFILE(XMITC)
/*
//XMITL DD DISP=SHR,DSN=COLIN.CBT509.FILEnnnn(XMITLOAD)
//XMITC DD DISP=SHR,DSN=COLIN.CBT509.FILEnnnn(XMITJCLC)
/*
/*

XMITJCLC

I used the MAKEXMIT member to convert the JCL and C file into a portable XMIT file with format FB LRECL 80 in the PDS. This member is the XMITted file

XMITLOAD

I used the MAKEXMIT member to convert the load library into a portable XMIT file with format FB LRECL 80 in the PDS. This member is the XMITted file.

Create the shippable object

In TSO

xmit a.a  dsn('COLIN.FILEnnnn')  OUTFILE('COLIN.FILEnnnn.XMIT')

FTP the COLIN.FILEnnnn.XMIT to my workstation in binary.

Send the file to CBT.

Why can’t I connect my something to my laptop over Ethernet?

I was failing to connect a Wi-fi repeater to my laptop via Ethernet. It is a very simple device. It about the size if a plug, and says connect to 192.168.11.1. I did, and it didn’t connect.

Once I spotted the problem, it was obvious.

On Linux, I had to configure the wired connection so support this address. Under IPv4, I added

Routes
192.168.11.1 | 255.255.255.0 | 10.1.0.2

and it all worked.

Simple when you know how!

Chaining through control blocks with C. The terrible, the right and the difficult.

The terrible

I have some terrible old C code for chaining through z/OS control blocks, which I wrote when I was first learning C.

 #define FLTCVT     16L 
#define CVTASCBH 564L
#define ASCBJBNI 172L
#define ASCBJBNS 176L
char *plStor = (char*)FLTCVT;
char *plCVT = (char*)*(long*)plStor;
char *plASCB = (char*)*(long*)(plCVT+CVTASCBH);
char *pJBNI = (char*)*(long*)(plASCB+ASCBJBNI);
char *pJBNS = (char*)*(long*)(plASCB+ASCBJBNS);

if( pJBNI != 0 ) // pointer to job name
printf("JobnameI %8.8s\n",pJBNI);

if( pJBNS != 0 ) // pointer to started task
printf("JobnameS %8.8s\n",pJBNS);

This prints out the job name. This has worked, and is not the world’s best C code. *(long*)plStor says take the value of (*) plStor, treating it as a pointer to a long ((long *)).

It needs to have “char *plCVT” so when I add an offset, the offset units is char. If I had had long * plCVT I would have to use offset CVTASCBH/4 instead of CVTASCBH to give the correct offset. The offset of CVTASCBH chars is 564. The offset of CVTASCBH longs is 2256.

Chaining – the right way

In the code above I specified offsets. This is not best practice. It is better to use header files as it makes the code easier to understand – and future proof.

#include <cvt.h> 
#include <ihaascb.h>
struct cvtmap * cvt = *((struct cvtmap **)0x10);
struct ascb * pASCB = (struct ascb *)(pCVT -> cvtascbh);
char * pName = pASCB -> ascbjbns;
printf("name %8.8s\n",pName);

The “*((struct cvtmap **)0x10)” code does not feel very elegant, but that’s C for you. Thanks to David Crayford for improving my C code. Who also said

It’s good practice to always code for 64 bit, even if you compile 31bit.

The above code is right – but it can be done in one instruction (which may not be as clear).

pName = 
((struct ascb *) ((struct cvtmap *) *((struct cvtmap **)0x10) ) -> cvtascbh) -> ascbjbns;
111111111111111 11111111111111111 111111111111111111
2222222222222222222222222
333333333333333333333333333333333333333333333
444444444444444444444444444444444444444444444444444444444444444444444444444

Where the 1,2,3 are the extent of the parenthesis. This is harder to understand than the previous example.

You can do

typedef struct ascb  *  ASCB; 
typedef struct cvtmap * CVT;
pName = ((ASCB) ((CVT) *((struct cvtmap **)0x10) ) -> cvtascbh)-> ascbjbns;

or

#define zASCB ( struct ascb  *) 
#define zCVT ( struct cvtmap *)
pName = (zASCB (zCVT *((struct cvtmap **)0x10) ) ) > cvtascbh)-> ascbjbns;

But I do not think these are as clear as the first examples (too many brackets for one thing).

Some clever code – which you should not use

I saw some “clever” code chaining along control blocks.

char * p = (char * )((int * __ptr32 * __ptr32 * )0)[4][165][53]; 

This is a good example of something which takes an expert seconds to write, but takes people not familiar with this a long time to understand.

The interpretation (thanks Bobby)

  • Consider address 0 as pointing to an array of 4-byte elements (the last ptr32), take the [4]th element, so at 16 bytes after 0, which is where CVT lives,
  • take that address and also consider that pointing to an array of 4-byte elements (the first ptr32), take the [141]th element, so (141* 4= )564 bytes beyond the start of the CVT, which is indeed CVTASCBH,
  • take that address and also consider that pointing to an array of integer elements, also 4 bytes, take the 44th element, so (44 * 4= )176 bytes beyond the start of the ASCB, which points to the jobname.

The generated code looks like

*    ppp = (char * )((int  * __ptr32 * __ptr32 * )0)[4][141][44]; 
5810 0010 L r1,16
5810 1234 L r1,(*)int*(,r1,564) 4 * 141
5800 10B0 L r0,(*)int(,r1,176) 4* 44
5000 D0A8 ST r0,ppp(,r13,168)

Changing it slightly

 *  char*   pppq= (char * )((char * __ptr32 * __ptr32 * )0)[4][141][44]; 
5810 0010 L r1,16
5810 1234 L r1,(*)uchar*(,r1,564) 4 * 141
E300 102C LLC r0,(*)uchar(,r1,44) 1 * 44 because it is a character
5000 D0AC ST r0,pppq(,r13,172)

This loads the single character at offset 44 (not 4 * 44 as it was for long *) and stores the single character. The offsets were of length 4.

If you know what you are doing the above code is compact and concise. For anyone else it could take hours to understand it. (I had to ask!)

If you make a small change it may not behave as you expect!

64 bit programs

The examples above were for 31 bit programs, referencing 31 bit addresses. You need to allow for 64 bit programs. You can specify an address is a 31 bit address by using the C qualifier __ptr32.

When compiled with 64 bit addressing the output is

ppp = (char * )((int  *        *         * )0)[4][141][44]; 
LG r6,32
LG r6,(*)int*(,r6,1128)
LGF r0,(*)int(,r6,176)
STG r0,ppp(,r4,2240)

Where some offsets are now 64 bit – and the CVT at offset 16 is now at offset 32 ( 4 * 8 byte longs) and so wrong.

You can force it to treat an address as 31 bit using

char * ppp = (char * )((int * __ptr32 * __ptr32   )0)[4][141]    ; 
LLGF r6,16
LLGF r6,(*)uchar*(,r6,564)
LLGC r0,(*)uchar(,r6,44)
STG r0,pppq(,r4,2240)

and it now treats the offsets as length 4.

You can use __ptr64 to say this is a 64 bit address. __ptr32 and __ptr64 both work in both 31 and 64 bit programs.