Skip to main content

Customizing the Caché System

This chapter discusses several ways to customize and extend a standard Caché installation.

Using System Classes for National Language Support

Modern applications are often designed so that they can adapt to various languages and regions without engineering changes. This process is called internationalization. The process of adapting an application to a specific region or language by adding specific components for that purpose is localization.

The set of parameters that defines the user language, country and any other, special variant preferences is a locale. Locales specify the conventions for the input, output and processing of data. These are such things as

  • Number formats

  • Date and time formats

  • Currency symbols

  • The sort order of words

  • Automatic translation of strings to another character set

A locale often is identified by noting the language in use and its geographic region (or other variation). These are usually given by the International Standards Organization (ISO) abbreviations for languageOpens in a new tab and locationOpens in a new tab. For example, en-us can represent the conventions of the English language as it is used in the United States, and en-gb as English is used in Great Britain.

Note:

The Caché instances on all members of a mirror must have the same locale and collation. See the “Mirroring” chapter of the Caché High Availability Guide for more information.

The %SYS.NLS Classes

Caché supports localization via classes in the package %SYS.NLS (NLS refers to National Language Support). These classes contain the information Caché needs to adapt an internationalized program to its runtime circumstances. This section summarizes your options; for additional detail, see the class documentation for each class.

Note:

Using any of these classes, an application can obtain the values currently set for the system or the process. Changing the values associated with the process takes effect immediately. To change the system settings, your application must define a new locale with the appropriate values and direct Caché to start using the new locale.

%SYS.NLS.Locale

The properties in %SYS.NLS.LocaleOpens in a new tab contain information about the current locale that you might need to consult. Changing any of them will not affect any behavior of the system.

%SYS.NLS.Device

The class %SYS.NLS.DeviceOpens in a new tab contains some properties for the current device, not necessarily the device that was current when the object was instantiated.

Usually, the properties for a specific device are set when the device is opened. This guarantees that the correct translations will be used. It is possible to change the translation table once the device is open by changing the XLTTable property in the process instance of this class, but this is not recommended without a solid reason for doing so.

Other properties in %SYS.NLS.DeviceOpens in a new tab enable you to handle errors that occur during translations. By default, when a character cannot be handled by the current table, no error is triggered and the offending character is translated as a question mark (?). This character, called the replacement value or replacement string can be changed to any other string. Furthermore, instead of silently translating undefined characters, it is possible to issue an error. This behavior is called the default action, and the possible choices are:

  • 0 — Generate error

  • 1 — Replace the untranslatable character with the replacement value

  • 2 — Ignore the error and pass the untranslatable character through

There are separate properties for the input and output operations in the properties of this class:

  • InpDefaultAction

  • InpReplacementValue

  • OutDefaultAction

  • OutReplacementValue

%SYS.NLS.Format

The class %SYS.NLS.FormatOpens in a new tab contains properties that affect the behavior of $ZDATE() and related functions. These properties are inherited from the values defined for the current locale but can be altered at the process level without affecting other users. The properties DateSeparator and TimeSeparator, for example, hold the characters that separate date and time items respectively.

The documentation for $ZDATE, $ZDATEH, and $FNUMBER describes the effect of changing these values.

Locale Property

The Locale property in the class %SYS.NLS.FormatOpens in a new tab allows control of the “look” of values in the current process. For example:

  • If Locale is a empty string, the system default formats (usually US English) are in effect.

  • If Locale is a locale name such as rusw or csy8, the formats come from that locale.

  • If Locale is Current, the formats come from the system.

The property can be changed after the object is instantiated or by passing the desired locale to the %New() method as in the following:

 Set fmt = ##class(%SYS.NLS.Format).%New("jpnw")

These changes affect only the current process.

%SYS.NLS.Table

The class %SYS.NLS.TableOpens in a new tab can instantiate objects that reflect either the system default or the current process settings for the various categories of tables. A table is the basic NLS mechanism that allows application data to be accepted as input, ordered, and displayed in the format appropriate to the specified locale. As with %SYS.NLS.LocaleOpens in a new tab, changing any property of a system object will not affect the system. However, changing a property from a process object will cause the associated behavior to change immediately.

NLS tables can be classified into I/O and Internal tables. Each table type has its own set of related data:

I/O Tables

These tables translate between the basic underlying character set supported by the current locale in which the systems is operating and a foreign character set supported by some entity outside Caché. The locale character set might be, for example, Latin2 (more properly known as ISO 8859-2) and the foreign character set might be UTF-8, generally used to communicate with the Terminal. Thus, on output, a table like Latin2–to-UTF8 would be used and, on input, a reverse mapping table would be needed, UTF8–to-Latin2.

Although there are two tables involved here (one for input and another for output), these tables usually complement one another. For simplicity, when speaking of locale definitions and system defaults, Caché uses a single name for a pair of I/O tables. This name is usually the name of the foreign character set, with the tacit assumption that the other half is the locale character set. However, when creating custom tables, any name that conveys the meaning of the exchange can be chosen.

I/O tables are used in devices; in this case, the word device refers to any interface where Caché meets the external world and where translation is needed, including the process and system call interfaces.

  • Terminal

  • Other terminal connections

  • External files

  • Magnetic tape

  • TCP/IP connections

  • Connections to DSM-DDP and DTM-DCP systems

  • Printer

  • Caché processes

  • System call

Internal Tables

The internal tables also map strings of characters from the current local character set to some other value, but they are not intended to be used in communication with the external world. The internal tables identify characters that are part of:

  • Pattern matching

    Identify the characters that match certain pattern codes such as letters, numbers, punctuation, and so on.

  • Identifiers

    Identifier tables indicate which characters can be used in identifiers.

  • Uppercase, lowercase alphabets, and uppercase when used in titles.

    These similar in structure to the I/O tables; they map from one character set to another which just happens to be the same set. However, they are used in the context of $ZCONVERT(), not with some I/O operation.

  • Collation ordering

    These tables map a string of characters into an internal representation of that string suitable for use in global subscripts. Different languages have differing rules about how words should collate in dictionary order; these rules are encapsulated in a collation table.

  • $X/$Y action

    These tables map characters into values that indicate how they interact with the $X and $Y special variables. Should $X and/or $Y be incremented after this character is output? Is the character printable? These are questions that a $X/$Y table answers.

Note:

The list of available collations in any version of Caché is fixed. If your needs are not met by an existing collation, please contact the InterSystems Worldwide Support CenterOpens in a new tab for assistance.

Examples Using %SYS.NLS

Important:

These examples are all executable but none have a RunIt button, because they manipulate process-default values for the current locale. Also, many require administrative privileges and/or write access to the %SYS namespace. If you wish to execute them, please run them in a separate process, such as the InterSystems Terminal facility (Windows), or via a TCP/IP connection, and with the appropriate privileges.

Display Current Locale Information

This example displays information about the current system locale:

  Set Info = ##class(%SYS.NLS.Locale).%New()
  Set Items = "Name" _
              "/Description" _
              "/Country" _
              "/CountryAbbr" _
              "/Language" _
              "/LanguageAbbr" _
              "/Currency" _
              "/CharacterSet"
  
  Write !
  For i = 1 : 1 : $LENGTH(Items, "/")
  {
    Set Item = $PIECE(Items, "/", i)
    Write $JUSTIFY(Item, 15),": ", $PROPERTY(Info, Item), !
  }

Display Available Locales

This example displays information about the available locales:

  Znspace "%SYS"
  Set locales = ##class(%Library.ResultSet).%New("Config.NLS.Locales:List")
  If $IsObject(locales) {
    Set locales.RuntimeMode = 1
    Set sc = locales.Execute("*")
    If $SYSTEM.Status.IsOK(sc) {
      Write !
      While locales.Next() {
        Write locales.Data("Name"), " - ", locales.Data("Description"), !
      }
    }
  }

Display System and Process Table Data

This should display the same values for the system and process tables unless some properties have been externally altered before running this example.

  Set IOTables = "Process" _
                 "/CacheTerminal" _
                 "/OtherTerminal" _
                 "/File" _
                 "/Magtape" _
                 "/TCPIP" _
                 "/DSMDDP" _
                 "/DTMDCP" _
                 "/SystemCall" _
                 "/Printer"
  Set IntTables = "PatternMatch" _
                  "/Identifier" _
                  "/Uppercase" _
                  "/Lowercase" _
                  "/Titlecase" _
                  "/Collation" _
                  "/XYAction"
  
  // iterate over the systems, and then the process data
  For Type = "System", "Process"
  {
    Write !
    Set Table = ##class(%SYS.NLS.Table).%New(Type)
    Write "Type: ", Type, !
    
    Write "I/O Tables", !
    For i = 1 : 1 : $LENGTH(IOTables, "/")
    {
      Set PropName = $PIECE(IOTables, "/", i)
      Write $JUSTIFY(PropName, 15), ": ", $PROPERTY(Table, PropName), !
    }
    
    Write "Internal Tables", !
    For i = 1 : 1 : $LENGTH(IntTables, "/")
    {
      Set PropName = $PIECE(IntTables, "/", i)
      Write $JUSTIFY(PropName, 15), ": ", $PROPERTY(Table, PropName), !
    }
  }

Changing Date and Time Displays

The %SYS.NLS.FormatOpens in a new tab class contains the properties DateSeparator and TimeSeparator, for example, hold the characters used to separate the components of date and time items respectively. In the United States default locale, enu8 (or enuw for Unicode systems), these are the slash character (/) and the colon (:), respectively. The following example shows how these may be altered:

  // display the current defaults
  // date is 10 April 2005
  // time is 6 minutes 40 seconds after 11 in the morning
  Write $ZDATE("60000,40000"), !
  
  // now change the separators and display it again
  Set fmt = ##class(%SYS.NLS.Format).%New()
  Set fmt.DateSeparator = "_"
  Set fmt.TimeSeparator = "^"
  Write !, $ZDATE("60000,40000")

This following example changes the month names to successive letters of the alphabet (for demonstration purposes). To do this, it sets the property MonthName to a space-separated list of the month names. Note that the list starts with a space:

  // get the format class instance
  Set fmt = ##class(%SYS.NLS.Format).%New()
  
  // define the month names
  Set Names = " AAA BBB CCC DDD EEE FFF GGG HHH III JJJ KKK LLL"
  Set fmt.MonthAbbr = Names
  Set rtn = ##class(%SYS.NLS.Format).SetFormatItem("DATEFORMAT", 2) 
  
  // show the result
  Write $ZDATE(60000, 2)

Changing the Way Numbers Are Displayed

Some properties in %SYS.NLS.FormatOpens in a new tab control how numbers are interpreted by $Number(). In English locales, the decimal point is used to separate the integer from the fractional part of a number, and a comma is used to separate groups of 3 digits. This too can be altered:

  // give the baseline display
  Write $Number("123,456.78"), !
  
  Set fmt = ##class(%SYS.NLS.Format).%New()
  // use "/" for groups of digits
  Set fmt.NumericGroupSeparator = "."
  
  // group digits in blocks of 4
  Set fmt.NumericGroupSize = 4
  
  // use ":" for separating integer and fractional parts
  Set fmt.DecimalSeparator = ","
  
  // try interpreting again
  Write $Number("12.3456,78"), !

Setting the Translation for a File

The following shows that an application can control the representation of data written to a file.

  // show the process default translation (RAW, no translation performed)
  Set Tbl = ##class(%SYS.NLS.Table).%New("Process")
  Write "Process default translation: ", Tbl.File, !
  
  // create and open a temporary file
  // use XML for the translation
  Set TempName = ##class(%Library.File).TempFilename("log")
  Set TempFile = ##class(%Library.File).%New(TempName)
  Do TempFile.Open("WSNK\XML\")
  Write "Temp file: ", TempFile.CanonicalNameGet(), !
  
  // write a few characters to show the translation
  // then close it
  Do TempFile.WriteLine(("--" _ $CHAR(38) _ "--"))
  Do TempFile.Close()
  
  // now re-open it in raw mode and show content
  Do TempFile.Open("RSK\RAW\")
  Do TempFile.Rewind()
  Set MaxChars = 50
  Set Line = TempFile.Read(.MaxChars)
  Write "Contents: """, Line, """", !
  
  // finish
  Do TempFile.Close()
  Do ##class(%Library.File).Delete(TempName)
  Set TempFile = "" 

For more information on translation tables, see the section on “Three-Parameter Form: Encoding Translation” in the documentation for the $ZCONVERT function.

The Config.NLS Classes

Unlike %SYS.NLS, which is available everywhere and is intended for general use, the classes in Config.NLS can be used only in the %SYS namespace and only by a user with administrative privileges. Normally, administrators who need to create custom locales and tables would use the NLS pages in the Management Portal. Only users with very special requirements should need to use Config.NLS.

There are three classes in package Config.NLS:

  • Locales – Contain all the definitions and defaults for a country or geographical region.

  • Tables – Contain a high level description of tables, but not the mapping itself.

  • SubTables – Contain the character mappings proper and may be shared by more than one Table.

The main reason for having separate Tables and SubTables classes is to avoid duplication of data. It is possible to have Tables for different character sets that happen to share the same mappings and thus the same SubTable. Also, the classes in Tables define a default action and a replacement value (see description of these properties in %SYS.NLS above). Therefore, it is possible to have separate Tables in which these attributes are different even though they share the same SubTable. This flexibility adds some complexity in managing the correct relationships between Tables and SubTables, but the gains make it worthwhile. The separation of Tables from SubTables is kept hidden from users in the Management Portal and the %SYS.NLS classes, where all the housekeeping is done. However, when working with Config.NLS this needs to be done explicitly.

Conventions for Naming User-Defined Locales and Tables

To differentiate your custom items from the system items, and to simply upgrades, use a y at the start of the name of your items; for example: XLT-yEBCDIC-Latin1 and XLT-Latin1-yEBCDIC.

Caution:

User-defined tables, sub-tables and locales that do not follow this convention may be deleted during a system upgrade. The way to avoid this is to export user-defined tables and locales to XML files and re-import them after the upgrade.

When a custom SubTable is created from a copy of some InterSystems SubTable, the utilities that perform this task automatically use the same name and append a numeric suffix. Thus, copies of the Latin2-to-Unicode SubTable would be named XLT-Latin2-Unicode.0001 and XLT-Unicode-Latin2.0001, and so on.

Examples Using Config.NLS

This section presents the following examples:

Listing the Available Locales

This example uses a query to obtain a list of the available locale identifiers and descriptions. When Caché is installed, only the locales appropriate to the system are made available for use — 8-bit locales for systems that only support 8-bit characters and Unicode locales for systems that support multibyte characters.

  // use the query in Config.NLS to get the locales
  ZNspace "%SYS"
  Set Query = ##class(%Library.ResultSet).%New("Config.NLS.Locales:List")
  Set code = Query.Execute("*")
  If (##class(%SYSTEM.Status).IsError(code))
  {
    Do ##class(%SYSTEM.Status).DisplayError(Code)
    Quit
  }
  
  // display each of them in turn
  Write "Available locales and descriptions", !
  While (Query.Next(.code))
  {
    If (##class(%SYSTEM.Status).IsError(code))
    {
      Do ##class(%SYSTEM.Status).DisplayError(Code)
      Quit
    }
    Write Query.Get("Name"), ": ", Query.Get("Description"), !
  }

Listing the Tables in a Specific Locale

The following example shows the tables that make up the Unicode locale for United States English (if it is available).

  ZNspace "%SYS"
  
  // establish the locale identifier, try
  // United States - English - Unicode
  // United States - English - 8-bit
  Set Loc = "enuw"
  Do ##class(Config.NLS.Locales).Exists(Loc, .Ref, .Code)
  If (##class(%SYSTEM.Status).IsError(Code))
  {
    Set Loc = "enu8"
    Do ##class(Config.NLS.Locales).Exists(Loc, .Ref, .Code)
    If (##class(%SYSTEM.Status).IsError(Code))
    {
      Do ##class(%SYSTEM.Status).DisplayError(Code)
      Quit
    }
  }
  
  // get the local array of table names
  Write "Tables for locale: ", Loc, !
  Do Ref.GetTables(.Tables)
  Set Type = $ORDER(Tables(""))
  While (Type '= "")
  {
    Set Name = $ORDER(Tables(Type, ""))
    While (Name '= "")
    {
      Set Mod = $ORDER(Tables(Type, Name, ""))
      While (Mod '= "")
      {
        Write Type, " - ", Name, " - ", Mod, !
        Set Mod = $ORDER(Tables(Type, Name, Mod))
      }
      Set Name = $ORDER(Tables(Type, Name))
    }
    Set Type = $ORDER(Tables(Type))
  }

Creating a Custom Locale

This example will provide a template for creating a custom locale with a custom table. The custom table will translate between EBCDIC (the common form used in the US) and Latin-1 (ISO-8859–1). For more details, see the documentation for the respective classes.

As for any other table, first we need to get the definition for the character mappings. For this example we are using the data file from the web site http://source.icu-project.orgOpens in a new tab (International Components for Unicode). The relevant data fileOpens in a new tab is a text file with comment lines starting with a pound sign (#) and then a series of translation definition lines of the form:

<Uuuuu>  \xee |0

A small excerpt of the file looks like:

#
#UNICODE EBCDIC_US
#_______ _________
<U0000>  \x00 |0
<U0001>  \x01 |0
<U0002>  \x02 |0
<U0003>  \x03 |0
<U0004>  \x37 |0
<U0005>  \x2D |0
...

The lines indicate that Unicode character Uaaaa maps to EBCDIC character \xbb (where aaaa and bb are expressed in hexadecimal). We assume that the table is reversible and that EBCDIC character \xbb maps back to Unicode character Uaaaa. This allows us to create both sides (that is, EBCDIC-to-Latin1 and Latin1-to-EBCDIC) from the same data file in a single scan. Because the Unicode range is just from 0 to 255, this is actually a Latin-1 table.

The process first creates the SubTable object, then the Table, and finally the Locale. For the first step, the process creates two SubTables objects, initializes their Name and Type properties, and then fills in the FromTo mapping array with data read from the definition file.

SubTable names take the form, Type–FromEncoding–ToEncoding. The Type for regular I/O translations is “XLT” and so the SubTable names will be XLT-yEBCDIC-Latin1 and XLT-yLatin1-EBCDIC.

The following code creates the SubTables objects. In a real world program, the code would perform a number of consistency checks that omitted here for the sake of clarity. This example deletes an existing previous versions of the same objects (SubTables, Tables and Locales) so that you can run the example multiple times. More properly, you should check for the existence of previous objects using the class method Exists() and take a different action if they are already present.

  // Names for the new SubTables (save for later)
  Set nam1 = "XLT-Latin1-yEBCDIC"
  Set nam2 = "XLT-yEBCDIC-Latin1"

  // Delete existing SubTables instances with same ids
  Do ##class(Config.NLS.SubTables).Delete(nam1)
  Do ##class(Config.NLS.SubTables).Delete(nam2)

  // Create two SubTable objects
  Set sub1 = ##class(Config.NLS.SubTables).%New()
  Set sub2 = ##class(Config.NLS.SubTables).%New()

  // Set Name and Description
  Set sub1.Name = nam1
  Set sub1.Description = "ICU Latin-1->EBCDIC sub-table"
  Set sub2.Name = nam2
  Set sub2.Description = "ICU EBCDIC ->Latin-1 sub-table"

The SubTables object contains a property, type, that is a small integer indicating whether we are dealing with a multibyte translation or not. This example sets type to zero indicating a single-byte mapping. The mapping is initialized so that code points (characters) not defined in the data file are mapped to themselves.

  // Set Type (single-to-single)
  Set sub1.Type = 0
  Set sub2.Type = 0
  
  // Initialize FromTo arrays
  For i = 0 : 1 : 255
  {
    Do sub1.FromTo.SetAt(i, i)
    Do sub2.FromTo.SetAt(i, i)
  }

Next the application reads the file. Definitions in the file override those set as the default mapping. The function $ZHEX() converts the codes from hexadecimal to decimal.

  // Assume file is in the mgr directory
  Set file = "glibc-EBCDIC_US-2.1.2.ucm"

  // Set EOF exit trap
  Set $ZTRAP = "EOF"
  
  // Make that file the default device
  Open file
  Use file
  For
  {
    Read x
    If x?1"<U"4AN1">".E
    {
      Set uni = $ZHEX($E(x,3,6)),ebcdic = $ZHEX($E(x,12,13))
      Do sub1.FromTo.SetAt(ebcdic,uni)
      Do sub2.FromTo.SetAt(uni,ebcdic)
     }
  }

EOF  // No further data
  Set $ZT = ""
  Close file

  // Save SubTable objects
  Do sub1.%Save()
  Do sub2.%Save()

The character mappings are now complete. The next step is to create the Table objects that reference the SubTables objects just defined. Table objects are really descriptors for the SubTables and have only a few properties. The following code makes the connection between the two:

  // Delete existing Tables instances with same ids
  Do ##class(Config.NLS.SubTables).Delete("XLT", "Latin1", "yEBCDIC")
  Do ##class(Config.NLS.SubTables).Delete("XLT", "yEBCDIC", "Latin1")

  // Create two Table objects
  Set tab1 = ##class(Config.NLS.Tables).%New()
  Set tab2 = ##class(Config.NLS.Tables).%New()

  // Set description
  Set tab1.Description = "ICU loaded Latin-1 -> EBCDIC table"
  Set tab2.Description = "ICU generated EBCDIC -> Latin-1 table"

  // Set From/To encodings
  Set tab1.NameFrom = "Latin1"
  Set tab1.NameTo = "yEBCDIC"
  Set tab2.NameFrom = "yEBCDIC"
  Set tab2.NameTo = "Latin1"

  // Set SubTable
  Set tab1.SubTableName = nam1
  Set tab2.SubTableName = nam2

  // Set Type
  Set tab1.Type = "XLT"
  Set tab2.Type = "XLT"

  // Set Default Action
  // 1 = Replace with replacement value
  Set tab1.XLTDefaultAction = 1
  Set tab2.XLTDefaultAction = 1

  // Set Replacement value of "?"
  Set tab1.XLTReplacementValue = $ASCII("?")
  Set tab2.XLTReplacementValue = $ASCII("?")

  // Set Reversibility
  // 1 = Reversible
  // 2 = Generated
  Set tab1.XLTReversibility = 1
  Set tab2.XLTReversibility = 2

  // Set Translation Type
  // 0 = non-modal to non-modal
  Set tab1.XLTType = 0
  Set tab2.XLTType = 0

  // Save Table objects
  Do tab1.%Save()
  Do tab2.%Save()

With the Tables defined, the last step of the construction is to define a locale object that will incorporate the new tables. The application creates an empty Locale object and fills in each of the properties as was done for the Tables and SubTables. A Locale, however, is bigger and more complex. The easiest way to make a simple change like this is to copy an existing locale and change only what we need. This process uses enu8 as the source locale and names the new one, yen8. The initial y makes it clear this is a custom locale and should not be deleted on upgrades.

  // Delete existing Locales instance with the same id
  Do ##class(Config.NLS.Locales).Delete("yen8")
  
  // Open source locale
  Set oldloc = ##class(Config.NLS.Locales).%OpenId("enu8")
  
  // Create clone
  Set newloc = oldloc.%ConstructClone()
  
  // Set new Name and Description
  Set newloc.Name = "yen8"
  Set newloc.Description = "New locale with EBCDIC table"

With the locale in place, the process now adds the EBCDIC table to the list of I/O tables that are loaded at startup. This is done by inserting a node in the array property XLTTables, as follows:

XLTTables(<TableName>) = <components>
  • tablename identifies the pair of input and output tables for this locale.

    Because the name does not need to start with y, we use EBCDIC.

  • components is a four-item list as follows:

    1. The input “From” encoding

    2. The input “To” encoding

    3. The output “From” encoding

    4. The output “To” encoding

The following code adds the table to the list of available locales:

  // Add new table to locale
  Set component = $LISTBUILD("yEBCDIC", "Latin1", "Latin1", "yEBCDIC")
  Do newloc.XLTTables.SetAt(component, "EBCDIC")

If this locale will be frequently used, for example, for reading with EBCDIC magnetic tapes, the following code will set it as the default for this class of devices:

  // Set default for Magnetic Tapes
  Set newloc.TranMagTape = "EBCDIC"
  
  // Save the changes
  Do newloc.%Save()

Before the locale is usable by Caché, it must be compiled into its internal form. This is also sometimes called validating the locale. The IsValid() class method does a detailed analysis and returns two arrays, one for errors and one for warnings, with human-readable messages if the locale is not properly defined.

  // Check locale consistency
  If '##class(Config.NLS.Locales).IsValid("yen8", .Errors, .Warns)
  {
    Write !,"Errors: "
    ZWrite Errors
    Write !,"Warnings: "
    ZWrite Warns
    Quit
  }
  
  // Compile new locale
  Set status = ##class(Config.NLS.Locales).Compile("yen8")
  If (##class(%SYSTEM.Status).IsError(status))
  {
    Do $System.OBJ.DisplayError(status)
  }
  Else 
  { 
    Write !,"Locale yen8 successfully created."
  }

Using %Library.GlobalEdit to Set the Collation for a Global

The collation of newly created Caché globals is automatically set to the default collation of the database in which the global is created. The databases created by Caché installation are all set to the Caché standard collation, except USER, which is set to the default collation for the locale with which Caché is installed

After you create a database, you can edit its properties to change its default collation. You can select Caché standard, the default collation for the locale, or any other collation loaded in the instance. Once the default collation of the database is set, any globals created in this database are created with this default collation.

Caché also supports the ability to override this behavior and specify a custom collation for a global. To do this, use the Create() method in the class %Library.GlobalEditOpens in a new tab supplying the collation desired:

  Set sc = ##class(%Library.GlobalEdit).Create(ns,
                                               global,
                                               collation,
                                               growthblk,
                                               ptrblock,
                                               keep,
                                               journal,
                                               .exists)

where:

  • ns — Specifies the namespace, where "" indicates the current namespace, or ^^directoryname references a specific directory.

  • global — Specifies the global name, including leading ^, such as ^cz2.

  • collation — Specifies the collation, where collation is one of the supported collations.

  • growthblk — Specifies the starting block for data.

  • ptrblk — Specifies the starting block for pointers.

  • keep — Specifies whether or not to keep the global’s directory entry when the global is killed. Setting this to 1 preserves the collation, protection, and journal attributes if the global is killed.

  • journal — This argument is no longer relevant and is ignored.

  • exists — Specifies, by reference, a variable that indicates whether the global already exists.

In environments in which some globals require different collations from other globals, InterSystems recommends that you set up a database for each different collation, and that you add a global mapping within the namespace to map each global to the database with its required collation. This method allows mixed collations to be used without changing application code to specially use the Create() method call.

Supported Collations

The following are supported in Caché, for use in the collation argument of the CreateGlobal^%DM subroutine:

  • 5 — Cache standard

  • 10 — German1

  • 11 — Portuguese1

  • 12 — Polish1

  • 13 — German2

  • 14 — Spanish1

  • 15 — Danish1

  • 16 — Cyrillic1

  • 17 — Greek1

  • 18 — Czech1

  • 19 — Czech2

  • 20 — Portuguese2

  • 21 — Finnish1

  • 23 — Cyrillic2

  • 24 — Polish2

  • 27 — French1

  • 28 — Finnish2

  • 29 — Hungarian1

  • 30 — German3

  • 31 — Polish3

  • 32 — Spanish2

  • 33 — Danish2

  • 34 — Greek2

  • 35 — Finnish3

  • 36 — Lithuanian1

  • 41 — Danish3

  • 44 — Czech3

  • 45 — Hungarian2

  • 47 — Spanish3

  • 49 — Spanish4

  • 51 — Spanish5

  • 52 — Finnish4

Note:

To see a similar list, including which collations have been loaded into the instance, open a Terminal window, change to the %SYS% namespace, and enter the command DO ^COLLATE.

Default Collation for the Installed Locale

The default collation for the locale of a new installation of Caché is always the most recent version of the collation, that is, the one with with the highest numeric suffix (as shown in the list in the previous section). For example, whn installing with a Spanish locale, the default collation is Spanish5. Older versions of the collation are supported for compatibility with existing databases.

When a Caché instance is upgraded, the default collation is preserved wven when the updated locale uses a new default. For example, if existing instance’s locale uses Finnish3 as the default collation and the updated instance would use Finnish4, the upgrade preserves Finnish3 as the default, but makes Finnish4 available for new globals and databases.

Customizing Start and Stop Behavior with ^%ZSTART and ^%ZSTOP Routines

Caché can execute your custom code when certain events occur. Two steps are required:

  1. Define the ^%ZSTART routine, the ^%ZSTOP routine, or both.

    In these routines, you can define subroutines to execute when the certain activities start or stop.

    ^%ZSTART and ^%ZSTOP must be defined in the %SYS namespace, although they can be mapped to a non-default database.

  2. Use the Management Portal to configure Caché to invoke the desired subroutines.

Specifically, if you define the routine ^%ZSTART and ^%ZSTOP and you include subroutines with specific names, the system automatically calls these subroutines when the activity is beginning or ending. The subroutine names are as follows:

  • SYSTEM — Executed when Caché as a system starts or stops

  • LOGIN — Executed when a user performs a login or logout using the %Service_Console or Service_Telnet services.

  • JOB — Executed when a JOB begins or ends

  • CALLIN: — Executed when an external program begins or completes a CALLIN

For example, when a user logs in, the system automatically invokes LOGIN^%ZSTART, if that is defined and if you have used the Management Portal to enable this subroutine.

These subroutines are not intended to do complex calculations or run for long periods of time. Long calculations or potentially long operations like network accesses will delay the completion of the activity until the called routine returns. In this case, users may take a long (elapsed) time to login, or JOB throughput may be curtailed because they take too long to start.

Note:

These subroutines are called as part of normal Caché operation. This means that an external event which terminates Caché abnormally, such as a power failure, will not generate a call to ^%ZSTOP.

Note:

If a system implements ^%ZSTOP, and an application implements one or more $HALT routines, the ^%ZSTOP code is not executed until the last $HALT terminates with a HALT command. The failure of a $HALT routine to issue its own HALT command can prevent the ^%ZSTOP code from running.

Design Considerations

Because ^%ZSTART and ^%ZSTOP run in a somewhat restricted environment, the designer must keep several things in mind, namely:

  • The routines must be written in ObjectScript.

  • Since ^%ZSTART is essentially run as though it is started with an argumentless new command, it can not be used to perform tasks such as initializing local variables for users.

  • There are no values passed as arguments when any of the routine entry points are called. If different algorithms are applicable in various circumstances, the called entry point must determine what to do by examining data external to the routine: global, system variables, and so on.

  • Make sure that the routines are well-behaved under all possible conditions. They should be written defensively. That is, they should check to make sure that all the resources needed to complete their task are at hand and, if possible, reserved to them before computation starts. Errors which occur are reported as failures of that system function so it is important to think about the design from the viewpoint of error containment and handling. Failure to properly account for recovery in the face of missing resources or the presence of errors has varied consequences: Caché may fail to start; major functions such as Studio may act strangely; or more subtle and insidious consequences may occur which are not immediately detected. It is strongly recommended that these routines be carefully written and debugged under simulated conditions, and then tested under simulated environment conditions before being put into production systems.

  • No assumption should be made that conditions found during a previous invocation or a different entry point are still valid. Between successive calls to JOB^%ZSTART, for example, a file used by the prior call could have been deleted before this call occurred.

  • Each entry point should perform its task efficiently. If part of the task is potentially long-running, it is recommended that you queue enough information to complete the task for later completion by another part of your application.

  • If an entry point wishes to have data around persistently for, say, statistical purposes, it must use something like a global or an external file to hold the data.

  • The routines should make minimal assumptions about the environment they are running in. A developer of one of these routines cannot, for example, assume that the program will always be executed under a specific job number. The designer cannot assume that the various entry point will be called in a specific order. The sequence of bringing up the multiple processes that implement Caché is rarely deterministic.

  • The routine cannot assume that it is being called at a specific point during the system startup. The sequence of events during startup may change from release to release, or even from restart to restart.

  • With a few exceptions, the routine must leave things as it found them. As an illustration of this principle, reassigning the value of $IO in the subroutine without saving and restoring it upon entry and exit is an almost certain source of error. The calling routine has no way of knowing that such things are changed, and it is very difficult for the caller to defend against any possible change to the execution environment. Therefore, the burden of not disturbing the system processing context lies on the subroutine being called.

    The general exceptions to the no-changes rule are that changing process-local values specific to an application or installation are allowed. For example, the SYSTEM^%ZSTART entry point may set system-wide defaults. Similarly, for application testing, it could set the date to a specific value to validate end-of-month processing.

  • ^%ZSTOP cannot contain references to globals in remote databases. At the time it is called, some of these may no longer be accessible.

  • Once SYSTEM^%ZSTOP is invoked during the shutdown process, a user is no longer able to use the JOB command to start new processes. Currently existing processes are allowed to finish.

  • If these routines are mapped to a database different from CACHESYS, then Caché will attempt to execute them from that database, not from CACHESYS. Caché will, of course, make certain that the calling routine has the appropriate access to that database beforehand. It is the responsibility of the administrator to ensure that the routine has access to any application globals and mappings that it requires from that namespace.

  • SYSTEM^%ZSTART and SYSTEM^%ZSTOP are run with $USERNAME set to %SYSTEM and $ROLES set to %All. To run your code with a different username, use $SYSTEM.Security.Login() to set the desired name and then continue with your custom code. If you use JOB in your SYSTEM^%ZSTART code to launch any additional processes, those processes will inherit the same username (and roles) as the initiating process.

    Caution:

    All the entry points in ^%ZSTART and ^%ZSTOP are invoked at critical points in system operation and can have widespread effects on the operation of a system or even on its data. The specified purpose of these routines makes this high level of privilege necessary. Because of this, you must make sure that all code that can be invoked by these entry points has been thoroughly tested. Further, do not allow any user-specified code to be run via XECUTE or indirection.

  • The exiting (that is, halting) process may get a <FUNCTION> error on any reference that requires an answer from an ECP data server.

Note:

On upgrades, Caché preserves only the %Z* routines that are mapped to the CACHESYS database, and if the .INT or .MAC code is available, recompiles them. Preservation of routines in other databases are the responsibility of the site administrator.

Enabling %ZSTART and %ZSTOP

Once the routines have been designed, developed, compiled, and are ready to be tested, individual entry points may be enabled through the Management Portal. Navigate to the Startup Settings page by selecting System Administration, then Configuration, then Additional Settings, then Startup Settings, and edit the appropriate individual settings:

  • SystemStart, SystemHalt

  • ProcessStart, ProcessHalt

  • JobStart, JobHalt

  • CallinStart, CallinHalt

To deactivate one or more of the entry points, use the same procedure but change the value to false.

Debugging ^%ZSTART and ^%ZSTOP

The opportunities for debugging ^%ZSTART and ^%ZSTOP in their final environment are very limited. If an error occurs, errors are written to the operator console log, which is the current device while these routines are running. This file is cconsole.log and is found in the Manager’s directory.

The message indicates the reason for failure and location where the error was detected. This may be different from the place where the error in the program logic or flow actually occurred. The developer is expected to deduce the nature and location of the error from the information provided, or modify the routine so that future tests provide more evidence as to the nature of the error.

Removing %ZSTART and ^%ZSTOP

It is strongly recommended that you disable the entry point options via the Management Portal before deleting the routines. If the portal warns that a restart of Caché is needed for them to take effect, do this as well before proceeding. This guarantees that none of the entry points are being executed while they are being deleted.

Remember that ^%ZSTART and ^%ZSTOP (as well as any supporting routines) are stored persistently. To remove all traces of them, delete them through the Management Portal.

Example

The following example demonstrates a simple log for tracking system activity. It shows examples for ^%ZSTART and ^%ZSTOP, both of which use subroutines of a third example routine, ^%ZSSUtil, for convenience.

^%ZSSUtil Example

This routine has two public entry points. One writes a single line to the operator console log file. The other writes a list of name-value pairs to a local log file. Both files reside in the Manager’s directory, which is returned by the ManagerDirectory() method of the %Library.FileOpens in a new tab class.

%ZSSUtil ;
    ; this routine packages a set of subroutines 
    ; used by the %ZSTART and %ZSTOP entry points
    ; 
    ; does not do anything if invoked directly
    quit
    
#define Empty ""
#define OprLog 1
    
WriteConsole(LineText) PUBLIC ;
    ; write the line to the console log
    ; by default the file cconsole.log in the MGR directory
    new SaveIO
    
    ; save the current device and open the operator console
    ; set up error handling to cope with errors
    ; there is little to do if an error happens
    set SaveIO = $IO
    set $ZTRAP = "WriteConsoleExit"
    open $$$OprLog
    use $$$OprLog
    ; we do not need an "!" for line termination
    ; because each WRITE statement becomes its 
    ; own console record (implicit end of line)
    write LineText
    ; restore the previous io device
    close $$$OprLog
    ; pick up here in case of an error
WriteConsoleExit ;
    set $ZTRAP = ""
    use SaveIO
    quit
    
WriteLog(rtnname, entryname, items) PUBLIC ;
    ; write entries into the log file
    ; the log is presumed to be open as 
    ; the default output device
    ; 
    ; rtnname: distinguishes between ZSTART & ZSTOP
    ; entryname: the name of the entry point we came from
    ; items: a $LIST of name-value pairs
    new ThisIO, ThisLog
    new i, DataString
    
    ; preserve the existing $IO device reference
    ; set up error handling to cope with errors
    ; there is little to do if an error happens
    set ThisIO = $IO
    set $ZTRAP = "WriteLogExit"

    ; construct the name of the file
    ; use the month and day as part of the name so that 
    ; it will create a separate log file each day
    set ThisLog = "ZSS"
                _ "-"
                _ $EXTRACT($ZDATE($HOROLOG, 3), 6, 10)
                _".log"
    
    ; and change $IO to point to our file
    open ThisLog:"AWS":0
    use ThisLog
    
    ; now loop over the items writing one line per item pair
    for i = 1 : 2 : $LISTLENGTH(items)
    {
        set DataString = $LISTGET(items, i, "*MISSING*")
        if ($LISTGET(items, (i + 1), $$$Empty) '= $$$Empty)
        {
            set DataString = DataString
                           _ ": "
                           _ $LISTGET(items, (i + 1))
        }
        write $ZDATETIME($HOROLOG, 3, 1),
              ?21, rtnname,
              ?28, entryname,
              ?35, DataString, !
    }
    
    ; stop using the log file and switch $IO back
    ; to the value saved on entry
    close $IO
    ; pick up here in case of an error
WriteLogExit ;
    set $ZTRAP = ""
    use ThisIO
    quit

Here is an description for each label:

^%ZSSUtil

This routine (as well as the others) begins with a QUIT command so that it is benign if invoked via

    do ^%ZSSUtil

The #DEFINE sequence cosmetically provides named constants in the body of the program. In this instance, it names the empty string and the device number of the operator console log.

WriteConsole^%ZSSUtil

The entry point is very simple. It is designed for low volume output, and as a minimally intrusive routine to use for debugging output.

It takes a single string as its argument and writes it to the operator console log. However, it must take care to preserve and restore the current $IO attachment across its call.

Each item sent to the device results in a separate record being written to the console log. Thus, the following results in four records being written.

    WRITE 1, 2, 3, !

The first three consist of a single digit and the fourth is a blank line. If multiple items are desired on a line, it is the responsibility of the caller to concatenate them into a string.

WriteLog^%ZSSUtil

This subroutine can be called by any entry point within ^%ZSTART or ^%ZSTOP. The first two arguments supply the information needed to report how the subroutine was launched. The third argument is a $LIST of name-value pairs to be written to the log.

This entry point first builds the name of the file it will use. To make log management easier, the name contains the month and day when the routine is entered. Therefore, calls to this subroutine create a new file whenever the local time crosses midnight. because the name is determined only at the time of the call. All the name-value pairs passed as the argument will be displayed in the same file.

Once the name has been constructed, the current value of $IO is saved for later use and the output device is switched to the named log file. The parameters used for the OPEN command ensure that the file will be created if it is not there. The timeout of zero indicates that Caché will try a single time to open the file and fail if it cannot.

Once the file has been opened, the code loops over the name value pairs. For each pair, the caller routine name and the entry point name are written followed in the line by the name-value pair. (If the value part is the empty string, only the name is written.) Each pair occupies one line in the log file. The first three values on each line are aligned so they appear in columns for easier scanning.

When all the pairs have been written, the log file is closed, the previous value $IO is restored and control returns to the caller.

^%ZSTART

This routine contains the entry point actually invoked by Caché. It uses the services of ^%ZSSUtil just described. All the entry points act more or less the same, they place some information in the log. The SYSTEM entry point has been made slightly more elaborate than the others. It places information in the Operator console log as well.

%ZSTART ; User startup routine. 
 
#define ME "ZSTART"
#define BgnSet "Start"
#define Empty ""

    ; cannot be invoked directly
    quit
 
SYSTEM ;
    ; Cache starting
    new EntryPoint, Items
     
     set EntryPoint = "SYSTEM"
    
     ; record the fact we got started in the console log
     do WriteConsole^%ZSSUtil((EntryPoint
                               _ "^%"
                               _ $$$ME
                               _ " called @ "
                               _ $ZDATETIME($HOROLOG, 3)))
    
    ; log the data accumulate results
     set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3),
                           "Job", $JOB,
                           "Computer", $ZUTIL(110),
                           "Version", $ZVERSION,
                           "StdIO", $PRINCIPAL,
                           "Namespace", $ZUTIL(5),
                           "CurDirPath", $ZUTIL(12),
                           "CurNSPath", $ZUTIL(12, ""),
                           "CurDevName", $ZUTIL(67, 7, $JOB),
                           "JobType", $ZUTIL(67, 10, $JOB),
                           "JobStatus", $ZHEX($ZJOB),
                           "StackFrames", $STACK,
                           "AvailStorage", $STORAGE,
                           "UserName", $ZUTIL(67, 11, $JOB))
    do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
    
    quit
LOGIN ;
    ; a user logs into Cache
    new EntryPoint, Items
    
    set EntryPoint = "LOGIN"
     set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3))
    do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
    quit
    
JOB ;
    ; JOB'd process begins
    new EntryPoint, Items
    
     set EntryPoint = "JOB"
     set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3))
    do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
    quit
     
CALLIN ;
    ; a process enters via CALLIN interface
    new EntryPoint, Items
    
     set EntryPoint = "CALLIN"
     set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3))
    do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
    quit

Here is an description for each label:

^%ZSTART

This routine begins with a QUIT command so that it is benign if invoked as a routine rather than beginning its execution properly at one of its entry points.

This routine also defines named constants (as macros) for its own name, a starting string and the empty string.

SYSTEM^%ZSTART

This subroutine constructs a string consisting of the calling routine name, entry point, and the date and time it was invoked. Then it calls WriteConsole^%ZSSUtil to place it in the operator console log.

Afterward, it constructs a list of name-value pairs that it wishes to be displayed. It passes this to WriteLog^%ZSSUtil to place into the local log file. Then it returns to its caller.

LOGIN^%ZSTART, JOB^%ZSTART, and CALLIN^%ZSTART

These subroutines do not place any information in the operator console log. Instead, they construct a short list of items, enough to identify that they were invoked, and then use WriteLog^%ZSSUtil to record it.

^%ZSTOP

This routine contains the entry points actually invoked by Caché and it uses subroutines in ^%ZSSUtil. This example is similar to the example for ^%ZSTART. See the previous section for details.

%ZSTOP ; User shutdown routine. 
 
#define ME "ZSTOP"
#define EndSet "End"
#define Empty ""

    ; cannot be invoked directly
    quit
 
SYSTEM ; Cache stopping
    new EntryPoint
    
    set EntryPoint = "SYSTEM"
    ; record termination in the console log
    do WriteConsole^%ZSSUtil((EntryPoint
        _ "^%"
        _ $$$ME
        _ " called @ "
        _ $ZDATETIME($HOROLOG, 3)))
    ; write the standard log information
    do Logit(EntryPoint, $$$ME)
    quit
    
LOGIN ; a user logs out of Cache
    new EntryPoint
    
    set EntryPoint = "LOGIN"
    do Logit(EntryPoint, $$$ME)
    quit
    
JOB ; JOB'd process exits. 
    new EntryPoint
    
    set EntryPoint = "JOB"
    do Logit(EntryPoint, $$$ME)
    quit
     
CALLIN ; process exits via CALLIN interface. 
    new EntryPoint
    
    set EntryPoint = "CALLIN"
    do Logit(EntryPoint, $$$ME)
    quit
    
Logit(entrypoint, caller) PRIVATE ;
    ; common logging for exits
    
    new items
    
    set items = $LISTBUILD($$$EndSet, $ZDATETIME($HOROLOG, 3))
    do WriteLog^%ZSSUtil(caller, entrypoint, items)
    quit

Extending Languages with ^%ZLANG Routines

Note:

It is customary (but inaccurate) to refer to a routine as if the caret is part of its name. This documentation follows this custom except when referring directly to the actual names of the routines.

You can use the ^%ZLANG feature to add custom commands, functions, and special variables to the ObjectScript and other languages. Your extensions are invoked in the same way as standard features, and follow the same rules for syntax, operator precedence, and so on. ^%ZLANG features generally do not execute as rapidly as standard Caché features. Consider this point when coding performance-critical routines.

To add such extensions:

  1. Define routines with the following names, as needed:

    %ZLANGCnn
    %ZLANGFnn
    %ZLANGVnn
    

    Routines with the name %ZLANGCnn define commands, routines with the name %ZLANGFnn define functions, and routines with the name %ZLANGVnn define special variables. The nn part of the routine name indicates the language in which these items are available. nn is one of the following:

    • 00 — ObjectScript

    • 01 — DSM-11

    • 02 — DTM

    • 06 — DSM-J

    • 07 — DTM-J

    • 08 — MSM

    • 09 — Caché Basic

    • 11 — MV Basic

  2. In these routines, define public subroutines as follows:

    • For the subroutine label, use the name of the command, function, or special variable that you are defining.

      The name must start with the letter Z and can include only letters. Unicode characters are permitted, if the locale defines them as alphabetic. All letters must be in uppercase, though execution is not case-sensitive. The maximum length of a name is 31 letters.

      The name cannot be the same as an existing command, function, or special variable (if it is, Caché ignores it). InterSystems also strongly recommends that you do not use an SQL reserved word.

      If you are defining a function, even a function with no arguments, the label must include parentheses.

      A label for a special variable can include parentheses.

    • Optionally include an additional label to define an abbreviation. Be careful that it is not already used by Caché.

    • Define the subroutine appropriately. See the “Notes” section for details.

  3. As good programming practice, the first command in the parent routine should use QUIT so that nothing occurs if a user invokes the routine directly.

    It is also helpful for the routine to include a comment at the top that indicates the name of the routine itself.

Caution:

For other subroutines, including ones invoked by the public subroutines, make sure that the labels for those are in lowercase or mixed case (or do not start with Z). Or implement them as private subroutines.

That is, because a ^%ZLANG routine extends the language, it is important to make sure that only the desired subroutines are available outside of it.

Notes

Commands are handled like a DO of a routine or procedure. Arguments are passed as call parameters.

Your code should preserve the values of system state such as $TEST and $ZREFERENCE unless you intend for them to be a result of your code. (But note that for functions and special variables, the system automatically preserves $TEST.)

You can SET the value of a special variable. There is only one entry point for the variable. To determine whether to set the value or retrieve the value, your code should check whether an argument is given. For example:

ZVAR(NewValue) public {
         if $DATA(NewValue) Set ^||MyVar=NewValue Quit
         Quit $GET(^||MyVar)
}

Then, a user can either set this variable or retrieve it, as demonstrated here:

SAMPLES>w $ZVAR
 
SAMPLES>s $ZVAR="xyz"
 
SAMPLES>w $ZVAR
xyz

To return an error code from a command or function, use $SYSTEM.Process.ThrowError().

Examples

For example, to define a custom special variable for use in ObjectScript, you define the routine ^%ZLANGV00, which could look like the following:

 ; implementation of ^%ZLANGV00
 ; custom special variables for ObjectScript
  QUIT

ZVERNUM        ; tag becomes name of a special variable
ZVE
  QUIT $PIECE($ZVERSION,"(Build")


Then, for demonstration, you can use the new variable in the Terminal as follows:

SAMPLES>w $zvernum
Cache for Windows (x86-32) 2011.1
SAMPLES>w $zve
Cache for Windows (x86-32) 2011.1

For another example, suppose that you define the ^%ZLANGF00 routine as follows:

 ; implementation of ^%ZLANGF00
 ; custom functions for ObjectScript
 QUIT
 
ZCUBE(input) public {
 Quit input*input*input
}

Then, for demonstration, you can use the new function in the Terminal as follows:

SAMPLES>w $zcube(2)
8

The following example shows ^%ZLANGC00, which creates a command that executes the system status utility ^%SS:

 ; %ZLANGC00
 ; custom commands for ObjectScript
  QUIT    

ZSS       ; tag name of a command to check system status
  DO ^%SS
  QUIT
FeedbackOpens in a new tab