Where’s The Module?
August 22, 2012 Susan Gantner
Note: The code accompanying this article is available for download here.
Over the years I have participated in many discussions about how to find all the programs that have a specific module bound into them. Since the DSPPGM command doesn’t support output to a file, it provides a challenge to find this information. The question always seems odd to me, because my philosophy is that if the code in a module is needed in more than one program, it goes into a service program. The service program is then referenced by (and shared by) all the programs that need that function. That way, a module would only ever exist in one place–either in a program or in a service program–and never in multiple programs.
However, I’ve heard the question so many times over the years, it would appear that a lot of shops don’t adhere to the same philosophy. Sure enough, I was recently working with a client’s application that had modules that were bound by copy into many programs. And, you guessed it, I had to make a change to some of those modules and replace them in all those programs. They didn’t have change control software to do the replacement for me nor did they have cross-reference software to tell me all the programs I needed to update.
So I dusted off a utility program I had written awhile back that gathers information about modules in programs and service programs and dumps it into a simple database table. Querying that table, I was able to quickly get a list of the programs I needed to update.
There was also a requirement to ensure that all modules were compiled to a previous release and that they had their observability removed. So I decided to make some additions to the data gathered to include the target release and the observable state so I could ensure I hadn’t missed any of those requirements. While I was at it, I decided to collect some other information that could prove useful in the future, such as the module’s source member, date compiled, and the date the source member was last changed.
To collect this information, my utility uses the QBNLPGMI (List ILE Program Information) and the QBNLSPGM (List Service Program Information) system APIs. In case you may find yourself in a similar situation and can make use of this kind of information about modules in your ILE programs, let’s take a quick look at the highlights of the code that does this. You can get the complete program source here.
Let’s first look at the prototypes for the two APIs below. Note that both APIs have identical parameter requirements. As we’ll see a little later, both have nearly identical formats of information as well, simplifying my coding effort significantly.
D ListPgmInf PR ExtPgm('QBNLPGMI') D UsrSpcNam 20A Const D Format 8A Const D QualPgmName 20A Const D ErrorFeedback Like(ErrorInfo) D ListSrvPgmI PR ExtPgm('QBNLSPGM') D UsrSpcName 20A Const D Format 8A Const D QualSrvPName 20A Const D ErrorFeedback Like(ErrorInfo)
In both cases, the first parameter has two parts: the user space name is in the first 10 characters with the library name occupying the last 10. Both APIs offer data in multiple formats, so I specify which format I want using the second parameter. The third parameter is another two-part parameter, with the object name in the first half and the library in the last half. However, in this case, both names can be special values, such as *ALL or *LIBL, and wild cards are also possible, such as ABC*. The error feedback structure is one of the standard structures used in most APIs.
In my program, I receive the program (or service program) name and the library as parameters. I typically use *ALL for the program name and a specific library name that I want to collect information about. This is a good time to mention one of the limitations of this program. The simple techniques used here will only work if the information for all the programs requested will fit into the user space. The format of data I’m using with these APIs occupies about 4000 bytes per module per program (or service program). Since the maximum size of a user space is 16 MB, my program can only handle processing information for roughly 4000 modules at a time. That was plenty for the application library I was working with, but be aware that if your library is likely to exceed those limits, you’ll need to do a little more work. There are other techniques one could use to accommodate larger lists if necessary, but I was lucky enough to get away with this simple technique.
Next, let’s look at the two data structures we’ll need to use to process the data in the user space. Below you’ll find the HeaderInfo and the PGML011structures. The format for these structures is documented in the API reference in the IBM Information Center, so I won’t spend much time on the details here. I’ll just highlight a few points.
D HeaderInfo DS Based(pHeaderInfo) D 103A D ListStatus 1A D 20A D ListOffset 10I 0 D ListSize 10I 0 D NbrEntries 10I 0 D EntryLen 10I 0
List APIs have a standard block of information at the beginning of the user space describing the data that follows. That’s the purpose of HeaderInfo. Note that I only bothered to define the parts of this structure that were useful for my task. You’ll also note that both structures are based on pointers, which will allow my logic to access that data the API has placed in the user space.
The first is the ListStatus character. If I don’t see a “C” for Complete here, that’s an indication that the user space filled up before the API could put all the requested information in it. It’s critical to check that status to ensure the data you have is complete. The ListOffset variable allows me to position to the beginning of the module data and the EntryLen variable provides the means to move from one module information entry to the next. The NbrEntries variable tells me when to stop processing data.
D PGML0100 DS Based(pPgmL0100) D Pgm 10A D PgmLib 10A D Mod 10A D ModLib 10A D Srcf 10A D SrcLib 10A D SrcMbr 10A D ModAttrib 10A D CrtDate 7A D CrtTime 6A D UpdDate 7A D UpdTime 6A D TgtRls 6A Overlay(PGML0100:161) D Obsrv 1A Overlay(PGML0100:212)
Each module in each program will have information stored in the requested format. The format I have chosen to work with is named PGML0100, so that’s what I’ve named the data structure in the program. As before, I’ve only bothered to define the parts of the structure I actually plan to use. In this case, I’ve made the subfields in the data structure match the column names in my database table, so that a simple WRITE operation will take care of getting the data for each module into the database. The only exceptions to that are the date and time values, which I have chosen to reformat into date and time data-type columns.
The logic of this program is very simple. I will not cover the parts of the program that create the user space (if necessary) and get a pointer for the user space. Those steps are fairly standard fare for any List API and such programs have been covered in Guru in the past with examples of similar logic. I’ll pick up the logic at the call to the first list API.
(A) ListPgmInf ('MODULEINF QTEMP' : 'PGML0100' : QualPgmName : ErrorInfo ); (B) If BytesUsed <> 0; Dsply ('API Error ' + ExpID); BytesUsed = 0; EndIf; If ListStatus <> 'C'; Dsply 'Module Data is Incomplete'; EndIf; (C) pPGML0100 = pHeaderInfo + ListOffset; PgmTyp = '*PGM'; ExSr ProcEntries;
Here’s more detail to go along with the letters listed in the code shown above:
(A) I first call the List Program Information API. Since I have defined all my parameters as Const, I can hard-code the values for things such as the name of the user space and the format name. The third parameter contains the combined program and library names received as parameters. In my case, it will contain a value something like: *ALL MYLIBNAME
(B) Using the standard API error handling structure, I check to see if the API ran successfully by interrogating BytesUsed. I also check the ListStatus to ensure I have complete data in the user space. Obviously, my simple example program here isn’t reacting adequately to these conditions. I’ve simply used DSPLY to alert myself of any problems that may have occurred to keep the logic simple for this example.
(C) Assuming the API ran successfully and I have complete data to process, I calculate the address of my PGML0100 structure to be the address of the user space (which was retrieved using an API earlier in the code not shown here), plus the ListOffset value from the HeaderInfo structure. I also set the value of the PgmTyp variable to *PGM since I’m processing only program objects at the moment. Once the programs are processed, the process repeats for service program objects.
In real life, I don’t write subroutines. I always write subprocedures. In this example, I have used a subroutine to process the data from the user space just in case there are readers out there who are procedure-phobic. The subroutine’s logic is shown below.
BegSr ProcEntries; For i = 1 to NbrEntries; ModCrtDate = %Date(CrtDate:*CYMD0); ModCrtTime = %Time(CrtTime:*ISO0); SrcUpdDate = %Date(UpdDate:*CYMD0); SrcUpdTime = %Time(UpdTime:*ISO0); Write MODXREFR; pPGML0100 = pPGML0100 + EntryLen; EndFor; EndSr;
The subroutine’s logic simply reformats the date and time information, writes the row, and repositions the pointer to the next block of module information by adding the EntryLen value to the current pointer address. All this is done for each block of module information until we’ve processed all the entries, which is determined by the NbrEntries value from the HeaderInfo structure.
Once I’ve processed all the modules found in the programs, it’s time to repeat the same process for service programs. I was lucky that the formats of data used for both the program and service program retrieval APIs are identical, so I reused the same PGML0100 structure for both and the processing logic can also be reused.
ListSrvPgmI ('MODULEINF QTEMP' : 'SPGL0100': PgmName + LibName : ErrorInfo ); If BytesUsed <> 0; Dsply ('API Error ' + ExpID); BytesUsed = 0; EndIf; If ListStatus <> 'C'; Dsply 'Module Data is Incomplete'; EndIf; pPGML0100 = pHeaderInfo + ListOffset; PgmTyp = '*SRVPGM'; ExSr ProcEntries;
That’s it! At the end, I have a database table with lots of useful information about each module in each program and service program requested. If I want to add more data to the table, I simply run it again (e.g., for *ALL in another library perhaps). However, if/when I need to refresh the data for the same programs/libraries, this program doesn’t help me. I could clear the table and start over. But it would also be simple to add some logic here to first remove any existing rows in the table for the requested programs/service programs.
I hope you may find this program a useful base for creating some utility programs of your own to gather information about modules in ILE applications.
Susan Gantner is half of Partner400, a consulting company focused on education on modern programming and database techniques and tools on the IBM i platform. She is also a founding partner in System i Developer, a consortium of System i educators and hosts of the RPG & DB2 Summit conferences. Susan was a programmer for corporations in Atlanta, Georgia, before joining IBM. During her IBM career, she worked in both the Rochester and Toronto labs, providing technical support and education for application developers. Susan left IBM in 1999 to devote more time to teaching and consulting. Together with Jon Paris, she now runs Partner400, and appears regularly at many technical conferences, including System i Developer’s RPG & DB2 Summit. Send your questions or comments for Susan to Ted Holt via the IT Jungle Contact page.