Showing posts with label ISPF-related. Show all posts
Showing posts with label ISPF-related. Show all posts

Monday, September 19, 2022

Breathing Room

 

When I left my last contract, I had an idea for software that was — to use an expression — 'half-baked'.  It was an idea for a problem reporting-responding system for your local friendly tools maven — someone like me.  Someone, perhaps, like you.  I thought there was a need for a vehicle for programmers — the folks who do the real work of producing software that generates the reports and files that management uses on a day-to-day basis to run the companies that produce the wealth that pays our salaries — to log that some particular tool exhibits unusual behavior or to suggest that some tool needs additional function.

The problem is that to develop mainframe software, one really needs a mainframe, and when I say 'mainframe', what I really mean is an 'IBM-ish operating system'.

There aren't too very many of those around, and what few there are are generally devoted to revenue-producing activity, as opposed to blue-sky and pie-in-the-sky ventures... like problem-reporting systems.

It's not easy to find such things — IBM-ish mainframes — but it's not impossible, either, and I managed to find one that didn't require me to pay $50/hr of computing time.

As a result, I've managed to get that problem reporting-responding system pulled together into a form that seems to me ready for some real-world testing.  So...

I have put 'FIXTOOL' out on my alternate source code page, http://frankclarke.dx.am/REXX/listindx.html, and now invite all and sundry to steal a copy and run it through its paces, reporting any errors back to me at my main email address.  You will also need to snag copies of several other routines that are needed in one way or another, and FIXTOOL also requires that SMTPNOTE (an IBM-originated routine) be active on your site.

  • Routine DFLTTLIB should be downloaded and adjusted to fit your local environment.  It designates the local default ISPTLIB dataset that will house all the ISPF tables used by my software.  Every user of FIXTOOL must be able to WRITE to its table.
  • Routine ICEUSER should be downloaded and customized to identify the 'special' users who are expected to manage the tools. 
  • Before running FIXTOOL, you must run TBLMSTR.  TBLMSTR will create an AA-form table (AAMSTR) with a single entry describing itself, and write that table onto the default ISPTLIB. 
  • You, the maintainer, must add a row onto AAMSTR for the IT-type table so that TBLGEN will know how to build it when ordered.  The easiest way to do this is to invoke FIXTOOL with the single parameter 'LOADQ'.  This only has to be done ONCE.  Subsequent invocations of FIXTOOL need not (should not) use LOADQ.

So, here is the component list of all the software that must be in place in order to run FIXTOOL: 

  • DFLTTLIB
  • FCCMDUPD
  • FIXTOOL
  • ICEUSER
  • LA
  • TBLGEN
  • TBLMSTR
  • TRAPOUT

Several of these routines reference SYSUMON, a usage logging routine that uses RXVSAM to update a KSDS where tool-usage statistics are kept.  You can either disable it or build the appropriate KSDS and let it count.  Up to you.

If you decide to fix any problems you find rather than have me fix them, I would still appreciate hearing from you what those problems were and how you addressed them.

 

Thursday, December 29, 2016

Saving Time; Saving Compiles

Let's face it:  programs fail, and if they're like my programs, they'll fail at the most inopportune time, generally around 2 in the morning.

A thought just occurred to me:  if you live or work in an environment where you are dependent on a compiler — a PL/I, COBOL, or ForTran environment, for instance — you need to be saving your compiler listings.  "OMG!  You can't be serious!  That would take up mountains of disk space!"  Yes, I'm serious.  The mountains of disk space are peanuts compared to the time your programmers will spend at 2am recreating — if it's even possible — a compiler listing from before the last change to the compiler.

When some piece of compiled code FOOPs in the wee hours, the last thing you need is to have to locate the code and compile it.  The compile listing you get from doing that may not, in fact, be an accurate representation of the load module that failed.  Comforting thought, no? 

The only plausible solution is to save the compiler listing for the module you will eventually roll into production.  Whenever you last produce a load module that is destined for production, that compiler listing must be saved to a place where, when you need it at 2am, you can find it quickly and easily.  How you pull this off is likely dependent on the system programmers who are in charge of your infrastructure, the processes you use to get software into the production libraries.  The greatest challenge here is to get those process-oriented folk to understand that the process isn't just one step, that actions taken in the past have serious consequences for actions that will take place in the future.  Apologies in advance to sysprogs this doesn't apply to, but my experience is that most of them are fundamentally incapable of thinking three moves ahead.  The compiler listing must be saved now in case it is needed in the future.  So...

If your protocol is that you always compile at 'acceptance test' time and move the resultant load module to production when it's accepted, you must capture the a/t compiler listing.  If you re-compile before the move to production, that compiler listing is the one you need to preserve.  You need a large compiler-listing dataset whose members will match one-to-one with the production load library.  You will also want to capture the linkeditor's output since that forms a critical mass of information about the load module, and therein lies a problem:  the DCB of the LKED listing is potentially different than that of the compiler output.  I got yer solution right here:

Go to the REXX Language Assn's free code repository and get COMBINE.  In the JCL for doing the compile/link, insert an IKJEFT01 step after the LKED has run and execute COMBINE there specifying the DDs for the compiler output and the LKED output.  COMBINE will combine (naturally) those two files and produce a single file that can be saved to your listing dataset.  See the HELP text for COMBINE for the calling sequence, but it will generally be something like:

COMBINE COMPILE LKED
with //COMPILE DD referencing the compiler output and //LKED DD referencing the LKED output.  Your combined output will be on //$PRINT DD.

Start today.  Nothing moved to production before you start doing this will be included, so the sooner you start, the better.

Monday, May 7, 2012

Scrolling a Panel Left and Right

Back on November 3rd, I wrote about "cylindrical arrays" and said "ISPF facilities do not allow the shifting of panels right and left except for some very special IBM-originated panels", but Chris Lewis introduced me to a way to simulate it.  Herewith.

The first thing is to rig the panel so that it can have any column headings you desire and fit it with a variable )MODEL.  The "desc" following is the last line of the )BODY and is where the column headings would be stored.  It is followed by a )MODEL section where the actual model is a variable:

@desc
)MODEL ROWS(SCAN)
&modl

Before the panel can be TBDISPL'd, values for those must be set up...

   /* Set up values for the variable-format panel(s)                 */
   desc1   = "V -Member- Type  A/R Base  -Userid-  ---Date--- Time   Li",
             || "btype   Cleared"
   desc2   = "V -Member- Type  A/R Base  -Userid-  -Username----      Cl",
             || "eared  As"
   modl1   = "_z!mlroot  +!z   !mlbase   !mluid    !mldate    !mltime!ml",
             || "loc    !mlclrdt "
   modl2   = "_z!mlroot  +!z   !mlbase   !mluid    !mlsname           !m",
             || "lclrdt !mlclrnm "

...and loaded to the variables named on the screen:

      modl = Value('modl'style)
      desc = Value('desc'style)

Now, whenever the response from the panel is a PF10 or PF11, call the code that inches the "cylindrical array" left or right:

   select
 
      when pfkey = "PF11" then,
         style = style//stylect + 1    /* next style                 */
 
      when pfkey = "PF10" then,
         style = (style+stylect-2)//stylect + 1       /* prev style  */
 
      otherwise nop
   end

That's all there is to it.  Once STYLE is set based on the PF10 or PF11, that style-number is used to select DESC1/MODL1 or DESC2/MODL2 just before the panel is redisplayed.  It's the same panel (really), but the content displayed there will be different enough that it will appear the panel has actually been shifted.

Sunday, December 4, 2011

File Tailoring for Fun and Profit

Creating JCL for running a mainframe task is the most common use for ISPF's File Tailoring — because it does it so well.  Too often, though, such tasks are approached with a 'Bigger Hammer' mentality because the process to be tailored is seen as uncomfortably complex.  For instance:  your installation uses COBOL, PL/I, and ForTran;  some programs use DB2;  some run as part of CICS.  When it comes time to compile something, you may have to run the DB2 preprocessor;  you may have to run the CICS preprocessor;  the compiler must be appropriate to the source language;  if the compile is successful, the object module is to be link-edited;  much of the compiler listing for DB2/CICS modules is crap that does little beyond making the printed version harder to lift.  Is the compiler listing to be saved as a dataset or just printed?  If 'saved', where?  Are you going to trim away all the frou-frou before you print or save?

Faced with such a collection of decisions, most installations opt to have individual PROCs for each potential combination and to name them under a convention that allows the programmer to correctly specify the name of the PROC that does a DB2/non-CICS/PLI compile-and-link.  When a new compiler is released, somebody gets to fix and test a dozen PROCs to insert the updated library information, and it's usually several weeks before anyone realizes that an error sneaked through.  Ouch.

The reason file tailoring is not considered in these instances is because of all those mays above.  The DB2 precompile hands its output to the CICS precompile, but there may not be a DB2 precompile step at all.  The compiler gets its input from the CICS precompile step...  unless there isn't a CICS precompile step.  Where does the input come from then?  There is a solution.

You always start off with an IEFBR14, the safest program in the visible universe:

//ORIGIN   EXEC PGM=IEFBR14
//SOURCE    DD DISP=SHR,DSN=&SRCDS(&SRC)
)SET LSTEP = ORIGIN
)SET LDDN  = SOURCE

Note that this piece of skeleton identifies itself by setting LSTEP and LDDN.  Later steps can use these to find the input they need to focus upon.  Let's posit that if we have to ZORK and PARFLE and FLENCH a program, they always happen in that order, although some (or none) of them may happen at all.

)SEL    &ZORK = Y
//* --------------- ZORK processor --------------     */
//ZORK     EXEC PGM=ZORK
//SYSIN     DD DSN=*.&LSTEP..&LDDN
//SYSPRINT  DD SYSOUT=*
//SHIPOUT   DD UNIT=VIO,SPACE=(CYL,(5,1)),DISP=(,PASS),
//             DCB=(RECFM=FB,LRECL=80,BLKSIZE=0)
)SET LSTEP = ZORK
)SET LDDN  = SHIPOUT
//            IF (ZORK.RC << 8) THEN
)ENDSEL &ZORK = Y
)SEL    &PARFLE = Y
//* --------------- PARFLE processor ------------     */
//PARFLE   EXEC PGM=PARFLV2
//PARFIN    DD DSN=*.&LSTEP..&LDDN
//SYSPRINT  DD SYSOUT=*
//PARFOUT   DD UNIT=VIO,SPACE=(CYL,(5,1)),DISP=(,PASS),
//             DCB=(RECFM=FB,LRECL=80,BLKSIZE=0)
)SET LSTEP = PARFLE
)SET LDDN  = PARFOUT
//            IF (PARFLE.RC << 4) THEN
)ENDSEL &PARFLE = Y
)SEL    &FLENCH = Y
//* --------------- FLENCH processor ------------     */
//FLENCH     EXEC PGM=FLENCH00
//SYSIN     DD DSN=*.&LSTEP..&LDDN
//SYSPRINT  DD SYSOUT=*
//OUTFLEN   DD UNIT=VIO,SPACE=(CYL,(5,1)),DISP=(,PASS),
//             DCB=(RECFM=FB,LRECL=80,BLKSIZE=0)
)SET LSTEP = FLENCH
)SET LDDN  = OUTFLEN
//            IF (FLENCH.RC << 8) THEN
)ENDSEL &FLENCH = Y

If the ZORK processor is activated, it gets its input from '*.ORIGIN.SOURCE' because those are the current settings for LSTEP and LDDN.  When it's complete, it indicates that any later steps should use ZORK.SHIPOUT by setting LSTEP to 'ZORK' and LDDN to 'SHIPOUT'.  If the ZORK processor is not activated, LSTEP remains set at 'ORIGIN' and LDDN at 'SOURCE'.

If only the FLENCH processor is cleared to run, neither of the other two steps have altered LSTEP or LDDN and it picks up the dataset mentioned by the IEFBR14 step as its input.  Whichever combination of steps ran or didn't, the next step downstream will always find its input via '*.&LSTEP..&LDDN'.

Of course, things could be more complicated necessitating a little more finesse, but File Tailoring is, I assure you, up to the task.

Sunday, November 27, 2011

ISPF Tables — Copying

For any number of reasons, you may decide you need a copy (usually partial) of some ISPF table.  Possibly, the subset of the table you wish to operate on may not be easily parsed on the basis of the table's data content.  You will have to build a temporary table and load it from the original table, perhaps row-by-row.  Careful, now...

TBQUERY will tell you what an existing table looks like.  As soon as the table is opened, TBQUERY can be invoked to give a list of the keys and names.  From this, you can TBCREATE a replica table.

    "TBOPEN SAMPLE"
    "TBQUERY SAMPLE KEYS(knames) NAMES(nnames)"
    "TBCREATE NEWCOPY KEYS"knames "NAMES"nnames 

The column names returned from TBQUERY are enclosed within parentheses.  Unless you have other uses for the lists, it isn't even necessary to trim them off.  At this point, you are ready to write a duplicate or subset table, save for one easily overlooked aspect.

Remember 'extension variables'?  Unless you ask when reading each input row you will not know whether there are extension variables attached to the row, nor will you know their names.  Not knowing their names means not being able to specify which names should be attached to each output row.

   do forever
      "TBSKIP" intbl "SAVENAME(xvars)"
      if rc <> 0 then leave
      "TBMOD " outtbl "SAVE"xvars
   end

Notice that the extension variables themselves did not have to be operated on in any way.  We did not have to assign them, for instance.  Because the row was read (the NOREAD parameter was not specified), all the row's variables: keys, names, and extension; were populated to the variable pool.  They are therefore immediately available to be loaded to the output table row.

In fact, because of this behavior: populating every variable regardless of type; it might be good practice to never read a table row without acquiring the list of extension variables.

You can get fancier than this, of course.  For an idea of just how much fancier you can get, take a look at TBCOPY over on my REXX page.  TBCOPY doesn't use TBQUERY.  It relies on an external subroutine, TBLGEN, which itself relies on a special table in which is stored the DNA of all permanent tables, sort of a data dictionary for tables.  TBLGEN is much more elaborate than is strictly necessary, because it is intended to do all of an installation's TBCREATEs for permanent tables.  TBCOPY is likewise much more elaborate than is strictly necessary because it is intended to do all TBCOPYs for permanent tables within an installation.

Saturday, November 26, 2011

ISPF Tables — Extension Variables

Sometimes we just have to deal with tables we didn't design (and would have designed differently had we had the opportunity).  Sometimes we just need a tiny little tweak to make that table perfect (reg., U.S. Patent Pending).  Sometimes we want to let everyone know that this table row is unusual either for what it has or for what it hasn't.  Enter the extension variable.

Every table row in a given table has room for every KEY and NAME field originally specified when the table was TBCREATEd.  Flexible little devils that they are, table rows also have unlimited* space for other stuff, and every table row can be unique as to what other stuff it holds.  Observe:

    "TBCREATE BILOMAT KEYS(BMITEM) NAMES(BMDESCR BMONHAND BMECOQTY)"

our Bill-of-Materials table.  It is keyed by the item number (BMITEM), and has fields for a description, the number on hand in inventory, and the economic order quantity.  Wha...?  Where are the 'materials' that make up the 'bill'?  They're in extension variables, naturally.

    "TBVCLEAR BILOMAT"
    bmitem   = 'ER4590T'
    bmdescr  = 'Whatsis, large, left-handed, tan'
    bmonhand = qty.whatsis_l_lh_tan
    bmecoqty = ecocalc.whatsis_l_lh
    xvars    = 'DD114 DF33 DF34 DF36 DF38 EM2030'
    dd114    = 6
    df33     = 1
    df34     = 1 
    df36     = 1
    df38     = 2 
    em2030   = 4
    "TBADD BILOMAT SAVE("xvars")"

The row for 'ER4590T' in table BILOMAT has all four canonical fields filled, and there are six additional extension variables, one each for the six components of every ER4590T.  When the supplier for the EM2030 component advises you there will be an unavoidable 40% increase in the price of that component, you're going to want to know which of your products (BMITEMs) is going to be affected and by how much.  The catalog entries for each of those products will have to be adjusted to correctly show the new price of everything that relies on the EM2030, right?  Of course!

    "TBVCLEAR BILOMAT"
    em2030  = 0
    "TBSCAN BILOMAT ARGLIST(EM2030) CONDLIST(GT)"
    "TBDISPL BILOMAT PANEL(SRCHPART) AUTOSEL(NO)"

TBVCLEAR zaps all the 'regular' names mentioned in the definition of table BILOMAT, variable 'EM2030' is set to zero, TBSCAN specifies that we seek all rows where an extension variable 'EM2030' has a value greater than zero.  Panel SRCHPART should, if coded correctly, display a scrollable list of all rows (all BMITEM keys) associated with subassembly EM2030.

Zounds!  This is almost DB2!  Except that it didn't take us eight months and require the approval of two vice-presidents.  And if they decide later that they really do want to implement it in DB2, you already have the proof-of-concept ready to demonstrate.

 

(*) — You didn't think when I said 'unlimited' I actually meant 'without limit', did you?  Tsk.  Of course there are limits.  It's just that you're unlikely to hit them before your table is so large your system chokes trying to open it.  I've seen that happen with complex tables as small as 6,000 rows, but I have also seen 50,000-row tables that worked just fine, and I suspect they could have grown much larger without causing a problem.

Friday, November 25, 2011

ISPF Tables — Searching

There's no telling how many programmers have simply given up using ISPF tables because 'the @#$% table services never work the way you expect them to work!'  They actually work just fine.  It's the @#$% IBM manuals that don't make their operation clear — don't make the operation of some very complex services as clear as they ought to be.  Whether this is simply a matter of writing style or whether IBM does so deliberately, the delicate interrelationship between TBVCLEAR, TBSARG, TBSCAN, and TBDISPL rarely gets to a programmer's conscious level.  The lack of examples showing TBVCLEAR, TBSARG, and TBDISPL working in concert also doesn't help.

The most important ISPF table service related to table searching is neither TBSARG nor TBSCAN;  it is TBVCLEAR.  The IBM manuals (almost) completely gloss over the fact that when searching an ISPF table, every element that has a value, whether it is part of the explicitly-named search argument or not, participates in setting the search argument.  Every element that has a value.  The service that makes sure elements do not have values is TBVCLEAR.

If you carefully read the manual text for TBSARG, there are subtle clues to this behavior as it relates to setting search/scan arguments:

A value of null for one of the dialog variables means that the corresponding table variable is not to be examined during the search.
and
To set up a search argument, set table variables in the function pool to nulls by using TBVCLEAR.
and
The search arguments are specified in dialog variables that correspond to columns in the table, and this determines the columns that take place (sic) in the search.

You could be forgiven for thinking that specifying this variable or that one in a TBSARG might have some bearing on which variables get used for setting the search argument.  Alas, it has less 'bearing' than might seem obvious.

Let us assume that we have a table, TBL, and that this table is defined as

    "TBCREATE  TBL  KEYS(KEY1 KEY2) NAMES(NAME1 NAME2 NAME3) WRITE REPLACE"

We have read a particular row, either by TBGET or TBSKIP, and now we wish to find all the rows in TBL with the same NAME2 value.  These will be displayed via TBDISPL for further processing.

    "TBSARG TBL NAMECOND(NAME2,EQ)"

is insufficient.  It is insufficient because KEY1, KEY2, NAME1, and NAME3 all have values and are not null.  In fact, a subsequent TBSCAN or TBDISPL will return a single row, the one that was just read.  That's the only row on the table with a matching KEY1 and KEY2.  In order to expand the search to the remainder of the table, we must zap all those variables:

   nval = name2     /* preserve the value */
   "TBVCLEAR TBL"
   name2 = nval     /* load NAME2 */
   "TBSARG TBL NAMECOND(NAME2,EQ)"

In fact, since "EQ" is the default and all table variables that are not null participate in the search (and NAME2 is the only non-null variable at this point), it should be sufficient in this case to cast the TBSARG as

    "TBSARG TBL"

Since we're working in REXX, some may be tempted to DROP the elements that are not to be searched-for.  A REXX 'drop' is not the same thing as setting a table variable to null via TBVCLEAR and the results may be disappointing.  Likewise, setting the variables to null via 'closed quotes'

    parse value "" with key1 key2 name1 name3 .

also won't get you what you need.  This will cause the search to be limited to those rows for which KEY1 and KEY2 are empty.  There can only be one of those and there may not be even that.

Before TBSARG, before TBSCAN (if that's how you're setting the search argument), TBVCLEAR.  Voila!

Wednesday, November 23, 2011

ISPF Tables — Closing

How do you like that?  I wrote this and never published it.  This was supposed to be the blog entry for 11/22, and it seems to have been forgotten.  Ah, well...  better late than never...

When you've finished doing whatever needed to be done with the table, it must be TBCLOSEd.  As with TBOPEN, a LIBDEF is needed, but not to ISPTLIB.  ISPTLIB is the input side of table processing.  ISPTABL is the output side of table processing.  Since data will be written to ISPTABL, ISPTABL cannot be a concatenation.  Only one dataset may be allocated to ISPTABL:

   if noupdt = 1 | sw.0tbl_chgd = 0 then "TBEND  " $tn$ /* don't save */
   else do
        "LIBDEF  ISPTABL  DATASET  ID("isptabl")  STACK"
        "TBCLOSE" $tn$
        "LIBDEF  ISPTABL"
        end 

If there have been no changes to the table, it is not necessary to TBCLOSE (which writes the table to disk).  A simple TBEND is sufficient.  Both TBEND and TBCLOSE flush the in-storage copy of the table, but TBEND doesn't write to disk first.  Since it doesn't write to disk, the LIBDEF for ISPTABL is not necessary for TBEND.

As with other LIBDEFs, this one only has to exist long enough to close the table.  After that, it can be (and should be) released immediately.

ISPF Tables — Defining and Building

Building an ISPF table is done by the TBCREATE service.  The table may or may not have a key or keys.  The table may or may not have one or more name (data) fields.  It will almost certainly have at least one of either, but 'almost' is not 'absolutely'.  In fact, an ISPF table may be TBCREATED with no specifically-named fields at all...  but you had better know what in Hell you are doing in that case.  Very likely, before doing so, you would very definitely know what you had in mind.  The uses for an 'extension-variables-only' table are breathtakingly esoteric.

    "TBCREATE" table_name "KEYS(" key_field_names ")",
                         "NAMES(" data_field_names ")" other_table_qualities
    "TBCREATE TEMP  [ KEYS() NAMES() ] WRITE REPLACE"       

To populate a new table row, each of the named fields, whether KEYS or NAMES, is assigned a value (or not — more later) and the TBADD service is called.  The row is created with all KEYS and NAMES fields, whether populated or not, plus all extension variables referenced by the SAVE parameter.  Extension variables are potentially unique to each row, and are only loaded to the table if specifically requested in the TBADD/TBMOD service call.

    "TBCREATE ZORK KEYS(INDEX OFFSET) NAMES(PLACE TYPE VALUE)"
    index   = index + 1
    /* OFFSET not populated */
    place   = left
    type    = normal
    value   = total
    oper    = userid()
    time    = Date("S") Time("N")
    "TBADD ZORK SAVE(OPER TIME)"

This added one row to table ZORK.  All fields except OFFSET have values, and there are two extension variables, OPER and TIME, which are not part of the canonical definition.  There is a field for OFFSET, but it is null, and here null has a different meaning than it does in REXX.  Here, it means: doesn't exist.

That's it.  That's all there is to populating a table.  Updates to the table thereafter can be done via TBADD (for new rows) or TBMOD (for replacement rows — or new rows if the key does not presently exist).

Monday, November 21, 2011

ISPF Tables — Opening

When using an ISPF table for input, the table library must be available via ISPTLIB.  The library containing the table to be used has to be either part of the original ISPTLIB allocation or LIBDEF'd into a preferential position.  If LIBDEF'd, the LIBDEF can reference either a DATASET or a LIBRARY.  If a LIBRARY, the library itself must have been previously ALLOCATEd under a unique DDname.  This latter method is fraught with hazard and I do not recommend it.  I recommend instead using the DATASET route as being much less likely to give you an ulcer:

   "LIBDEF  ISPTLIB  DATASET  ID("isptlib")  STACK"
   --- use the dataset ---
   "LIBDEF  ISPTLIB"

Once the dataset is in place, you must open it with TBOPEN...  unless it's already open...  unless it's not even present.  Well, heck, how do we know that?  TBSTATS:

   "LIBDEF  ISPTLIB  DATASET  ID("isptlib")  STACK"
   "TBSTATS" $tn$ "STATUS1(s1) STATUS2(s2)"
   if s1 > 1 then do
      say "Table" $tn$ "not available."
      zerrsm = "Table" $tn$ "not available."
      zerrlm = "Table" $tn$ "not found in the ISPTLIB library chain"
      sw.0error_found = "1"
      end; else,
   if s2 = 1 then do                   /* table is not open          */
      "TBOPEN "   $tn$   openmode.NOUPDT
      if rc > 4 then do
         sw.0error_found = 1
         zerrsm = "Table did not OPEN"
         zerrlm = "Table" $tn$ "cannot be opened due to prior",
                  "enqueues."
         "SETMSG  MSG(ISRZ002)"
         end
      end
   else "TBTOP" $tn$
   "LIBDEF  ISPTLIB"

STATUS1 generally addresses the data-content of the library.  STATUS2 references the OPEN-state of the named table, in this case, variable "$tn$".  A value of "1" in STATUS1 indicates that the named table exists in ISPTLIB.  A value greater than one signals trouble:  the table you've named isn't where it can be opened.  This certainly constitutes 'an error'.

A value of "1" in STATUS2 signals that the named table is not presently open or otherwise in use.  If this is not the case (the table is already open - STATUS2 returned a value greater than "1") there is no need to re-open the table.  ISPF will ignore such a request.  With the table presently in a not-open state, a TBOPEN can be issued for the table.  The example here shows that the mode of opening is determined by the current setting of variable "NOUPDT" (set in REXXSKEL's TOOLKIT_INIT section).  The table will be opened for WRITE if NOUPDT is off, and NOWRITE if it is on.

A table which is already open need only be TBTOPped to make it ready for our use.

As soon as the table is determined to be open, the LIBDEF for the ISPTLIB is no longer needed — a full copy of the table now exists within the user's region.  It's good practice to immediately drop the LIBDEF the instant it has served its purpose.

That's it.  That's all there is to opening an ISPF table.  From here on in until the table is TBCLOSEd or TBENDed, the complete contents are available for your use.  Unfortunately, that makes them unavailable for anyone else's use if the table was opened WRITE, so be a good user: get in, get done, get out.

If you as the programmer discover that some of your users are hogging the table, you may have to take drastic action to prevent that.  'Drastic action' may include TBCLOSEing the table after every update, TBENDing it after every inquiry, and consequently repeatedly TBOPENing it for each new cycle.  In between, you will have to maintain a record of what the original looked like when it was first fetched so that you'll know whether it was changed in the meantime.  Lots of CPU cycles wasted because of undisciplined users.  You may even wind up rewriting the app to eliminate ISPF...  but you will have had the experience of prototyping it (easily) in ISPF and will get the benefits Dr. Brooks promised when he said:

When planning a program, plan to do it twice.  You're going to do it twice;  you might as well plan for it.

Friday, November 18, 2011

ISPF Tables

Most of our uses of ISPF tables are very simple.  Every once in a while, however, we might bump into a relatively-more-complex situation where the uses are not so simple.  Without fail, such times are opportunities for us to learn some very difficult and painful lessons.  It's much better to learn them on a blog than while sitting before a terminal pumping code in a vain attempt to make a deadline.  The most difficult lesson is this: reading a table row populates any REXX variables that have matching names (and creates them otherwise).  This is most dangerous when you have to work with more than one table simultaneously.  Let me illustrate:

   address ISPEXEC 
   "TBSKIP ORDERS" /* sets vendor, product, date, qty, invoice */
   "TBGET  INVENT" /* keyed by 'product', sets qty, descrip */ 

The value for 'qty' from 'ORDERS' has been overlaid by 'qty' from 'INVENT'.  To preserve the original 'qty', it has to be copied to a different variable name.  Code-writing time is the wrong place to have to suddenly rename variables wholesale.  That is why I recommend that every table variable name should have a two-character prefix unique to the particular table:

   address ISPEXEC 
   "TBSKIP ORDERS" /* sets orvendr, orprod, ordate, orqty, orinvc */
   "TBVCLEAR INVENT"
   inprod = orprod
   "TBGET  INVENT" /* keyed by 'inprod', sets inqty, indesc */ 

There is now 'orqty' and 'inqty' and they can be compared, perhaps to see whether it's time to reorder.

Problem solved?  Hardly.  ISPF tables have more tricks up their sleeve(s).  Behold... extension variables.

Extension variables are unique to each row, and only the programmer exercising care in their naming can prevent clashes such as above.  As with ordinary table variables, extension variables also populate REXX variables when the row is read, and there is no warning that a simple TBSKIP may have clobbered other variables... unless the programmer is smart enough to ask if any extension variables were transferred with the rest of the row:

   "TBSKIP ORDERS SAVENAME(xvars)"
   parse var xvars  "("  xvars  ")"

which lets us know that 'ornotes', 'ormgrnam' and 'ormgrtel' were set without warning.  If we were to update that row without also commanding that those three extension variables be included in the update, they would simply disappear, also without warning.  It's a quick way to shoot holes in table data.  That's not us, thankfully.  We're smart enough to:

   "TBMOD   ORDERS  SAVE(" xvars ")"

thus preserving the data in those extension variables.

As with everything else, a few small proof-of-concept routines can be an invaluable learning aid, and (of course) you need to test the logic to death.

Wednesday, November 9, 2011

Invoking an ISPF-dependent routine from READY

So you've written this peachy routine that uses all sorts of ISPF facilities.  It runs great from any command line just about anywhere within ISPF, but when you try invoking it from READY, it crashes because there is not an ISPF environment available.  You could simply start ISPF whenever you want to run it, run it from the primary option panel, and flush ISPF when it's done...

...or you could rig it so that it will run from READY as easily as it does from within ISPF.  That way, you never have to remember which of your routines need ISPF and which don't.  The ones that do will pave their own road ahead of their need for it.

WARNING: the code that follows presumes you have all the capability provided by REXXSKEL.  For more information about REXXSKEL and what it does, pop on over to my REXX page and take a look at the write-up titled "How To REXXSKEL".  Of particular interest in that regard is the subroutine SWITCH and the flow of control from the mainline to TOOLKIT_INIT through LOCAL_PREINIT.

TOOLKIT_INIT in a REXXSKEL-enabled routine sets, among other things, "sw.inispf" which tells us whether or not an ISPF environment exists.  (Yes, I know it should be "sw.0inispf", but REXXSKEL was built a long, long time ago in a galaxy far away...)  When an ISPF environment does not exist, we can generate one via ISPSTART, but we have to be prepared to rip it down when we're done with it.  During the early mainline of your well-structured program, you will need to restart the routine in a newly-invoked ISPF environment and to halt the current execution as soon as that restarted execution completes:

if /sw.inispf  then do                 /* after TOOLKIT_INIT return  */
   arg line
   line = line "((  RESTART"           /* tell the next invocation   */
   "ISPSTART CMD("exec_name line")"    /* Invoke ISPF...             */
   exit 4                              /* ...then bail out           */
   end

When the restarted routine ends, it should end with a non-zero return code to skip processing for any generated LOG or LIST datasets written by the newly-invoked ISPF environment:

if sw.0Restarted then,                 /* just after DUMP_QUEUE      */
   exit 4

To determine whether or not this execution was restarted, LOCAL_PREINIT (part of initialization, but customized routine by routine) has checked the parameter string for the presence of the token "RESTART":

   sw.0Restarted = SWITCH("RESTART")

That's it.  That's all there is to it.  When you fire this routine from READY, it discovers that it is not in ISPF, restarts itself (via ISPSTART).  The restarted invocation ends non-zero, and returns to its calling point in the original, whereupon the original exits back to the operating system.

Sunday, November 6, 2011

(TSO) ALTLIB = (ISPF) LIBDEF

Back on Novenber 2, I did a short introduction to ALTLIB, the TSO command that allows you to insert one or more datasets into a preferential position so that (in that case) REXX routines could be found for execution.  ISPF has an equivalent command that, if you write ISPF dialogs, and especially if you write them in REXX, can come in very handy.  Let's quickly review how ALTLIB works:

"ALTLIB   ACT APPLICATION(EXEC)  DA('........') "
if rc > 4 then do
   say "ALTLIB failed, RC="rc
   exit
   end

.... call one or more routines stored there ....

"ALTLIB DEACT APPLICATION(EXEC)"

This obviously takes place in 'address TSO'.  ALTLIB ACT names a dataset to be searched ahead of the normal search order for (in this case) SYSEXEC.  A non-zero return code indicates the ALTLIB didn't happen, and the process ends with either an 'exit' or a 'return'.  If the ALTLIB happened, the process can use the code stored there, and when the task is complete, an ALTLIB DEACT undoes the work of the earlier ALTLIB ACT.

ISPF has an almost-exactly-equal process called LIBDEF.

   "LIBDEF  ISPTLIB  DATASET  ID("isptlib")  STACK"
 .... use the contents of the ISPTLIB ....
   "LIBDEF  ISPTLIB"

This operation must take place in 'address ISPEXEC'.  The LIBDEF parms are (1) the DDName to be altered, (2) 'DATASET' or 'LIBRARY', and (3) the identity of that dataset or library.  There are other parameters which can be passed including, as shown here, 'STACK'.  Note that when using 'LIBRARY' the ID-portion is the DDName under which the library or libraries have been allocated;  that implies a prior ALLOC for those assets.  When using 'DATASET', no prior ALLOC need have been done.  Programmers who complain that some ISPF asset is tied up and can't be released are often referring to a 'LIBDEF ... LIBRARY' which has gotten tangled somehow.  Avoid entanglements;  use 'LIBDEF ... DATASET'.

When the asset is no longer needed, it is good practice (you should consider that required) to release the LIBDEF by specifying the active DDName alone (as shown).

Between the two LIBDEF operations, the asset is as much a part of the active DDName as if it had been part of the original DD during LOGON processing, and this is true whether the active DDName is ISPPLIB, ISPTLIB, ISPTABL, ISPMLIB, ISPSLIB, or any other.  Every ISPEXEC command will search the appropriate ISPxLIB allocations in reverse order, original allocations last, until it finds the asset it's looking for.

Now...  STACK.  If the original LIBDEF did not specify STACK, the following LIBDEF, the undoing LIBDEF, not only releases this LIBDEF, but all prior LIBDEFs whether done with STACK or not.  In other words, if process "A" LIBDEFs an ISPTLIB into place and then calls process "B" which LIBDEFs a further ISPTLIB into place before calling process "C" which LIBDEFs a third ISPTLIB into place, it is possible for process "C" to eliminate all three LIBDEFs before processes "A" and "B" can use them.  Processes "A" and "B" will likely fail because the proper asset is not present, although something worse could happen: they might find their data in the wrong asset libraries and use those, thus appearing to have operated correctly when, in fact, they did not.