What DBB APIs issue z/OS commands
Java and Groovy both provide easy mechanisms to create directories, copy files and even invoke external programs. However, these mechanisms are only for use with distributed operating systems such as Unix, Linux and Windows that use hierarchical file systems (HFS). There is usually no native support for working with z/OS datasets or invoking z/OS programs and commands. DBB provides the following set of Java classes to be used for issuing z/OS commands:
- CreatePDS
- CopyToPDS
- CopyToHFS
- MVSExec
- ISPFExec
- TSOExec
- JCLExec
This section of documentation is meant to provide an overview of these commands as well as tips on how to use them in your build scripts. For a more detailed look at the Java API specifics for each command, see the Toolkit Javadoc.
All of the z/OS command Java classes are located in the com.ibm.dbb.build
package.
CreatePDS
This command is used to create a new cataloged partitioned data set (PDS). It does nothing if the data set already exists. The command has two attributes: the data set name and the creation options.
def newPDS = new CreatePDS()
newPDS.setDataset("USR1.SRC.COBOL")
newPDS.setOptions("cyl space(1,1) lrecl(80) dsorg(PO) recfm(F,B) dsntype(library)")
newPDS.execute()
The CreatePDS command also supports setter method chaining which provides a more compact and readable way to define a command for execution. For example:
new CreatePDS().dataset("USR1.SRC.COBOL").options("cyl space(1,1) lrecl(80) dsorg(PO) recfm(F,B) dsntype(library)").execute()
The CreatePDS command uses the BPXWDYN text interface to dynamically allocate a new cataloged data set. The options attribute must consist of valid BPXWDYN dynamic allocation options.
CopyToPDS
This command is used to copy source files, binary files and load modules from a zSeries File System (zFS) to a partition data set (PDS). The source of the command can be:
- a single zFS file
- a zFS directory
- a list of physical dependencies generated by the DBB DependencyResolver
The target of the command is a PDS. By default the copied PDS member name is derived from the source file name by removing any file extension and uppercasing the result. When copying a single file, an optional PDS member name can be set.
// Copy a single file
def copyFile = new CopyToPDS()
copyFile.setFile(new File("/u/usr1/build/helloworld.cbl"))
copyFile.setDataset("USR1.BUILD.COBOL")
copyFile.setMember("HELLO")
copyFile.execute()
// CopyToPDS supports setter method chaining
new CopyToPDS().file(new File("/u/usr1/build/helloworld.cbl")).dataset("USR1.BUILD.COBOL").member("HELLO").execute()
// Copy a entire directory to PDS
new CopyToPDS().file(new File("/u/usr1/src/cobol")).dataset("USR1.SRC.COBOL").execute()
//Copy resolved dependencies to PDS
def resolver = tools.getDefaultDependencyResolver(file)
def deps = resolver.resolve()
new CopyToPDS().dependencies(deps).dataset("USR1.SRC.COPYBOOK").execute()
The CopyToPDS class also has a createMemberName
(name) static method that can be used to create a PDS member name from a zFS file path. It can be useful when needing the member name of a copied file later in a build script.
def fileName = "/u/usr1/build/hello.cbl"
def dsName = "USR1.BUILD.COBOL"
def memberName = CopyToPDS.createMemberName(fileName) // memberName = HELLO
new CopyToPDS().file(new File(fileName)).dataset(dsName).member(memberName).execute()
. . .
compile.dd(new DDStatement().name("SYSIN").dsn("$dsName($memberName)").options("shr"))
The value of copyMode
can be TEXT
, BINARY
or LOAD
. If copyMode
is not specified, the default TEXT
is used. For more information, see setCopyMode(CopyMode copyMode) and copyMode(CopyMode copyMode).
Archive files support
You can also use CopyToPDS to copy files contained within archives. This is useful for copying binary or text files to a data set from archived or compressed archive files.
// copy individual binary file with standard member name
copy = new CopyToPDS()
copy.setDataset("USR.INDIV") // data set to copy to
copy.setArchive("archive/archive_bin.tar") // acrhive to copy from
copy.setArchivedFile("bin/EPSMLIST").copyMode(CopyMode.BINARY) // file in archive
copy.execute()
// CopyToPDS supports setter method chaining
cmd = new CopyToPDS().dataset("USR.INDIV").archive("archive/archive_bin.tar").archivedFile("bin/EPSMLIST").copyMode(CopyMode.BINARY)
cmd.execute()
Handling codepages
By default, the copied PDS member has the same codepage as the source zFS file. The code page of the file is determined in the following order:
-
The file encoding tag of the source file is used if present.
- Rocket's Git client automatically adds file encoding tags when cloning or pulling source files from a distributed Git server.
-
The ZLANG environment variable is used if set.
-
The default IBM-1047 codepage is used.
Additionally, you can manually set both the source file codepage and the target data set member codepage.
// Indicates that the file is encoded as IBM-037 and will write the PDS member as IBM-037
new CopyToPDS().file(new File(fileName)).hfsEncoding("IBM-037").dataset(dsName).member(memberName).execute()
If the PDS encoding differs from the HFS encoding, a codepage conversion is automatically performed during the copy process.
// Indicates that the file is encoded as UTF-8 and will write the PDS member as IBM-1047
new CopyToPDS().file(new File(fileName)).hfsEncoding("UTF-8").dataset(dsName).member(memberName).pdsEncoding("IBM-1047").execute()
Note: The PDS and HFS encoding options are applied only when the value of copyMode
is TEXT
. These values are ignored for BINARY
or LOAD
values.
CopyToHFS
This command is used to copy PDS members to zFS files. A common use of CopyToHFS command is to copy the SYSPRINT outputted from the COBOL compiler to a zFS directory as a log file so that you can view it without having to change environments.
// Copy a PDS member to zFS
def copyLog = new CopyToHFS()
copyLog.setDataset("USR1.BUILD.SYSPRINT")
copyLog.setMember("HELLO")
copyLog.setFile(new File("/u/usr1/build/helloworld.log"))
copyLog.execute()
// CopyToHFS supports setter method chaining
new CopyToHFS().dataset("USR1.BUILD.SYSPRINT").member("HELLO").file(new File("/u/usr1/build/helloworld.log")).execute()
CopyToPDS can also use as its source the name of an allocated data definition (DD) statement. This is useful to copy when copying temporary data sets to zFS.
new CopyToHFS().ddName("SYSPRINT").file(new File("/u/usr1/build/helloworld.log").execute()
The CopyToHFS command supports four modes of operation:
Copy Mode | Usage | Description |
---|---|---|
TEXT | .copyMode(CopyMode.TEXT) |
Default. Copy data set members to zFS files as text. |
ASA_TEXT | .copyMode(CopyMode.ASA_TEXT) |
Copy SYSPRINT data set members to zFS files as text preserving ASA carriage control charaters. |
BINARY | .copyMode(CopyMode.BINARY) |
Copy data set members to zFS files in binary mode. |
LOAD | .copyMode(CopyMode.LOAD) |
This load module copy is similar to a BINARY copy but also preserves the metadata of the load module being copied and including ISPF Stats and ALIAS. |
For more information, see setCopyMode(CopyMode copyMode) and copyMode(CopyMode copyMode).
Handling Codepages
By default the copied zFS file will have the same codepage as the source PDS member or data set. The code page of the dataset is determined in the following order:
- The ZLANG environment variable is used if set.
- The default IBM-1047 codepage is used.
Additionally both the source data set and the target zFS codepage can be manually set by the user. When using DBB in conjunction with a Jenkins CI build server, a common practice is to convert the SYSPRINT files to UTF-8 when they are copied to zFS in order that they can display correctly in Jenkin build result dashboards.
new CopyToHFS().ddName("SYSPRINT").file(new File("/u/usr1/build/helloworld.log").hfsEncoding("UTF-8").execute()
Note: The PDS and HFS encoding options are only applied when the value for copyMode
is TEXT
. These values are ignored for BINARY
or LOAD
values.
MVSExec
This command is used to invoke z/OS programs such a compilers, link editors, pre-compilers. It has similar characteristics of a JCL EXEC statement:
def zoscmd = new MVSExec()
zoscmd.setPgm("WKLYRPT")
zoscmd.setParm("COL=3")
def rc = zoscmd.execute()
The MVSExec command execute method returns the standard RC of the command as an int
value that can be evaluated to determine the success or failure of the command. When rewritten using setter method chaining, you can begin to see the
similarities between a JCL EXEC statement and the MVSExec command:
// JCL
//STEP1 EXEC PGM=WKLYRPT,PARM='COL=3',COND=(8,LE)
// DBB
def step1 = new MVSExec().pgm("WKLYRPT").parm("COL=3").execute()
if (step1 > 8) System.exit(1)
The above example is simplistic in that most z/OS programs also require data definition (DD) statements to define the data sets that the program uses when it runs.
DDStatement
The DDStatement class is a utility class used by the three Exec commands (MVSExec, ISPFExec, TSOExec) to define data definition (DD) statements that will automatically be allocated before and freed after the program or command is executed.
// Delete a dataset by calling IEFBR14
def delete = new MVSExec().pgm("IEFBR14")
def oldDS = new DDStatement()
oldDS.setName("OLDDS")
oldDS.setDsn("USR1.BUILD.COBOL")
oldDS.setOptions("old delete")
delete.addDDStatment(oldDS)
def rc = delete.execute()
// Using setter method chaining
def delete = new MVSExec().pgm("IEFBR14")
delete.dd(new DDStatement().name("OLDDS").dsn("USR1.BUILD.COBOL").options("old delete"))
def rc = delete.execute()
The four main parts of a DD statement are:
- DD Name (name) - The data definition name. Can be omitted (see DD Concatenation below).
- Dataset Name (dsn) - The dataset to be allocated for the program to read or write. Can be omitted (see Temporary Datasets below).
- Path (path) - The path name of a file to allocate. Mutually exclusive to DSN. Can be omitted.
- BPXWDYN Options (options) - The DDStatement uses the BPXWDYN text interface to dynamically allocate datasets during the build. The options attribute must consist of valid BPXWDYN dynamic allocation options.
DD Concatenation
Many z/OS compilers, linkers will require one or more data sets to be concatenated together to form a library to be searched for referenced modules needed by the z/OS program. In JCL, this is done by creating a DD concatenation.
// JCL
//SYSLIB DD DSNAME=USR1.OBJ,DISP=SHR
// DD DSNAME=CEE.SCEELKED,DISP=SHR
// DD DSNAME=DFH.CICS.SDFHLOAD,DISP-SHR
In DBB there are two ways to define a DD concatenation:
-
Create a parent DDStatement and manually concatenate additional DDStatements to it.
def linker = new MVSExec().pgm("IEWBLINK") def syslib = new DDStatement().name("SYSLIB").dsn("USR1.OBJ").options("shr") syslib.concatenate(new DDStatement().dsn("CEE.SCEELKED").options("shr")) syslib.concatenate(new DDStatement().dsn("DFH.CICS.SDFHLOAD").options("shr")) linker.dd(syslib)
-
Adding a DDStatement without a DD name to a MVSExec command will automatically concatenate it to the last DDStatement added with a DD name.
def linker = new MVSExec().pgm("IEWBLINK") linker.dd(new DDStatement().name("SYSLIB").dsn("USR1.OBJ").options("shr")) linker.dd(new DDStatement().dsn("CEE.SCEELKED").options("shr")) linker.dd(new DDStatement().dsn("DFH.CICS.SDFHLOAD").options("shr"))
TASKLIB vs STEPLIB
A major difference between the JCL EXEC statement and the DBB MVSExec command is that the JCL STEPLIB has been replaced with a TASKLIB in DBB. This is because DBB runs as a Java application in a zOS UNIX subsystem where the STEPLIB is defined as an environment variable that cannot be modified during the DBB build process. Additionally DBB invokes z/OS programs using the ATTACHX assembler macros. The ATTACHX macro uses the TASKLIB as its searchpath to locate the program being invoked.
// JCL
//COBOL EXEC PGM=IGYCRCTL,REGION=0M,PARM='LIB'
//STEPLIB DD DISP=SHR,DSN=IGY.SIGYCOMP
// DBB
def cobol = new MVSExec().pgm("IGYCRCTL").parm("LIB")
cobol.dd(new DDStatement().name("TASKLIB").dsn("IGY.SIGYCOMP").options("shr"))
Temporary data sets
Temporary data sets are data sets that are created when they are allocated and deleted when they are freed. In DBB a temporary data set is created by defining a DDStatement with no data set name (DSN) or providing a data set name that begins with "&&".
// Create temporary datasest that exists only for the length of the MVSExec command
compile.dd(new DDStatement().name("SYSUT1").options("cyl space(5,5) unit(vio) new"))
compile.dd(new DDStatement().name("SYSLIN").dsn("&&TEMPOBJ").options("cyl space(5,5) unit(vio) new"))
Passing temporary data sets
In some cases it is useful to have a temporary data set continue to exist after the MVSExec command completes to be used by another MVSExec command. This is done by using the pass
option when declaring the DDStatement which will prevent
the MVSExec command from freeing the DD statement upon completion.
compile.dd(new DDStatement().name("SYSLIN").dsn("&&TEMPOBJ").options("cyl space(5,5) unit(vio) new").pass(true))
After the compile MVSExec command completes, the SYSLIN DD allocation still exists and can be used by following MVSExec commands such as the linker which requires a SYSLIN allocation as input.
def linkedit = new MVSExec().pgm("IEWBLINK").parm("MAP,RENT,COMPAT(PM5)")
The pass option only applies to DD statements with temporary data sets. When using the pass option, you need to provide a data set name for temporary data sets. For permanent data sets it is ignored if set and the DD allocation will be freed at the end of the MVSExec command as normal.
IMPORTANT Passed DD statements can cause a problem if not used in conjunction with the MVSJob utility class.
Free temporary DDs allocated by REXX and C compilers
Some compilers such as REXX and C often allocate temporary DDs when certain files are being included. These temporary DDs are not freed automatically at completion of the compiler run. Beginning from DBB V1.0.3, MVSExec has a freePgmAllocatedDDs()
option that you can use to free these DDs when a complier run completes. The freePgmAllocatedDDs() option triggers a comparison of all the DDs before and after the compiler is called. If some DDs are detected to be not freed by the compiler, specifying
the freePgmAllocatedDDs() option frees those DDs.
MVSJob
In JCL a passed DD statement is automatically freed at the end of the JCL job. However in DBB this would not happen until the entire build process finishes. This can cause a problem when the same called build script is invoked repeatedly while building
a list of programs of the same type. The second time the script is invoked, it would fail due to a BPXWDYN allocation error. Probably either a 0x410 - Specified ddname unavailable
or a 0x420 - Specified ddname or dsname associated with an open data set
.
DBB provides MVSJob, which frees DDs until the end of MVSJob, so that you can use MVSJob to define the scope of the passed DD statement. This is useful with temporary data sets. Moreover, you can embed several MVSExec commands in one MVSJob.
// use MVSJob start and stop commands to handle passed DDs
def job = new MVSJob()
job.start()
rc = compile.execute()
if (rc <= 4)
rc = linkedit.execute()
job.stop()
When the MVSJob start method is invoked all passed DDStatements with temporary data sets are registered in a table when allocated and will remain allocated until the MVSJob stop method is invoked in which case all of the DDStatements in the table are freed. There is no limit or restriction of how many or what type of statements can be coded between the MVSJob start and stop method invocations. All statements are ignored except MVSExec statements.
MVSJob also has an executable mode that uses setter method chaining to add several MVSExec commands to the MVSJob and then execute the entire MVSJob. An explicit start() is invoked at the beginning of the job execution and an implicit stop() is invoked at the end of the job.
// Build a BMS map
def copybookGen = new MVSExec().file(file).pgm("ASMA90").parm("SYSPARM(DSECT),DECK,NOOBJECT")
. . .
def compile = new MVSExec().file(file).pgm("ASMA90").parm("SYSPARM(MAP),DECK,NOOBJECT")
. . .
def linkedit = new MVSExec().file(file).pgm("IEWBLINK").parm("MAP,RENT,COMPAT(PM5)")
. . .
def rc = new MVSJob().executable(copybookGen).executable(compile).executable(linkedit).maxRC(0).execute()
In the above example, the three MVSExec commands are executed in the order they are added to the MVSJob. An optional maxRC can be added that will cause the job to terminate early if any MVSExec command returns an RC greater than the maxRC.
Multi-thread support with MVSJob
Executing two or more MVSExec commands at the same time in a multi-thread environment often requires the ability to concurrently allocate the same DD name. This is generally not possible when running the commands in a single JVM because they use
the same address space and will often lead to an error when a second process tries to allocate the same DD that was already allocated by the first process. Beginning with v1.0.2, DBB provides a new Java option DBB_SUBPROCESS_ENABLED
that when set to true
will cause all MVSExec executions and DD allocations performed as part of a MVSJob to be done in a separate address space. The DBB_PROCESS_ENABLED
option can be set using Java -D option:
groovyz -DDBB_SUBPROCESS_ENABLED=true build.groovy
Note: You might have set the USS environment in such a way as to prevent processes from creating separate address spaces.
While this is not an issue when launching DBB build scripts from a Jenkins remote agent, it does impact DBB build processes launched from the command line. It is recommented to use the DBB groovyz shell script included in the DBB toolkit at /usr/lpp/IBM/dbb/bin/groovyz
to launch DBB build scripts from the command line as it has been modified to allow for the creation of additional address spaces.
Copying temporary data sets
As mentioned in the section covering the CopyToHFS command, it can be useful to copy the SYSPRINT output from compilers and linkers to zFS as log files to retain for later use. Instead of having to write the SYSPRINT file to a permanent data set, the CopyToPDS can also use as its source a DD statement name. Additionally declared CopyToHFS commands can be added to an MVSExec command in the same way the DD statements are added. These CopyToHFS commands are then executed as part of the MVSCommand execution.
def compile = new MVSExec().file(file).pgm("IGYCRCTL").parm("LIB")
compile.dd(new DDStatement().name("SYSPRINT").options("cyl space(5,5) unit(vio) new"))
compile.copy(new CopyToHFS().ddName("SYSPRINT").file(new File("/u/usr1/build/helloworld.log").hfsEncoding("UTF-8"))
When the MVSExec command is executed the following occurs:
- All DD statements are allocated
- The z/OS program is executed
- All CopyToHFS commands are executed
- All DD statements are freed except statements with temporary data sets marked as pass
In-stream data sets
An in-stream data set is a way to define input content to a z/OS program without having to read in a file or data set from disk.
// JCL
//SYSIN *
HELLO
/*
// DBB
new DDStatement().name("SYSIN").instreamData("HELLO")
Use Java newline characters to separate the String into multiple lines of text.
// JCL
//SYSIN *
RECORD1
RECORD2
RECORD3
/*
// DBB
def records = "RECORD1\nRECORD2\nRECORD3"
new DDStatement().name("SYSIN").instreamData(records)
You can also use triple quotes for this purpose:
// JCL
//SYSIN *
RECORD1
RECORD2
RECORD3
/*
// DBB
def records = '''RECORD1
RECORD2
RECORD3'''
new DDStatement().name("SYSIN").instreamData(records)
Note: In-stream data has a maximum line length of 80 characters. It is typical that column 1 for In-stream data would be a white space.
ISPFExec and TSOExec
These commands are used to run TSO or ISPF commands using the TSO/ISPF Client Gateway. They are combined into one section because other than the command names, the API for each are identical.
// copy a member from one dataset to another
def memberCopy = new TSOExec().command("OCOPY INDD(IN) OUTDD(OUT) TEXT CONVERT(YES) TO1047")
memberCopy.dd(new DDStatement().name("IN").dsn("USR1.BUILD.LOAD(EPSCMORT)").options("shr"))
memberCopy.dd(new DDStatement().name("OUT").dsn("USR1.BUILD.LOADBK(EPSCMORT)").options("shr"))
memberCopy.logFile(new File("/u/usr1/build/logs/EPSCMORT_ispf.log"))
int rc = memberCopy.execute();
Using ISPF Gateway
Setting up ConfDir
ISPFExec and TSOExec commands require location of the DBB configuration directory i.e. confDir which contains the location of the shell script used to communicate with the gateway. The easiest way to do this is to set the DBB_CONF
environment
variable. Beginning in v1.0.2 DBB requires two new environment variables: DBB_HOME
and DBB_CONF
in order to function correctly. For more information, see “Environment variables” in Installing and configuring toolkit on z/OS.
The confDir can also be set on the command itself. This will be used in place of the value in DBB_CONF
.
def memberCopy = new TSOExec().command("OCOPY INDD(IN) OUTDD(OUT) TEXT CONVERT(YES) TO1047")
memberCopy.confDir("/usr/lpp/IBM/dbb/conf")
Legecy vs Interactive gateway
DBB supports using both the Legacy ISPF Gateway and the newer Interactive ISPF Gateway. The type of gateway to be used must be specified by the user so that DBB will generate the correct gateway XML API. The easiest way to specify the gateway type is to set the DBB gateway type configuration property in a property file and read it in to the DBB BuildProperties at the beginning of the build process
# The dbb.gateway.type property determines which gateway type is used for the entire build process
# Possible values are 'legacy' and 'interactive. Default if not indicated is 'legacy'
dbb.gateway.type=interactive
The gateway type can also be set on the command itself:
def memberCopy = new TSOExec().command("OCOPY INDD(IN) OUTDD(OUT) TEXT CONVERT(YES) TO1047")
memberCopy.gatewayType("interactive")
Using the Legacy Gateway
Support for the Legacy Gateway requires DBB to generate and execute a temporary REXX script.
IMPORTANT! Users must include a DD allocation for CMDSCP with a valid DSN to store the temporary script that will be executed.
This script is automatically generated by DBB.
// legacy gateway commands require DD allocation CMDSCP
def memberCopy = new TSOExec().command("OCOPY INDD(IN) OUTDD(OUT) TEXT CONVERT(YES) TO1047")
memberCopy.dd(new DDStatement().name("CMDSCP").dsn("USR1.ISPFGWY.EXEC").options("shr"))
memberCopy.dd(new DDStatement().name("IN").dsn("USR1.BUILD.LOAD(EPSCMORT)").options("shr"))
memberCopy.dd(new DDStatement().name("OUT").dsn("USR1.BUILD.LOADBK(EPSCMORT)").options("shr"))
memberCopy.logFile(new File("/u/usr1/build/logs/EPSCMORT_ispf.log"))
int rc = memberCopy.execute();
Using the Interactive Gateway
As the name implies, the Interactive Gateway allows for users to execute programs that are interactive, using a conversational pattern. In order to facilitate interacting with conversational programs, DBB has added the following additional APIs to the ISPFExec and TSOExec commands:
getOutput()
- returns a String with the output from<tso/>
or<ispf/>
tags contained in the service response from the last execution of the command.- Note that getOutput() is applicable for both legacy and interactive gateway calls. For legacy calls, the last line containing the
ISPF_RETURN_CODE = 0
is removed from the output. - Since the returned output text from the gateway is embedded in a formatted XML document, additional white space such as beginning and ending newline characters as well as indentation characters will be included in the output text.
- Note that getOutput() is applicable for both legacy and interactive gateway calls. For legacy calls, the last line containing the
isWaitingForResponse()
- flag indicating that the command is waiting for a response from the usersetResponse(String response)
orresponse(String response)
- the value for the RESPONSE message to send to the gatewayexecute()
method is used to execute both the initial command as well as the follow up responses.cancel()
- used to cleanly terminate a command while it is still waiting for a response.
Example:
// HELLO Rexx script keeps prompting for a name
def hello = new TSOExec().command("exec 'USR1.REXX(HELLO)'")
hello.execute()
println hello.getOutput()
["John", "Paul", "George", "Ringo"].each { name ->
if (hello.isWaitingForResponse()) {
println name
hello.response(name).execute()
println hello.getOutput()
}
}
// if the hello program is still waiting for a response then cancel the session to end
if (hello.isWaitingForResponse()) {
println "Cancelling conversation"
hello.cancel()
}
Output:
What is your name?
John
Hello John
What is your name?
Paul
Hello Paul
What is your name?
George
Hello George
What is your name?
Ringo
Hello Ringo
What is your name?
Cancelling conversation
The interactive gateway requires additional configuration values to be set:
# Procedure Name - specified with the procname parameter
dbb.gateway.procedureName=
# Account number - specified with the acctnum parameter
dbb.gateway.accountNumber=
# Group name - specified with a groupid parameter
dbb.gateway.groupId=
# Region size - specified with the regionsz parameter
dbb.gateway.regionSize=
# Gateway logging level. Add values for multiple types:
# 1 - Log error information
# 2 - Log debug information
# 4 - Log communication information
# 8 - Log time information
# 16 - Log information to the system console
dbb.gateway.logLevel=2
Note: The interactive gateway support does not require the generation of a REXX program, so a CMDSCP
DD statement is not required.
Using ISPF gateway in multi-threaded mode
By default, DBB runs its build process in a single thread and therefore builds programs one at a time. This is not an issue for incremental builds or even full builds for small applications. However, when an application is comprised of hundreds of programs, it causes a full build to take hours to perform. You can speed up the build process by setting up a multi-threaded build; therefore, that multiple programs can build at the same time thus speeding up a full build process significantly.
To use TSOExec or ISPFExec to call the ISPF gateway in multi-threaded mode, complete the following steps:
- Copy ISPZXENV to a new location (such as
/etc/ispf
) - Update settings in ISPZXENV for
CGI_ISPPREF
- Add the location to your runIspf.sh PATH
- If necessary, update
DBB_CONF
orconfDir
to point to the modified runIspf.sh directory
Additional details can be found in the following sections.
Configuring ISPZXENV
ISPZXENV is the ISPF gateway environment file that contains customizable settings for the gateway.
The file is provided as part of z/OS. By default, this file is installed into /usr/lpp/ispf/bin
. This environment file is run by the ISPF gateway to get the value of certain environment variables necessary for the ISPF gateway to run.
Default variables include the following:
-
STEPLIB
By default it is set to
STEPLIB = 'ISP.SISPLPA:ISP.SISPLOAD'
Note: You can add more system load libraries to this STEPLIB allocation. For example, DB2.SDSNLOAD, if you have programs or executable files that start Db2 functions such as BIND.
-
CGI_ISPCONF
By default it is set to
CGI_ISPCONF = '/etc/ispf'
-
CGI_ISPWORK
By default it is set to
CGI_ISPWORK = '/var/ispf'
-
CGI_ISPPREF
By default it is set to
CGI_ISPPREF = '&SYSPREF..ISPF.VCMISPF'
The default directory that contains ISPZXENV (/usr/lpp/ispf/bin
) is read-only after installation. To set variables to non-default values, copy ISPZXENV to a writable configuration directory, such as /etc/ispf
, and then
set the variables to your required values.
When running in multi-threaded mode, several temporary ISPF data sets need to be created. The unique identifier that the ISPF gateway generates is not unique enough when running in multi-threaded mode. However there are some changes that can be
made to create a unique prefix that ISPF can use. In ISPZXENV, instead of setting the CGI_ISPPREF
environment variable to &SYSPREF..ISPF.VCMISPF
, it is possible to generate a prefix based on the PID (Process Identifier)
of the ISPF gateway process that is running. To do this, add the following code to replace the setting of CGI_ISPPREF
:
/* Running multithreaded we get issues with ISPF.VCMISPF not being unique */
/* Use a hex representation of the PID to uniquely create ISPF.VCMISPF */
address syscall 'getpid' ; pid = retval
Say 'Pid:'pid
If Length(Pid) > 7 Then
Do
CGI_ISPPREF = '&SYSPREF..P$'D2X(Substr(pid,1,5))||,
'.P$'D2X(Substr(pid,6))
End
Else
CGI_ISPPREF = '&SYSPREF..P'D2X(pid)
Update PATH in runIspf.sh
If you copied the ISPZXENV file to another directory and edited it, you must then change the PATH
environment variable in DBB’s runIspf.sh to point to the file. The PATH
variable must point to the ISPF gateway home directory.
By default the variable is set to export PATH=$PATH:/usr/lpp/ispf/bin
.
You might need to set it to the following:
export PATH=$PATH:/etc/ispf:/usr/lpp/ispf/bin
If necessary, you should also update DBB_CONF
or confDir
to point to the modified runIspf.sh file.
JCLExec
This command is used to submit a JCL job and retrieve the status and job outputs. The target JCL source could be located in a sequential data set, a PDS or PDSE member, a file on zFS, or dynamically created in the build script and passed in as a Java String.
Note: Similar to TSOExec and JCLExec, this command requires the location of the configure directory to locate the jobStatus REXX script. The command uses this REXX script to check the status of the submitted job.
Security implication: When using JCLExec to save job output to a file, sometimes the content of the file is empty. This could be caused by the fact that the current user does not have permission to look at jobs from another user,
or the job name does not start with the user ID. To get more details of the issue, you could enable security tracing by specifying -Dcom.ibm.dbb.SecurityTrace=true
.
Using SDSF function from a Java™ program is protected just as using SDSF interactively, or from a REXX exec, is protected, with the same SAF resources and ISFPARMS parameters.
Examples:
// declare the confDir location
def confDir = "/usr/lpp/IBM/dbb/conf"
// Execute JCL from a data set member
new JCLExec().dataset("USR1.JCL").member("TEST").confDir(confDir).execute()
// Execute JCL from a file on HFS
new JCLExec().file(new File("/u/usr1/jcl/test.jcl")).confDir(confDir).execute()
// Execute JCL from a String value in the script
def jcl = '''\
//HELLO JOB ,
// MSGCLASS=H,MSGLEVEL=(1,1),TIME=(,4),REGION=0M
//*
//* PRINT \"HELLO WORLD\" ON JOB OUTPUT
//*
//STEP0001 EXEC PGM=IEBGENER
//SYSIN DD DUMMY
//SYSPRINT DD SYSOUT=*
//SYSUT1 DD *
HELLO, WORLD
/*
//SYSUT2 DD SYSOUT=*
//
'''
new JCLExec().text(jcl).confDir(confDir).execute()
// Execute JCL and get output information
JCLExec jclExec = new JCLExec()
jclExec.dataset('USR1.JCL').member('TEST').execute()
def maxRC = jclExec.getMaxRC()
def jobID = jclExec.getSubmittedJobId()
def jobName = jclExec.getSubmittedJobName()
// Execute JCL and save the SYSPRINT output to zFS file
JCLExec jclExec = new JCLExec()
jclExec.dataset('USR1.JCL').member('TEST').execute()
jclExec.saveOutput('SYSPRINT', new File("/u/usr1/output/sysprint.out"), "UTF-8")