DOCUMENT #672 ======================================================================= NETWORKING WITH R:BASE: VARIABLE DIRECTORIES ======================================================================= PRODUCT: R:BASE VERSION : 3.1 or Higher ======================================================================= AREA : PROGRAMMING IN R:BASE CATEGORY: MULTI-USER ======================================================================= From Michael Peters Michael works for Janus s/w Projekte, Simon-Meister-Str.42, 5000 Koeln 60, Germany (which is the German Microrim representative) as an application developer. From the US, the easiest way to reach him is via Compuserve (CIS-ID: 100041,247). Otherwise, the telephone no. is (Germany)221-7393241. When designing R:BASE applications for networks, you need to meet the special requirements of multi-user data access but also need to integrate the application into the customer's working environment, that is, into the customer's network server directory structure. In large networks where many users access all kinds of data, the customer's system manager or network administrator must design the server directory structure with great care. During the design phase, the developer should ascertain from the system manager critical requirements for the application. Don't just throw databases and programs together Usually, as a developer, we are sometimes asked to place the database files into their server directory, with the application programs stored in another directory. In single user environments throwing all the stuff into one directory usually doesn't cause any problems, here it might! The system managers are responsible for data security. They are not interested in a confusing heap of database files, APX programs, temporary menus, printer files (.PRD), auxiliary DOS batch files and the like, but want all of this clearly divided and defined. With the database itself stored in a directory of its own, data backups are much easier to handle. Then, there is another directory just for the application programs and all kinds of auxiliary files. To keep the more adventurous network users from tampering with these files, the system manager may place a read-only restriction on this directory (and we should thank him for that...). On the other hand, the read-only flag can also be a severe restriction to our application. With the read-only flag on the programs directory, we can no longer use dynamically created menus, screen snaps, or any other fancy stuff we might want to include to enhance functionality, because we can no longer write temporary files to the hard disk - unless we write them into yet another directory (one without a writing restriction). Ok, so let's create a third directory just for all the temporary stuff! Handling Temporary Files in Network Applications ================================================ Unfortunately, one extra directory for temporary files is not enough. Imagine one user writing something into this new directory, say a dynamically created menu called TEMP.MNU. While this user works with the file, it is locked by the operating system. If another user happens to work with the same command block at the same time, his program will not be able to create the file, and might react in an unpredictable way. Similar problems might occur when using SNAP. Imagine coming back from a WHILE loop and having DISPLAY show the previous menu screen, just to see a different menu screen snapped by somebody else! There is a simple solution to this 'traffic problem': Put the directory for the temporary files on local drives instead of a network drive. Given that the workstations run under DOS and can manage no more than one session at a time, the local drive is the logical place for our third directory. Local drive access is fast, and temporary files usually don't need much disk space. Another reason is that there are more user-specific things going on on the local drives anyway. When working with R:BASE in a network environment, at least one 'private' file is needed for each user: RBASE.CFG, containing the user's unique network ID. It looks like a good idea to keep all the 'private' stuff together. So why don't we simply create one private directory on each workstation, initially containing nothing but the .CFG file. Later, when the application runs, it can simply place all the temporary files for our session there, without causing any problems. This private directory is also the logical place for the batch file that starts our application. (Remember that the directory containing RBASE.CFG should be part of the DOS search path. You should place it at the beginning of the path to avoid confusion with any RBASE.CFGs that might be wandering about somewhere else in local or server directories.) If diskless workstations are used, the 'temporary stuff' directories have to reside on the server, but there is no difference in principle: one private directory for each user is necessary to avoid file access collisions. If we take all of this into account, our working environment will go through a major change. Instead of a single directory which contains everything that belongs to the application, we will have three directories: 1. one server directory exclusively for the database (e.g. F:\RBASE\DBFILES) 2. another server directory for all the application programs (e.g. F:\RBASE\RBPROGS) 3. a multitude of local, private directories (e.g. C:\RBTEMP). Managing Three Directories in Program Code ========================================== Before starting to code on this basis, let's take a quick look at what has to be done. Actually, there is only one difference to traditional R:BASE programming: Everywhere in your code, the proper directory names have to precede the file names. That's all ! Simply replace expressions like 'RUN This IN That.APX' by 'RUN This IN F:\RBASE\RBPROGS\That.APX'. Write 'OUTPUT C:\RBTEMP\TEMP.MNU' instead of 'OUTPUT TEMP.MNU', and don't forget to replace the simple 'CONNECT Test' by 'CONNECT F:\RBASE\DBFILES\Test'. If an existing application has to be adapted to the new situation, no problem. Use a good word processor with macro programming, and the job can be done in a couple of hours, even less. Directories Might Be Subject to Change ====================================== The only thing we need to know before starting to change the code is: What will the real directory names be at the customer's site? Let's talk to the system operator again to ask him. He'll probably like the new directory structure we present to him. It fits his needs. But he might frown at us when we ask for the real directory names that we want to code into our application. Maybe he just doesn't know yet where to put all the stuff. Even if he knows, he doesn't want the directory names to be fixed for all time, without any chance to change them if a new hardware situation should arise. We begin to realize that changes to the directory structure or to file locations on the server might be necessary from time to time, even long after we have finished to work for our customer. We cannot hard-code directory names into our application. They can be subject to change anytime, and with hard-coded names, the program code would also have to be changed every time. On the other hand, the directory names have to be introduced somewhere in the code! So, how do we accomplish making changes to the code without changing the code ? To meet this requirement, two steps have to be taken. Two Steps to Flexibility ======================== Step 1: Our application has to become flexible - that means, it must not contain literal directory names anywhere! File names in the code have to be preceded by drive and directory name as explained, but instead of hard-coding the literal directory name ('RUN This IN F:\RBASE\RBPROGS\That.APX'), we should use expressions like 'RUN This IN &vPrg', with the variable vPrg containing the file name preceded by the currently valid program directory name. To connect the database, we need a similar variable with the current database directory and the database name, and for the temporary files, we need a third variable containing the name of the private directory and the temporary file name. Step 2: We have to make these variables directly accessible from the 'outside world'. The currently valid directory names have to be supplied somehow by the system operator, but how? We wouldn't want him to change anything in the code. Ideally, he should keep (and, if necessary, change) the three names somewhere entirely outside of the code. The code should 'know' where the names are kept and read them into variables during the start program. There are a number of ways to realize this. Where to Keep Directory Names ============================= At the first glance, the database itself may look like the logical place to keep the current directory names. We could place three rows in a special table which could be called 'SysDir'. These rows would contain the three directory names, and the first thing for the application to do would be to read them into global variables like vPrg. If necessary, changing the rows would be a simple task for the operator. But there is one problem: To evaluate the SysDir entries, the application has to connect to the database first, so the database directory name must be known before connecting. Under these circumstances, keeping the database directory name in the database itself would not make sense. There is a much better and even simpler approach: Put the three directory names (as we have shown, only three directories will be necessary) into DOS system variables using the SET command, and evaluate these variables in your application using the R:BASE ENVVAL function. To apply changes to DOS variables, the system operator simply has to modify one single batch file containing the three SET commands. This batch file, residing somewhere on the server, can then be called from the workstations' AUTOEXEC.BAT or from the batch file that starts our application. If you want the directory for the temporary files to be specific for each workstation, place the SET commands directly into the workstations' AUTOEXEC instead (or into the local batch file that starts the application), or keep them in user specific batch files on the server which can be called from the workstations. An Example Startup Batch File ============================= As explained above, we need three directories and a system variable for each of them. To declare system variables, we need the SET command. For each directory, we have to issue one SET. The SET commands can be issued from the DOS prompt, but, for the sake of simplicity, we will include them in the batch file that starts our application. This batch file - let's call it TESTAPP.BAT - should be located in the private directory. It will begin with the three following lines (be sure there are no blanks around the '=' equals symbol): SET RBTEMP=C:\RBTEMP SET RBDB=F:\RBASE\DBFILES SET RBPROGS=F:\RBASE\RBPROGS Besides this, the batch file contains the application start command. The R:BASE program directory must be part of the DOS search path, of course, and the program that comes starts the application - called STARTAPP - should also be in the private directory. RBASE -R STARTAPP (Before starting the application for the first time, you might want to check if the three new DOS system variables have been set correctly. Issue the three SET commands without starting R:BASE, then issue SET without parameters to check the environment that contains the variables. If the variables turn out to be crippled, you have to enlarge the DOS environment size.) Example Directories =================== C:\RBTEMP is our local private directory. In our example, it is also be our working directory. \RBTEMP contains the RBASE.CFG file, the batch file TESTAPP.BAT that is used to launch our application, and the startup program for the application itself, STARTAPP. This startup program contains lines of code that evaluates the current value of the three new system variables. \RBTEMP is also used as the wastedump for temporary files we might have to create during runtime. F:\RBASE\DBFILES is our example database directory. It is located somewhere on the server and contains nothing but the three RBF files. F:\RBASE\RBPROGS, also residing on the server, contains everything else, that is, all .APX files, .PRD files and all sorts of auxiliary stuff. This directory, as explained above, might be write-protected. Evaluating System Variables in the Startup Program ================================================== The startup program, STARTAPP, has to make sure that the three system variables are set. If they aren't, the application cannot run. If they are, the values of the DOS variables are loaded into global R:BASE variables. These variables have to be kept alive during the application, so don't use a CLEAR ALL VAR anywhere, or if you do, specify the three variables in the EXCEPT list. SET ERROR VAR vError SET V vErr INTEGER = 0 SET V vRBTemp TEXT = (ENVVAL('RBTemp')) SET V vErr = (.vErr + .vError) SET V vRBProgs TEXT = (ENVVAL('RBProgs')) SET V vErr = (.vErr + .vError) SET V vRBDB TEXT = (ENVVAL('RBDB')) SET V vErr = (.vErr + .vError) IF vErr <> 0 THEN CLS CLS FROM 1 TO 2 RED WRITE ' System variables are not set correctly!' AT 1 1 + WHITE ON RED WRITE ' Please contact your system manager.' AT 2 1 + WHITE ON RED PAUSE 1 EXIT ENDIF If all went well, we now have three global variables containing the directory names. Before starting the application itself, there are some preparations to do. Change into the Working Directory ================================= We recommend choosing the private directory on the local drive as your working directory. If you do start your application from there, you don't have to change directories first, because you already are in the correct directory. Just to make sure, however, you should include some code that does change into RBTEMP first. Then you will have a reliable starting point under all circumstances. Consider that the directory to change to could be on another drive. Another difficulty is that the CHDIR command doesn't work when it finds a trailing backslash in the name of the target directory, unless the target is a root directory. SET V vLen INTEGER = (SLEN(.vRBTemp)) SET V vColon INTEGER = (SLOC(.vRBTemp,':')) IF vColon <> 0 THEN --change drive if drive name found: SET V vDrive TEXT = (SGET(.vRBTemp,.vColon,1)) &vDrive IF vError <> 0 THEN CLS FROM 1 TO 1 RED WRITE 'Error changing drive to',.vDrive AT 1 1 WHITE ON RED PAUSE 1 EXIT ENDIF ENDIF SET V vLastChr TEXT = (SGET(.vRBTemp,1,.vLen)) IF vLastChr = '\' AND vLen > 3 THEN --remove trailing backslash --if not root dir SET V vLen = (.vLen - 1) SET V vRBTemp = (SGET(.vRBTemp,.vLen,1)) ENDIF SET V vRBTemp = (LUC(.vRBTemp)) --change directory CD &vRBTemp IF vError <> 0 THEN CLS FROM 1 TO 1 RED WRITE 'Error changing directory to', .vRBTemp AT 1 1 + WHITE ON RED PAUSE 1 EXIT ENDIF Preparing the Directory Variables ================================= After a successful change directory, we add a backslash to the working directory variable. This will make it easier to add filenames later in the program. The same applies to the two other variables. IF vLen > 3 THEN SET V vRBTemp = (.vRBTemp + '\') ENDIF SET V vRBProgs = (LUC(.vRBProgs)) SET V vLen INTEGER = (SLEN(.vRBProgs)) SET V vLastChr TEXT = (SGET(.vRBProgs,1,.vLen)) IF vLastChr <> '\' THEN SET V vRBProgs = (.vRBProgs + '\') ENDIF The name of the database itself must also be kept in a variable! Let's assume our database is called TEST. When working from another directory, a simple CONNECT TEST won't be sufficient anymore, of course. We create another global variable called vDB for the database name, preceded by the database directory name, and connect with this. SET V vRBDB = (LUC(.vRBDB)) SET V vLen INTEGER = (SLEN(.vRBDB)) SET V vLastChr TEXT = (SGET(.vRBDB,1,.vLen)) IF vLastChr = '\' THEN SET V vLen = (.vLen - 1) SET V vRBDB = (SGET(.vRBDB,.vLen,1)) ENDIF SET V vDB TEXT = (.vRBDB + '\TEST') If the database cannot be opened for some unknown reason, the SQLCODE is usually -7 regardless of the situation. Should this happen, show the 'Unable to connect database' message issued by R:BASE, and leave. SET MESSAGE ON SET ERROR MESSAGE ON CONNECT &vDB IF SQLCode = -7 THEN PAUSE 1 EXIT ENDIF SET MESSAGE ON SET ERROR MESSAGE ON (The database name itself, if you don't want it to appear anywhere in the code, could be taken from another DOS variable; however, usually this won't be necessary.) Working Within Variable Directories =================================== By having your variables ready, you can now apply some necessary changes to the code. It may seem like a difficult task to juggle with three directories in a program, but the rules are quite simple, even mechanical, as we have seen. (If you have to translate a major-sized single-directory-application into a variable-directory-application, you may try to devise some word processor macros to do most changes.) Here are some guidelines: 1. Don't use literal filenames in any RUN, QUIT TO, or ZIP. Instead, use these commands with variables, that is, filenames preceded by their directories. For example, use SET VAR vAPX = (.vRBProgs + 'TestMain.APX') RUN Thisthat IN &vAPX instead of the traditional RUN ThisThat IN TestMain.APX anywhere in your program. 2. Although it is recommended to create temporary files in your (private) working directory (which should be your current directory) you should add the RBTemp directory variable to the temp file names just to make sure. For example, use SET VAR vTempMnu = (.vRBTemp + 'TEMP.MNU') OUTPUT &vTempMnu instead of the traditional OUTPUT TEMP.MNU or use SET VAR vSnap = (.vRBTemp + 'Screen.Tmp') SNAP &vSnap instead of SNAP Screen.Tmp 3. Remember for EEPs in your forms. RUN EEP1 IN EEPS.APX by RUN EEP1 IN &vEEPS in all your EEP calls. The variable vEEPs has to be declared either in the form itself, or in every program using the form that calls the EEP. 4. UDF's are also program files. Everything described here applies to UDF's as well. So, replace UDF calls like SET VAR vFExists = (UDF('FILE.EXE', 'Testfile')) by SET VAR vUDFFile = (.vRBProgs + 'FILE.EXE') SET VAR vFExists = (UDF(.vUDFFile, 'Testfile')) Actually, the filename 'Testfile' that is looked for in this specific example would also have to preceded by a directory name! We really have to be careful. 5. Programs containing the R:BASE BACKUP/RESTORE commands have to be given special treatment. RESTORE always restores into the current directory, so the current directory has to be changed from RBTEMP to RBDB, and back after the restoration. We have already seen how to do that in principle. 6. Check if there are any CLEAR ALL VARs in your program. If so, include the directory and database variables in the EXCEPT list. Conclusion ========== Writing network applications doesn't have to be more difficult than writing small, single-user programs. Most concurrency regulations are already solved for us. R:BASE takes care of them and we usually don't even notice. However, a small set of special network rules and guidelines for database and program design should be taken into account. One recommendation is: Keep your application directory structure flexible. As we have seen, this can be achieved quite simply, thanks to R:BASE flexibility.