Tuesday, July 26, 2016

Using calculated columns:


Inform and RetrieVe are great tools for performing calculations on data. There are times when calculations cannot be performed on the data contained in single records, but instead requires the data from more than one record to complete some calculation. For example, if the database contains records denoting the begin and end times of events and one is tasked with determining the total time BETWEEN events, there is no method for performing this that doesn't require some programming.
Lately instead of writing one-off programs for such tasks I have been using I-types to call subroutines. Below is an example:

Note: All programming displayed on this site is for illustrative purposes only. Use at your own risk.

Here is a sample set of my test data containing events and their respective start date, start time, and end time:



>LIST EVENTS WITH DATE GE "01/01/2016" BY DATE BY START.TIME BY @ID DATE START.TIME END.TIME HDR.SUP COL.SPCS SAMPLE 10

EVENT. DATE.... START... END.....

160001 01/22/16 15:07:28 15:13:35
160002 01/22/16 15:13:39 15:14:31
160003 01/22/16 15:29:57 15:32:03
160004 01/22/16 15:32:22 15:34:39
160005 01/22/16 16:04:44 16:06:04
160006 01/27/16 11:21:34 11:22:13
160007 02/19/16 16:35:24 16:36:55
160008 04/01/16 13:27:55 13:32:58
160009 04/01/16 13:29:23 13:33:04

160010 04/04/16 08:22:41 08:57:05

Sample of 10 records listed.
>



A new I-type that will output the elapsed time between the current and previous event will be useful if different queries (selection criteria, sort criteria, etc.) will be run. For example:



>LIST DICT EVENTS "DOWNTIME" F1 F2 F3 F4 F5 F6 F7

Field}Name. DOWNTIME
F1......... I Calculate time between calls
F2......... SUBR('DOWNTIME',DATE,START.TIME,END.TIME)
F3.........
F4......... Downtime}Days:HH:MM:SS
FORMAT..... 13R
F6......... S
F7.........

1 records listed.
>



The corresponding subroutine that is referenced in the above I-type looks like the following:



subroutine downtime(out,begin.date,begin.time,end.time)
common /downtime/ prev.end.date,prev.end.time
out=""
if @RECCOUNT <= 1 then ;* Reinitialize when new query
   prev.end.date=end.date
   prev.end.time=end.time
end else
   numdays=(begin.date-prev.end.date) ;* Number of days
   if begin.date > prev.end.date then ;* If spans midnight
      new.time=86400-prev.end.time    ;* Previous end time to midnight
      end.time.orig=end.time
      prev.end.time=new.time
      out=numdays:":":oconv(prev.end.time,"MTS")
      prev.end.time=end.time.orig
      prev.end.date=end.date
   end else
      if begin.time > prev.end.time then
         numhrs=begin.time-prev.end.time
      end else numhrs=0
      out=numdays:":":oconv(numhrs,"MTS")
      prev.end.date=end.date
      prev.end.time=end.time
   end
end
return
end



Rerunning the same query but with the new I-type produces the elapsed time between events (with overlapping events indicating zero downtime):


>LIST EVENTS WITH DATE GE "01/01/2016" BY DATE BY START.TIME BY @ID DATE START.TIME END.TIME DOWNTIME HDR.SUP COL.SPCS SAMPLE 10

                                  Downtime
EVENT. DATE.... START... END..... Days:HH:MM:SS
160001 01/22/16 15:07:28 15:13:35    0:00:00:00
160002 01/22/16 15:13:39 15:14:31    0:00:00:03
160003 01/22/16 15:29:57 15:32:03    0:00:15:25
160004 01/22/16 15:32:22 15:34:39    0:00:00:19
160005 01/22/16 16:04:44 16:06:04    0:00:30:05
160006 01/27/16 11:21:34 11:22:13    5:07:53:55
160007 02/19/16 16:35:24 16:36:55   23:12:37:47
160008 04/01/16 13:27:55 13:32:58   42:07:23:04
160009 04/01/16 13:29:23 13:33:04    0:00:00:00
160010 04/04/16 08:22:41 08:57:05    3:10:26:56

Sample of 10 records listed.
>



Notice that there is no downtime for the first incident as there is no prior or previous event.



Monday, February 15, 2016

Semaphores

Recently I have had the opportunity to delve into UniVerse semaphores. While troubleshooting unusual system hangs on one server we found that when programs (including subroutines and functions) exit, any dynamic file variables (handles) that were opened and not in a common will cause UniVerse to lock a semaphore to check for splitting or merging groups. This behavior is not new and has never been a problem before. However since upgrading from UniVerse 10.3 to 11.1.0 and above we encountered the occasional system hang. Between ours and Rocket Software's engineers we isolated the problem to a utility subroutine that every user executes when logging into one of the main product accounts. The routine opens a dictionary (that happens to be dynamic) and reads a record to set a common variable for subsequent use by our products. Upon exiting the subroutine UniVerse performs its house keeping (locking the semaphore and checking if a split or merge operation is called for). Apparently during the house keeping some kind of deadlock situation was encountered (multiple processes all contending for the same semaphore lock). Sometimes when this occurred, after 30 minutes or so uvcleanupd would wake up and clear the locks.

The formula used by UniVerse to calculate the semaphore number for file access is:
MOD((groupaddress + inode#), GSEMNUM) + 1

For large files (with a large modulo) many users accessing many different records (likely hashing to different groups) should rarely contend for the same semaphore. However a relatively small dynamic file with a small modulo and with many processes accessing the same record ID the results of this formula may always return the same value. It was the latter scenario that this system (with over 600 users) was experiencing. The obvious solution (a work around really) was to resize the dictionary to be a static hashed file instead of a dynamic file.

While this type of lock contention can be avoided by changing the file type (an easy decision for a file dictionary that rarely changes) there is another way that users can be blocked from logging into UniVerse. It turns out that while the delivered uvlictool license tool (under UVHOME/bin) is running no one can login. If executed without piping to something that pauses the output then access is blocked for a short time... maybe one or two seconds at most. However if the command is piped to more or pg etc. (or anything that can momentarily prevent the command from completing) then no new user will be able to invoke the uvsh (or uvdls) shell to completion! So the next time you're checking the current license count, or monitoring workstations with multiple sessions (uvdls) and pause the uvlictool output be sure not to leave your desk for a break or leave work for the day. Also if you have a script that executes uvlictool and runs automatically take care that it does not run too frequently and make sure that the output path doesn't have the potential to result in pausing the uvlictool command.

Sunday, February 22, 2015

Looking For Trouble:

In my August 2014 post I discussed the UniVerse errlog and how to quickly display the most current entries. Recently while trying to aggregate the types of messages in the error log and having difficulty doing so, a coworker of mine came through with the final OS commands to display an aggregate list of distinct "program error" type entries in the log. Though the script and the results are simple, and not an example of database programming, having a way to collate and display the most prevalent program errors should be of interest to any programmer.

Note: All programming displayed on this site is for illustrative purposes only. Use at your own risk.


cat `cat /.uvhome`/errlog|awk -F"Program" '{ print $2 }'|sort|uniq -c|sort -rn


On a production system configured with a 10,000 line UniVerse error log, running this script has the following example output:


$ strings `cat /.uvhome`/errlog|awk -F"Program" '{ print $2 }'|sort|uniq -c|sort -rn
3454 "INVOICE.POSTING.SUB": Line 259, Variable "ctn" previously undefined. Empty string used.
3454 "INVOICE.POSTING.SUB": Line 261, Variable "per" previously undefined. Empty string used.
1530 "REPORT.DAILY": Line 425, Variable "OTP" previously undefined. Zero used.
600 "REPORT.DAILY": Line 326, Variable "USD" previously undefined. Empty string used.
600 "REPORT.DAILY": Line 303, Variable "OTP" previously undefined. Zero used.
126 "*FIN*GENLIST": Line 4826, Variable "VALUE" previously undefined. Empty string used.
78 "*FIN*GENLIST": Line 4817, Variable "VALUE" previously undefined. Empty string used.
39
24 "INVOICE.PRINT.SUB": Line 749, Variable "seqno" previously undefined. Empty string used.
24 "INVOICE.PRINT.SUB": Line 743, Variable "seqno" previously undefined. Empty string used.
13 "MEMPASS": Line 2043, Array index out of bounds.
10 "MEMPASS": Line 1947, Array index out of bounds.
8 "DISP.FILE.REL": Line 1041, Variable "select.item" previously undefined. Empty string used.



Seventy percent of the errors in the UniVerse error log are from a single program (in this example). Fixing the two errors would alleviate the majority of the log errors, and allow for longer periods of time to be logged (since the file is circular). Similar filtering and sorting could be applied to other program log files (usually in the &COMO& file) as well.

Saturday, January 31, 2015

An Exercise In Recursion:

I rarely have the need to create a recursive program, but I have always been interested in the concept. My introduction to recursion was in my COC3110 class at the University of Florida in 1983. The problem was called "Eight Queens". The challenge was to place eight queens on a chessboard without ever being in danger of being taken by another (previously placed) queen.
This is a great example of a problem requiring "brute force". There is no fast and logical formula to determine where the eight queens should be placed. They just have to be placed on the chessboard, one at a time. Coding this by hand would get tedious and confusing after the first several placements. Fortunately that's just the kind of thing computers are built for!

Here is a flow chart for the "Eight Queens" problem:

Thus far, I have only had the opportunity to develop recursive programs when I need to document a complex structure of nested objects with any number of levels (or depth) possible. An example where recursion is a good programming choice would be in a system that finds links and relationships among a database of criminals, using some common criteria like phone numbers and known locations. For this exercise I will show how to display everything "under" a menu that conforms to the UniVerse menu subsystem.

Start with the following series of menus, paragraphs, and programs:

Note: All programming displayed on this site is for illustrative purposes only. Use at your own risk.



>.L MAIN.MENU

     MAIN.MENU
001 M
002 APP.MENUS
003 MAIN.MENU

>MENUS
...

DISPLAY.MENU: CONTENTS OF THE  MENU =--> MAIN.MENU ON =--> APP.MENUS

DESCRIPTION.............................    ACTION..............
Data Entry                                  DATA.ENTRY
Reports                                     REPORTS
Administration                              ADMINISTRATION

1 records listed.

DISPLAY.MENU: CONTENTS OF THE  MENU =--> DATA.ENTRY ON =--> APP.MENUS

DESCRIPTION.............................    ACTION..............
A/R Data Entry                              AR.DATA.ENTRY
A/P Data Entry                              AP.DATA.ENTRY

1 records listed.

DISPLAY.MENU: CONTENTS OF THE  MENU =--> REPORTS ON =--> APP.MENUS

DESCRIPTION.............................    ACTION..............
A/R Detail Report                           AR.DETAIL.REPORT
A/R Summary Report                          AR.SUMMARY.REPORT
A/P Detail Report                           AP.DETAIL.REPORT
A/P Summary Report                          AP.SUMMARY.REPORT

1 records listed.

DISPLAY.MENU: CONTENTS OF THE  MENU =--> ADMINISTRATION ON =--> APP.MENUS

DESCRIPTION.............................    ACTION..............
Table Maintenance                           TABLE.MAINT.MENU
Printer Maintenance                         PRINTER.MAINT.MENU
User Maintenance                            USER.MAINT.MENU

1 records listed.

DISPLAY.MENU: CONTENTS OF THE  MENU =--> TABLE.MAINT.MENU ON =--> APP.MENUS

DESCRIPTION.............................    ACTION..............
Import from spreadsheet                     IMPORT.FROM.SPREADSHEET
Table Data Entry                            TABLE.DATA.ENTRY

1 records listed.

DISPLAY.MENU: CONTENTS OF THE  MENU =--> PRINTER.MAINT.MENU ON =--> APP.MENUS

DESCRIPTION.............................    ACTION..............
Set Default Printer                         SET.DEFAULT.PRINTER
Set Report Printer                          SET.REPORT.PRINTER
Set Check Printer                           SET.CHECK.PRINTER
Configure Default Printer                   CONFIG.DEFAULT.PRINTER
Configure Report Printer                    CONFIG.REPORT.PRINTER
Configure Check Printer                     CONFIG.CHECK.PRINTER

1 records listed.

DISPLAY.MENU: CONTENTS OF THE  MENU =--> USER.MAINT.MENU ON =--> APP.MENUS

DESCRIPTION.............................    ACTION..............
Add User                                    ADD.USER
Modify User                                 MODIFY.USER
Delete User                                 DELETE.USER

1 records listed.



Admittedly the following program is overkill if the task at hand is to determine what a single menu or series of menus is doing. Even going through each of the menus, sub-menus, VOC items, and programs manually (with the EDitor) would be quicker than writing a program. Where this proves helpful is if the same type of task needs to be performed over and over again, with differently configured systems.



SUBROUTINE MENU.CRAWLER (out, atid, atrecord, offset, menu.cnt, menu.level)
*
* This is meant to be run from a VOC or VOCLIB I-type in a production account.
* The program will start with the paragraph or menu indicated by the @ID and
* recursively drill down and parse through any and all menus and paragraphs in
* a report and display the "external" components (paragraphs, programs, etc.).
*
* For example:
* LIST VOC "MAIN.MENU" EVAL 'SUBR("MENU.CRAWLER",@ID,@RECORD,0,1,1)' FMT "132L" ID.SUP COL.SUP HDR.SUP
*
* Arguments
*  out       : I-type display text
*  atid      : @ID is the record key
*  atrecord  : @RECORD is the menu/paragraph contents
*  offset    : Indentation factor
*  menu.cnt  : Menu item number
*  menu.level: Anything greater than one is not a menu-level item
*
com /DISPMENU/ init,F.VOC,F.MENUS,F.NEWACC

equ true to 1
equ false to 0

default.menu.file = "APP.MENUS"
if not(init) then
   *
   * The first time through open files and initialize the output variable...
   *
   init = 1
   out = ""
   open '','VOC' to F.VOC else out<-1> = "Unable to open VOC!"; return
   open '',default.menu.file to F.MENUS else out<-1> = "Unable to open ":default.menu.file:"!"; return
   open '','NEWACC' to F.NEWACC else out<-1> = "Unable to open NEWACC!"; return
end
convert @VM to "" in atrecord ;* don't break on embedded valuemarks in column headings in LIST statements
first.line.processed = false  ;* keep track of whether or not the first line of object has been processed

*
* Loop through the object, line by line, looking for known objects (menus, paragraphs) to drill down into...
*
loop
   remove line from atrecord setting more 
   line = trim(line)
   if len(line) then
      *
      * Account for everything in the object we DO NOT want to display...
      *
      begin case
         case line[1,2] = "* " or line = "*"
         case upcase(line) matches "'IF '1X0X"
         case upcase(line) matches "'GO '1X0X"
         case upcase(line) matches "1A0X':'0X" ;* Internal labels
         case upcase(line) matches "'DISPLAY '1X0X" or upcase(line) = "DISPLAY"
*        case upcase(line[1,7]) = "SELECT " ;* This may be something TO display
         case upcase(line[1,11]) = "CLEARSELECT"
         case upcase(line[1,5]) = "DATA "
         case upcase(line[1,5]) = "SLEEP"
         case upcase(line[1,1]) = "M" and not(first.line.processed) ;* Object is a menu
            *
            * Drill down into the menu...
            *
            first.line.processed = true 
            remove menu.file.name from atrecord setting dummy       ;* Line two of menu record
            remove menu.name from atrecord setting more             ;* Line three of menu record
            menu.read.error = false
            if menu.file.name = default.menu.file then
               read menu.record from F.MENUS, menu.name else
                  menu.read.error = true
                  out<-1> = "Unable to read ":default.menu.file:" '":menu.name:"'!"
               end
            end else
               *
               * This is not the default menu file. Open it and read the menu record...
               *
               open '',menu.file.name to F.TEMP.MENU then
                  read menu.record from F.TEMP.MENU, menu.name else
                     menu.read.error = true
                     out<-1> = "Unable to read ":menu.file.name:" '":menu.name:"'!"
                  end
               end else
                  menu.read.error = true
                  out<-1> = "Unable to open ":menu.file.name:"!"
               end
            end
            if not(menu.read.error) then
               *
               * Display the menu object...
               *
               menu.line = menu.cnt:" ":trim(menu.record<1>):" (":trim(menu.name):") (m)"
               out<-1> = space(offset):menu.line
               out<-1> = space(offset):str("=",len(menu.line))

               *
               * Iterate through the menu items...
               *
               menu.items = dcount(menu.record<3>,@VM)
               for valcnt = 1 to menu.items
                   menu.item.name = menu.record<2,valcnt>
                   menu.item.cmd = menu.record<3,valcnt>
                   read dummy from F.NEWACC, menu.item.cmd then
                      *
                      * Command is a UniVerse delivered keyword or verb. Display it...
                      *
                      out<-1> = space(offset):menu.cnt:".":valcnt:" ":trim(menu.item.cmd)
                   end else
                      *
                      * Command is NOT a UniVerse delivered keyword or verb...
                      *
                      read voc.item from F.VOC, menu.item.cmd then
                         *
                         * Recursively drill down into the VOC item, increasing the indentation.
                         * Pass a literal one as the last argument to force the prefix of a menu #.
                         *
                         call MENU.CRAWLER(out, menu.item.cmd, voc.item, offset+2, valcnt, 1)
                         if false and out<dcount(out,@FM)> <> "" then ;* Remove false-flag for space between menu items
                            out<-1> = "" ;* blank line between menus
                         end
                      end else out<-1> = "Unable to read VOC, '":menu.item.cmd:"'!"
                   end
               next valcnt
            end
            more = false

         case upcase(trim(line[1,2])) = "PA" and not(first.line.processed)
            *
            * Display the paragraph name, and continue displaying any subsequent lines of interest...
            *
            first.line.processed = true
            if menu.level < 2 then prefix = menu.cnt:" " else prefix = ""
            out<-1> = space(offset):prefix:atid:" (pa)"

         case upcase(line)[1,1] = "V" or upcase(line)[1,1] = "S" and not(first.line.processed)
            *
            * Verbs and Sentences always have the command in field two...
            *
            first.line.processed = true
            if menu.level < 2 then prefix = menu.cnt:" " else prefix = ""
            out<-1> = space(offset):prefix:atid:" (":downcase(line)[1,1]:")"
            if atrecord<2> matches "'*'1X0X'*'1X0X" then
               cmd = field(atrecord<2>,"*",3)
            end else cmd = atrecord<2>
            out<-1> = space(offset+2):" ":cmd
            more = false

         case 1
            *
            * Likely the rest of a paragraph. Look for standard verbs or paragraphs...
            *
            paragraph.cmd.word = field(line," ",1)
            read dummy from F.NEWACC, paragraph.cmd.word then
               *
               * Command is a UniVerse delivered keyword or verb. Display it...
               *
               if len(line)+offset+2 > 100 then 
                  line = line[1,97-(offset+2)]:"..." ;* indicate there is more that is not displayed
               end
               out<-1> = space(offset+2):line
            end else
               read voc.item from F.VOC, paragraph.cmd.word then
                  *
                  * Recursively drill down into the VOC item. Indicate another level deeper
                  *
                  call MENU.CRAWLER(out, paragraph.cmd.word, voc.item, offset+2, menu.cnt, menu.level+1)
               end else
                  if len(line)+offset+2 > 100 then
                     line = line[1,97-(offset+2)]:"..." ;* indicate there is more that is not displayed
                  end
                  out<-1> = space(offset+2):line        ;* Don't know what it is, but display it
               end
            end            
      end case
   end
   until not(more)
repeat
go exit

Exit:
*----
if offset = 0 then init = 0 ;* force reinitialization for subsequent runs (in the same session)
return

end



Based on the various menus, paragraphs, and programs that make up all of the nested structures "under" the main menu, the output of this subroutine is:




>LIST VOC "MAIN.MENU" EVAL 'SUBR("MENU.CRAWLER",@ID,@RECORD,0,1,1)' FMT "132L" ID.SUP COL.SUP HDR.SUP COUNT.SUP

1 Main Menu (MAIN.MENU) (m)
===========================
  1 Data Entry (DATA.ENTRY) (m)
  =============================
    1 AR.DATA.ENTRY (s)
       WINDOW AR.ENTRY
    2 AP.DATA.ENTRY (s)
       WINDOW AP.ENTRY
  2 Reports Menu (REPORTS) (m)
  ============================
    1 AR.DETAIL.REPORT (pa)
      SET.REPORT.PRINTER (s)
         SETPTR 0,80,58,1,1,1,BRIEF,NHEAD,NFMT,AT <<F(CONFIG.FILE,PRINTER.REPORT,1),Printer,1X0X>>
      LIST ACCOUNTS.RECEIVABLE @DETAIL BY FUND BREAK.ON FUND BY ACCOUNT BREAK.ON ACCOUNT BY CREDI...
    2 AR.SUMMARY.REPORT (pa)
      SET.REPORT.PRINTER (s)
         SETPTR 0,80,58,1,1,1,BRIEF,NHEAD,NFMT,AT <<F(CONFIG.FILE,PRINTER.REPORT,1),Printer,1X0X>>
      LIST ACCOUNTS.RECEIVABLE @SUMMARY BY FUND BREAK.ON FUND BY ACCOUNT BREAK.ON ACCOUNT BY CRED...
    3 AP.DETAIL.REPORT (pa)
      SET.REPORT.PRINTER (s)
         SETPTR 0,80,58,1,1,1,BRIEF,NHEAD,NFMT,AT <<F(CONFIG.FILE,PRINTER.REPORT,1),Printer,1X0X>>
      LIST ACCOUNTS.PAYABLE @DETAIL BY FUND BREAK.ON FUND BY ACCOUNT BREAK.ON ACCOUNT BY CREDI...
    4 AP.SUMMARY.REPORT (pa)
      SET.REPORT.PRINTER (s)
         SETPTR 0,80,58,1,1,1,BRIEF,NHEAD,NFMT,AT <<F(CONFIG.FILE,PRINTER.REPORT,1),Printer,1X0X>>
      LIST ACCOUNTS.PAYABLE @SUMMARY BY FUND BREAK.ON FUND BY ACCOUNT BREAK.ON ACCOUNT BY CRED...
  3 Administration (ADMINISTRATION) (m)
  =====================================
    1 Table Maintenance (TABLE.MAINT.MENU) (m)
    ==========================================
      1 IMPORT.FROM.SPREADSHEET (v)
         IMPORT.FROM.SPREADSHEET
      2 TABLE.DATA.ENTRY (v)
         TABLE.DATA.ENTRY
    2 Printer Maintenance (PRINTER.MAINT.MENU) (m)
    ==============================================
      1 SET.DEFAULT.PRINTER (s)
         SETPTR 0,80,58,1,1,1,BRIEF,NHEAD,NFMT,AT <<F(CONFIG.FILE,PRINTER.DEFAULT,1),Printer,1X0X>>
      2 SET.REPORT.PRINTER (s)
         SETPTR 0,80,58,1,1,1,BRIEF,NHEAD,NFMT,AT <<F(CONFIG.FILE,PRINTER.REPORT,1),Printer,1X0X>>
      3 SET.CHECK.PRINTER (s)
         SETPTR 0,80,58,1,1,1,BRIEF,NHEAD,NFMT,AT <<F(CONFIG.FILE,PRINTER.CHECK,1),Printer,1X0X>>
      4 CONFIG.DEFAULT.PRINTER (s)
         WINDOW DEFINE.PRINTER "DEFAULT"
      5 CONFIG.REPORT.PRINTER (s)
         WINDOW DEFINE.PRINTER "REPORT"
      6 CONFIG.CHECK.PRINTER (s)
         WINDOW DEFINE.PRINTER "CHECK"
    3 User Maintenance (USER.MAINT.MENU) (m)
    ========================================
      1 ADD.USER (v)
         ADD.USER
      2 MODIFY.USER (v)
         MODIFY.USER
      3 DELETE.USER (v)
         DELETE.USER




When there are repeated patterns with references to other data structures, and there is a varying number of nested references to have to contend with, then consider using recursion to do the heavy lifting.

Monday, November 10, 2014

Issues With Caché Administration On A Personal Computer
(Or how to administer Caché with your tablet)

This post will concentrate on issues I have run into and had to solve while administering the Intersystems Caché® DBMS. Since getting my first iPad in 2010 and Kindle Fire a few years ago I have been using my laptop and Macbook less while using the tablets (and iPhone) more. Some tasks are best suited using a telnet client, but as most would agree, using a telnet client can be painful on a tablet (worse on an iPhone). Fortunately all Unix servers and some of the DBMS I work with provide a web user interface to help administer the system. Caché is one that offers an excellent system administration web UI, that is built on a delivered pared-down version of Apache (called the Private Web Server or PWS). By default the PWS listens on port 57772 (versus the well known port 80). Allowing remote devices to access the PWS pages may require modifying any firewall that is running on the Caché system (either a Windows PC or Mac for the purposes of this article).

Web:

Browsing to the CSP pages that are hosted by a production Caché installation running on a Unix or Windows server is straightforward. Simply enter the URL in the Safari address bar. However, to do the same thing for an installation on a Windows PC (versus a Windows server, typically configured for HTTP services) likely requires opening up the Windows firewall to allow web traffic.

Note: The configuration and programming examples (including screenshots) displayed on this site are for illustrative purposes only. Use at your own risk.







This article assumes that there is no web server already running (IIS, Apache, etc.). Installing Caché automatically installs Apache2 on the PC. It is this program that will be added to the firewall.
On the Windows or Mac computer running Caché attempt to browse to the Management portal. This will establish that the Apache server is running.

Attempting the same thing (browsing to the Windows or Mac computer) from a tablet is likely to fail. To allow browsing to the Caché web UI from another device then add the web server program to the firewall.

Windows 7:

To do this, open the "Network and Sharing center" window in the Windows Control panel, and click "Windows Firewall".


(add cache.web.ui.login.and.windows.firewall.2.png )
Then click "Allow a program or feature through Windows Firewall". The window to allow programs should appear, and may look similar to the following:




At this point click the "Allow another program..." button, and browse to the InterSystems installation directory and drill down into the directories to find the httpd directory, and in the http directory select httpd.exe and click "Open":







The "Add a Program" dialog displays the Apache HTTP Server program in the list. At this point it may I recommend clicking the "Network location types..." button and choosing the home or work network (versus a public network), to provide the option to restrict access, depending on the network.

Click "Add" to close the "Add a Program" window, and then click "OK" in the "Allowed Programs" window to allow web access through the firewall.






At this point with your tablet web browser attempt to bring up the Management portal login page on the PC (or Mac) using its IP address:

http://IP-address:57772/csp/sys/UtilHome.csp

















At a minimum this is all that is required to be able to use your tablet (or any other network device with a web browser) from your home network. If the computer running the personal installation of Caché is behind a router (typical in a home office environment) and there is a need to remotely view the Caché CSP pages, then consider adding a rule in the router to allow remote access (obviously great care should be taken to ensure only authorized access is granted).





To see how the newly installed Apache server is configured (startup options), view Apache's properties. To do this, click Start and right-click Computer and select Manage. Expand Services and then find the web server in the list. Note that it is installed with the option to run as a service. Also note that it is initially set to run manually. You can change this to start automatically on Windows start-up.



































OS X Mavericks 10.9.x:

As with the Windows firewall, add the Intersystems Caché httpd binary to the firewall:





























Or, if when browsing to the computer and the firewall is enabled then when prompted on the Mac to add access to "cache" click "Allow":
Since ipfw has been deprecated (I'm running 10.9.2) I find the GUI firewall does not work consistently. As the Caché delivered https uses a non standard port the OS X firewall appears to still block incoming HTTP traffic (The application firewall does not allow for applications with non-standard ports (see this Apple KB article)).




Telnet:

I want to spend some time discussing telnet. When I teach database administration to customers at our annual user conference, I work in a GUI as much as possible. However, when I am administering a system (or troubleshooting a problem on a customer system) I ONLY work in telnet. The tools in the OS are extremely powerful and with the ability to string Unix commands together there is little reason to use a GUI unless you are unfamiliar working at the OS level.


An issue I have run into is a conflict with the Caché telnet service daemon and an existing telnet server. On my Windows development laptop there is already a UniVerse telnet service running. If I run the Caché telnet client (or any telnet client for that matter) and connect to the localhost (my laptop) the regular UniVerse login dialog appears:
(insert Cache.launcher.start.telnet.png)






There can be only one listener on port 23 (the standard telnet port). To resolve the conflict stop the running telnet server process and start the Caché telnet server process in its place.



























Perhaps a better option is to change the port number that the telnet server is listening on. For the Caché telnet server daemon that can be accomplished by bringing up the Management Portal and going System Administration->Configuration->Device Settings->Telnet Settings and changing the "Telnet Port Number" to a number different than the standard (23). Use netstat to see what ports are in use. Here is a screenshot of the telnet port number setting:





Note: The configuration and programming examples (including screenshots) displayed on this site are for illustrative purposes only. Use at your own risk.