Unterabschnitte

Hilfsbibliothek libCUI-util

Die libCUI-util ist eine Sammlung von Hilfsroutinen, die direkt oder indirekt für die Programmierung von CUI-Programmen nützlich sein können. Zudem finden sich hier eisfair-spezifische Funktionen zum Umgang mit Konfigurationsdateien und anderen administrativen Aufgaben. Keine der beiden Bibliotheken ist von der anderen abhängig, was bedeutet, dass Programme die Hilfsbibliothek verwenden können, ohne die libCUI zu verwenden zu müssen. Dies ist umgekehrt ebenso.

Verwenden der libCUI-util

Wie auch die libCUI, so kann auch die libCUI-util dynamisch und statisch gelinkt werden.

Dynamisch:

gcc -Wall -Wstrict-prototypes -o test test.c -lcui-util

Statisch:

gcc -o test test.c /usr/lib/libcui-util.a

XML-Parser

Der hier enthaltene Implementierung ist ein einfacher Parser, der XML-formatierte Dateien in eine Baumstruktur einliest. Der eingelesene Baum, der aus Knoten, Objekten und Attributen besteht, kann anschließend im Speicher durchsucht und ausgewertet werden. Zudem besteht die Möglichkeit, den eingelesenen Baum zu modifizieren und in modifizierter Form wieder in eine Datei zu schreiben.

In den Datei ''cui-util.h'' sind die folgenden Funktionsprototypen für den XML-Parser definiert:

XMLFILE* XmlCreate  (const char* filename);

void XmlDelete      (XMLFILE* xml);

void XmlSetErrorHook(XMLFILE* xml, 
                     ErrorCallback errout,
                     void* instance);
                     
void XmlAddSingleTag(XMLFILE* xml, 
                     const char* name);

int XmlReadFile     (XMLFILE* xml);

int XmlWriteFile    (XMLFILE* xml);

void XmlPreserveNewline(XMLFILE* xml, int state);

XMLOBJECT* XmlGetObjectTree(XMLFILE* xml);

Mit der Funktion ''XmlCreate'' wird eine Instanz der Stuktur ''XMLFILE'' angelegt und dieser der Dateiname ''filename'' zugewiesen. Nun kann mit ''XmlReadFile'' die Datei eingelesen und mit ''XmlWriteFile'' wieder zurückgeschrieben werden. Die Rückgabewerte sind endweder ''TRUE'' oder ''FALSE'' je nachdem, ob die Funktion erfolgreich abgeschlossen werden konnte oder nicht. Freigegeben wird die Datenstruktur ''XMLFILE'' sowie der gesamte Objektbaum mit Hilfe der Funktion ''XmlDelete''.

Die Funktion ''XmlPreserveNewline'' sorgt beim Einlesen der Datei dafür, dass Zeilenumbrüche erhalten bleiben, sich die Formatierung des Textes in der ASCII-Datei auch in den Daten des Objektbaums wiederfindet. Standardmäßig werden Zeilenumbrüche ignoriert.

Läuft der Parser beim Lesen der Datei auf einen Fehler, dann wird dieser über eine Callback-Funktion ausgegeben, die dem Parser zuvor über die Funktion ''XmlSetErrorHook'' mitgeteilt werden muss. Der Paramter ''instance'' kann dabei optional eine Fensterinstanz erhalten und ist speziell für das Zusammenspiel mit der libCUI gedacht. Die Callback-Funktion muss von Typ ''ErrorCallback'' sein, die den folgenden Prototypen vorgibt:

typedef void (*ErrorCallback)(void* instance,
                              const char* errmsg,
                              const char* filename,
                              int line,
                              int is_warning);

Ein Code-Schnipsel, das eine XML-Datei einliest, kann nun wie folgt aussehen:

   XMLFILE* xmldata = XmlCreate("myfile.xml");
   XMLOBJECT* rootobj;
   if (xmldata)
   {
      XmlSetErrorHook(xmldata, MyErrorHook, win);
      if (XmlReadFile(xmldata))
      {
         rootobj = XmlGetObjectTree(xmldata);
         
         /* do something with the data */
         
      }
      XmlDelete(xmldata);
   }

Die Implementierung der Callback-Funktion ist je nach Programm und Art der Fehlerbehandlung verschieden. Hier ein mögliches Beispiel:

void
MyErrorHook(void* w, const char* errmsg, const char* filename,
             int linenr, int is_warning)
{
   CUIWINDOW* win = (CUIWINDOW*) w;
   if (win)
   {
      MYWINDATA* data = (MYWINDATA*) win->InstData;

      if ((data->NumErrors + data->NumWarnings) < 8)
      {
         char err[512];
         if (is_warning)
         {
            sprintf(err,"WARNING: (%i): %s",linenr,errmsg);
                        MywinAddMessage(win, err);
         }
         else
         {
            sprintf(err,"ERROR: (%i): %s",linenr,errmsg);
                        MywinAddMessage(win, err);
         }
      }
      else if ((data->NumErrors + data->NumWarnings) == 8)
      {
         MywinAddMessage(win, "... more errors");
      }

      if (is_warning)
      {
         data->NumWarnings++;
      }
      else
      {
         data->NumErrors++;
      }
   }
}

über ''MywinAddMessage'' wird die aktuelle Fehlermeldung zu einer Liste hinzugefügt (begrenzt auf max. acht Fehler). Nach Abschluss der Leseoperation kann später die Liste der Fehlermeldungen in einem Mitteilungsfenster (z.B. MessageBox) ausgegeben werden.

Die Abbildung zeigt, wie sich eine einfache XML-Datei im Objektbaum darstellt. Dieser besteht aus Objekten, Knoten, Attributen und Daten. Ausgehend vom stets vorhandenen Wurzelobjekt verläuft eine Liste von Knoten. Knoten können drei Typen besitzen:

XML_COMMENTNODE
Der Knoten steht für einen Kommentar. Das Datenfeld "Dataßeigt auf auf einen Kommentartext.

XML_OBJNODE
Der Knoten zeigt auf ein Objekt. Objekte besitzen selbst keine Daten, verfügen aber ihrerseits wieder über eine Liste von Knoten.

XML_DATANODE
Der Knoten steht für einen Block von Daten. Das Datenfeld "Dataßeigt auf auf eine Zeichenkette mit den enthaltenen Daten.

Die Elemente des Objektbaums bestehen aus den folgenden Datentypen:

typedef struct
{
   char* Name;       /* name of XML attribute */
   char* Value;      /* name of attribute value */
   void* Next;       /* next XML attribute */
} XMLATTRIBUTE;

typedef struct
{
   int   Type;       /* Type of node: data or child object */
   char* Data;       /* Data (if node is a data node) */
   int   DataLen;    /* Size of current data buffer */
   void* Object;     /* Pointer to child object (if object node) */

   void* Next;       /* Next node or NULL */
} XMLNODE;

typedef struct
{
   char* Name;              /* Name of XML object */

   XMLNODE*      FirstNode; /* The first node containing 
                               data or a child object */
   XMLNODE*      LastNode;  /* The last node containing 
                               data or a child object */
   XMLATTRIBUTE* FirstAttr; /* Pointer to first object attribute */
   XMLATTRIBUTE* LastAttr;  /* Pointer to last object attribute */
} XMLOBJECT;

Der Objektbaum kann nach dem Einlesen beliebig modifiziert werden. Die API Funktionen (deren Parameter weitestgehend selbsterklärend sein sollten) sind hier aufgelistet:

XMLOBJECT* XmlCreateObject    (XMLFILE* xml, XMLOBJECT* parent);
void       XmlDeleteObject    (XMLFILE* xml, XMLOBJECT* ob);
void       XmlSetObjectName   (XMLOBJECT* ob, const char* name);
void       XmlSetObjectData   (XMLOBJECT* ob, const char* data);
void       XmlAddObjectData   (XMLOBJECT* ob, const char* data);
void       XmlSetObjectComment(XMLOBJECT* ob, const char* data);
void       XmlAddObjectComment(XMLOBJECT* ob, const char* data);

XMLATTRIBUTE* XmlCreateAttribute  (XMLOBJECT* ob,
                                   const char* name);
void          XmlSetAttributeValue(XMLATTRIBUTE* attr, 
                                   const char* name);
XMLATTRIBUTE* XmlGetAttribute     (XMLOBJECT* ob, 
                                   const char* name);

Konfigurations-Parser

Der Konfigurationsparser ist ein Parser, der einfach aufgebaute Konfigurationsdateien der folgenden Form einliest:

FOO_NAME='Name'
FOO_PATH='/var/lib/foo'
FOO_ENUM_N='2'
FOO_ENUM_1_VALUE='1234'
FOO_ENUM_2_VALUE='0'

Die unter eifair gebräuchliche Form der Konfigurationsdateien sieht Wertepaare vor, bei denen der Wert in einfachen oder doppelten Anführungsstrichen steht. Es können auch ein- oder mehrdimensionale Arrays aufgebaut werden, indem Optionen einem Aufzählungsknoten (im Beispiel FOO_ENUM_N) zugeordnet werden.

Hinweis
Dieser Parser ist dazu gedacht, Konfigurationsdateien zu lesen, wie sie z.B. von CUI-Programmen unter eisfair verwendet werden (z.B. /etc/cui.conf). Der Nutzen des Parsers zum Einlesen von Paketkonfigurationen (z.B. /etc/config.d/foo) ist jedoch recht eingeschränkt, da Infrastrukturdateien (z.B. /etc/check.d/foo) nicht berücksichtigt werden. Dem Programm muss die Struktur der Konfigurationsdatei bekannt sein.


Der Parser bringt die folgende API zum Lesen der Dateien mit:

CONFIG*  ConfigOpen     (ErrorCallback errout, 
                         void* instance);
void     ConfigAddNode  (CONFIG* cfg, 
                         const char* n_node, 
                         const char* mask);
void     ConfigReadFile (CONFIG* cfg, 
                         const char* filename);
void     ConfigClose    (CONFIG* clg);

Der Aufruf von ConfigOpen bereitet das Lesen der Datei vor, indem eine ''CONFIG'' Struktur allokiert wird und dieser eine Callback- Funktion für Fehlerausgaben zugewiesen wird. Das Prinzip und der Funktionsprototyp der Callback- Funktion sind mit denen des XML-Parsers (siehe Abschnitt ''XML-Parser'') identisch.

Mit der Funktion ''ConfigAddNode'' wird dem Parser mitgeteilt, welche Optionen Array-Elemente sind und welchem Aufzählungsknoten sie zugeordnet werden. Der Parser lernt auf diese Weise die Struktur der Datei. Der Parameter ''n_node'' gibt dabei den Namen des Aufzählungsknotens an, der Parameter ''mask'' enthält eine Maske zur Erkennung des Optionsnamens.

Mit ''ConfigReadFile'' wird die Datei eingelesen. Es ist dabei zu beachten, dass die Funktion selbst keine Auskunft über Erfolg oder Misserfolg dibt. Fehler werden statt dessen über die Callback- Funktion ausgegeben und können hier ggf. gezählt werden.

Die Funktion ''ConfigClose'' gibt die ''CONFIG'' Struktur und alle damit verbundenen Daten wieder frei.

Nach dem Einlesen steht die Konfigurationsdatei als Wertebaum im Speicher zur Verfügung. Mit den folgenden API-Funktionen kann sie nun ausgewertet werden:

CONFENTRY*  ConfigGetEntry (CONFIG* cfg, 
                            CONFENTRY* parent, 
                            const char* name, 
                            int * index);
const char* ConfigGetString(CONFIG* cfg, 
                            CONFENTRY* parent, 
                            const char* name,
                            OPT_TYPE type, 
                            const char* defval, 
                            int * index);
int         ConfigGetBool  (CONFIG* cfg, 
                            CONFENTRY* parent, 
                            const char* name,
                            OPT_TYPE type, 
                            const char* defval, 
                            int * index);
int         ConfigGetNum   (CONFIG* cfg, 
                            CONFENTRY* parent, 
                            const char* name,
                            OPT_TYPE type, 
                            const char* defval, 
                            int * index);

Die Funktionen dienen dem Auslesen unterschiedlicher Typen aus der Datei. Die Parameter haben dabei immmer die gleiche Bedeutung:

''cfg'': CONFIG-Struktur der zuvor eingelesenen Datei.

''parent'': Aufzählungsknoten oder NULL.

''name'': Name der Option.

''type'': Handelt es sich um einen optionalen Wert?

''defval'': Standardwert, falls die Option nicht gefunden wird.

''index'': Array mit Indexwerten im Falle von Arrays. Die Anzahl der Indexwerte im Array muss dabei der Dimension des Arrayelements ''Option'' entsprechen.

Bis auf ''ConfigGetEntry'' geben die Funktionen immer einen Wert zurück. Wird die Option nicht gefunden, dann wird der Standardwert zurückgegeben. Allerdings kann mit dem Parameter ''type'' (type = REQUIRED) vorgegeben werden, dass die Option existieren muss. In diesem Fall wird eine Meldung über die Callback- Funktion ausgegeben, falls der Wert nicht gefunden wurde.

Zum Lesen der oben aufgeführten Datei kann nun der folgende Code verwendet werden:

MYDATA* data = (MYDATA*) win->InstData;
CONFIG* cfg = ConfigOpen(ErrOut, win); 
if (cfg)
{
   CONFENTRY* p;
   int i, index;
   
   /* reset error counter */
   data->NumErrors = 0;
   
   /* define file structure and read file */
   ConfigAddNode(cfg, "FOO_ENUM_N", "FOO_ENUM_%_VALUE");
   ConfigReadFile(cfg, "/etc/myconf.conf");
   
   /* get array dimensions */
   index = ConfigGetNum(cfg, NULL, "FOO_ENUM_N", 
                        REQUIRED, "0", NULL);
                        
   /* read array */
   p = ConfigGetEntry(cfg, NULL, "FOO_ENUM_N", NULL);
   if (p)
   {
      for (i = 0; i < index; i++)
      {
         val = ConfigGetNum(cfg, p, "FOO_ENUM_%_VALUE", &i);
		  
         /* do something with "val" */
      }
   }
   ConfigClose(cfg);
  
   if (data->NumErrors > 0)
   {
      /* display errors */
   }
}

Co-Prozesse ausführen

Es ist oftmals erforderlich, andere Programme im Hintergrund zu starten und deren Textausgaben oder Ergebniswerte anschließend weiterzuverarbeiten. Damit lassen sich Aufgaben an andere Programme delegieren, Schnittstellen zu Scripten etc. schaffen und vorhandene Funktionalität nutzen.

Die libCUI-util stellt eine API-Funktion zur Verfügung, mit der Co-Prozesse ausgeführt werden können:

int  RunCoProcess(const char* filename,
                  char* const parameters[],
                  TextCall callback,
                  int* exitcode);

Der Parameter ''filename'' gibt den Namen und den Pfad des externen Programms an, das Array ''parameters'' enthält die Programmargumente, ''callback'' ist eine Callback-Funktion für die Textausgaben des Programms und ''exitcode'' ist die Variable die den Rückgabewert des aufgerufenen Programms zurückgibt. Der Rückgabewert der Funktion ''RunCoProcess'' ist ''TRUE'' wenn der Aufruf erfolgreich war und ''FALSE'' falls nicht.

Bei ''parameters'' ist zu berücksichtigen, dass das erste Element (parameters[0]) den Namen des Programms erhalten muss. Das letzte Element enthält einen Nullzeiger und signalisiert damit das Ende der Liste.

Die Funktion vom Typ ''TextCall'' muss dem folgenden Prototypen entsprechen:

typedef void (*TextCall) (const char* buffer, int source);

Sie erhält die Textausgaben des Programms zeilenweise (also immer dann, wenn ein Zeilenumbruch vom Programm zurückgegeben wird. Der Parameter ''source'' kann die Werte ''PIPE_STDOUT'' und ''PIPE_STDERR'' annehmen und gibt damit an, über welchen Augabekanal der Text ausgegeben wurde.

Die Funktion ''RunCoProcess'' hält das Programm im Vordergrund an, bis der CoProcess beendet wurde.

Ein Beispiel:

void RunCallback(const char* buffer, int source)
{
   if (source == PIPE_STDOUT)
   {
      printf("INFO:  %s\n", buffer);
   }
   else
   {
      printf("ERROR: %s\n", buffer);
   }
}

int RunScript(const char* scriptfile)
{
   int result, status;
   const char* p[3];
   
   /* initialize for interpreted scripts */
   p[0] = "/bin/bash";
   p[1] = scriptfile;
   p[2] = 0;

   /* execute */
   result = RunCoProcess("/bin/bash", (char**) p, 
                         RunCallback, &status);
   
   /* evaluate result */
   return (result == TRUE) && (status == 0);
}

ToDo: Hier sollte noch ein Instanzzeiger enthalten sein, damit dieser an die Callback-Routine übergeben werden kann.

Ein Hinweis sein hier auch nochmal auf das Terminal Kontrollelement der libCUI gegeben. Dieses erlaubt die Ausführung von Programmen in einem Terminal-Fenster, während der Rest der Anwendung normal weiterläuft.

Holger Bruenjes 2016-12-12