/*
 *  GP.service - General Purpose Service für Envoy
 */

#define VERSION     "1.0"
/* Achtung: die Versionsnummer muß auch im makefile geändert werden! */
#define DATE        "12.9.94"
#define PROGNAME    "GP.server"


/********************************************************************
 *                              Includes                            *
 *******************************************************************/

#define USE_BUILTIN_MATH
#include <string.h>

#include <dos/dostags.h>
#include <envoy/envoy.h>
#include <envoy/errors.h>
#include <envoy/services.h>
#include <exec/memory.h>

#include <clib/dos_protos.h>
#include <pragmas/dos_pragmas.h>
#include <clib/exec_protos.h>
#include <pragmas/exec_sysbase_pragmas.h>
#include <clib/intuition_protos.h>
#include <pragmas/intuition_pragmas.h>
#include <clib/nipc_protos.h>
#include <pragmas/nipc_pragmas.h>
#include <clib/timer_protos.h>
#include <pragmas/timer_pragmas.h>
#include <clib/utility_protos.h>
#include <pragmas/utility_pragmas.h>

#include "GPService.h"
#include "GPServer.h"

/* Version-String */
static const char version[] = "\0$VER: " PROGNAME " " VERSION " (" DATE ")";

/********************************************************************
 *                      Prototypen für Funktionen                   *
 *******************************************************************/

static void server(void);
static BOOL getsystemtime(struct timeval *tv, struct svcbase *sb);
static BOOL openresources(struct svcbase *sb);
static void closeresources(struct svcbase *sb);
static void handle_transaction(struct Transaction *ta, struct svcbase *sb);
static void processmessages(struct svcbase *sb);
static void transaction_cmdline(struct Transaction *ta, struct svcbase *sb);
static void transaction_gettime(struct Transaction *ta, struct svcbase *sb);
static void transaction_request(struct Transaction *ta, struct svcbase *sb);

int __asm __UserLibInit(register __a6 struct svcbase *sb)
{
    /*
     *  Diese Funktion wird vom SAS/C-Library-Code aufgerufen,
     *  wenn die Library zum ersten mal geöffnet wird
     *
     *  liefert 0 wenn alles ok, sonst 1
     */
    struct Library *SysBase = *(struct Library **)4;
    if (SysBase->lib_Version < 37) return(1); /* unter Kick 2.04 geht nichts! */

    sb->ml_ExecBase = SysBase;
    InitSemaphore(&sb->startsema);

    return(0);
}

/********************************************************************
 *              Standard-Funktionen eines Envoy-Service             *
 *                                                                  *
 *  Ein Envoy-Service ist eine Exec-Library mit den folgenden drei  *
 *  Standardfunktionen RexxReserved(), StartServiecA() und          *
 *  GetServiceAttrsA().                                             *
 *                                                                  *
 *  Da diese drei Funktionen von jedem beliebigen Prozess, der den  *
 *  Service mit OpenLibrary() öffnet, angesprungen werden können,   *
 *  müssen sie wie jede Library-Funktion reentrant sein!            *
 *                                                                  *
 *  Dies bedeutet insbesondere, daß diese Funktionen globale Daten  *
 *  nur mit besonderer Sorgfalt benutzen dürfen!                    *
 *******************************************************************/

ULONG __asm
StartServiceA(register __a0 struct TagItem *taglist,
              register __a6 struct svcbase *sb)
{
    /*  Diese Funktion ist die wichtigste eines Envoy-Services.
     *  Sie wird bei jedem FindService() aufgerufen.
     *
     *  Die Aufgabe dieser Funktion ist es, den Server-Prozess zu
     *  starten, falls er noch nicht läuft, und als Ergebnis den
     *  Namen der Entity des Server-Prozesses zurückzuliefern.
     */
    struct TagItem *ti;
    struct Library *SysBase = *(struct Library **)4;
    struct Library *DOSBase;
    struct Library *UtilityBase;
    ULONG error = ENVOYERR_BADSTARTSERVICE;
    ULONG signal;

    /*  Der folgende Code darf nicht mehrmals gleichzeitig laufen,
     *  da sonst der Prozess theoretisch mehrmals gestartet werden könnte.
     *  Dies verhindern wir, in dem wir unsere Semaphore "startsema"
     *  benutzen. Dadurch können wir auch ohne weiteres die globalen
     *  Variablen serverprocess, startprocess, startsignalmask und
     *  startservererror benutzen.
     */
    ObtainSemaphore(&sb->startsema);

    if (!sb->serverprocess)
    {
        /*  Wenn der Server-Prozess noch nicht läuft, starten wir ihn
         *  mit CreateNewProc() der dos.library. Der neue Prozess beginnt
         *  dann mit der Funktion server(). Nach CreateNewProc() müssen wir
         *  unbedingt warten, bis der neue Prozess auch wirklich läuft.
         *  Daher warten wir, bis der Prozess uns dies signalisiert.
         *  Für diesen Mechanismus brauchen wir die globalen Daten
         *  startprocess und startsignalmask. Der Server-Prozess setzt
         *  vor dem Signal auch die globale Variable startservererror.
         */
        if (DOSBase = OpenLibrary("dos.library", 37))
        {
            if ((signal = AllocSignal(-1)) != -1)
            {
                sb->startprocess = FindTask(NULL);
                sb->startsignalmask = 1 << signal;

                if (sb->serverprocess = CreateNewProcTags(NP_Name, (ULONG)"GP Server",
                                                          NP_Entry, (ULONG)server,
                                                          TAG_DONE))
                {
                    /* Nun warten wir, bis der neue Prozess sich meldet ... */
                    Wait(sb->startsignalmask);

                    /* Jetzt können wir startservererror auswerten.
                     * Den Inhalt müssen wir uns außerdem in einer lokalen
                     * Variable merken, bevor wir ReleaseSemaphore() aufrufen,
                     * da er später noch gebraucht wird.
                     */
                    error = sb->startservererror;
                }
                FreeSignal(signal);
            }
            CloseLibrary(DOSBase);
        }
    }
    else error = ENVOYERR_NOERROR;

    ReleaseSemaphore(&sb->startsema);

    /*  Diese Funktion muß zwei Ergebnisse liefern:
     *  1. In den Datenbereich, auf den das Tag-Item SSVC_EntityName
     *     zeigt, muß der Name der Entity des Servers kopiert werden.
     *  2. Ein Envoy-Fehlercode wird zurückgegeben.
     */

    if (UtilityBase = OpenLibrary("utility.library", 37))
    {
        if (ti = FindTagItem(SSVC_EntityName, taglist))
        {
            /* Leider ist nirgends spezifiziert, wie groß der Datenbereich
             * ist, auf den SSVC_EntityName zeigt. Daher sollte der Name
             * nicht besonders lang sein ...
             */
            strcpy((char *)ti->ti_Data, "GPServer");
        }
        CloseLibrary(UtilityBase);
    }

    return(error);
}

void __asm
GetServiceAttrsA(register __a0 struct TagItem *taglist,
                 register __a6 struct svcbase *sb)
{
    /*  Diese Funktion eines Envoy-Services wird derzeit nur von
     *  "Services Configuration" benutzt.
     *
     *  Die Aufgabe dieser Funktion ist es, den Namen des Services
     *  zurückzuliefern.
     */
    struct TagItem *ti;
    struct Library *SysBase = *(struct Library **)4;
    struct Library *UtilityBase;

    if (UtilityBase = OpenLibrary("utility.library", 37))
    {
        if (ti = FindTagItem(SVCAttrs_Name, taglist))
        {
            strcpy((char *)ti->ti_Data, "GPServer");
        }
        CloseLibrary(UtilityBase);
    }
}

void __asm
RexxReserved(void)
{
    /*  Diese Funktion eines Envoy-Services ist für den Dispatcher
     *  einer ARexx-Library reserviert, den man benötigt, wenn dieser
     *  Service auch gleichzeitig eine ARexx-Library sein soll.
     *  Wird hier nicht benutzt.
     */
}


/********************************************************************
 *                  Funktionen des Service-Prozesses                *
 *                                                                  *
 *  Alle nachfolgenden Funktionen sind keine Funktionen, die von    *
 *  "außen" angesprungen werden können. Sie können also nicht       *
 *  mehrmals gelichzeitig abluafen und müssen somit nicht unbedingt *
 *  reentrant sein. Alle diese Funktionen laufen nur im Kontext des *
 *  Server-Prozesses, der in StartServiceA() gestartet wird.        *
 *                                                                  *
 *******************************************************************/

static void server(void)
{
    /*
     *  Der Server-Prozess, der in StartServiceA() gestartet wird,
     *  beginnt mit dieser Funktion.
     */
    struct Library *SysBase = *(struct Library **)4;
    struct svcbase *sb;

    /* Zunächst öffnen wir unseren eigenen Service, damit
     * + der Service nicht "expunged" werden kann, waehrend dieser
     *   Prozess läuft
     * + wir an unsere globalen Daten kommen, die sich in der Struktur
     *   svcbase befinden
     */
    if (sb = (struct svcbase *)OpenLibrary("GP.service", 0))
    {
        /* alle benötigten Resourcen allozieren */
        if (openresources(sb))
        {
            /* startservererror setzen und StartServiceA() signalisieren,
             * daß diese Variable nun ausgewertet werden kann
             */
            sb->startservererror = ENVOYERR_NOERROR;
            Signal(sb->startprocess, sb->startsignalmask);

            /* auf einkommende Nachrichten warten und diese bearbeiten */
            processmessages(sb);

            /* alle Resourcen wieder freigeben */
            closeresources(sb);
        }
        else
        {
            /* startservererror setzen und StartServiceA() signalisieren,
             * daß diese Variable nun ausgewertet werden kann
             */
            sb->startservererror = ENVOYERR_BADSTARTSERVICE;
            Signal(sb->startprocess, sb->startsignalmask);
        }

        ObtainSemaphore(&sb->startsema);
        sb->serverprocess = NULL;
        ReleaseSemaphore(&sb->startsema);

        CloseLibrary((struct Library *)sb);
    }
}

/*  Um Änderungen gegenüber dem Code in GPServer.c zu sparen,
 *  definieren wir hier einfach die globalen Variablen um.
 *  Für die Library-Bases muß dies sowieso gemacht werden.
 */

#define SysBase         (sb->ml_ExecBase)
#define IntuitionBase   (sb->intuitionbase)
#define DOSBase         (sb->dosbase)
#define NIPCBase        (sb->nipcbase)
#define entity          (sb->entity)
#define entitysignal    (sb->entitysignal)

static BOOL openresources(struct svcbase *sb)
{
    if (IntuitionBase = OpenLibrary("intuition.library", 37))
    {
        if (DOSBase = OpenLibrary("dos.library", 37))
        {
            if (NIPCBase = OpenLibrary("nipc.library", 39))
            {
                /* öffentliche Entity schaffen */
                if (entity = CreateEntity(ENT_Name, "GPServer",
                                          ENT_AllocSignal, (ULONG)&entitysignal,
                                          ENT_Public, TRUE,
                                          TAG_END))
                {
                    return(TRUE);
                }

                CloseLibrary(NIPCBase);
            }
            CloseLibrary(DOSBase);
        }
        CloseLibrary(IntuitionBase);
    }
    return(FALSE);
}

static void closeresources(struct svcbase *sb)
{
    /*
     *  Hier schliessen wir alle Libraries, die in openlibraries()
     *  geöffnet wurden.
     */
    if (entity) DeleteEntity(entity);
    if (NIPCBase) CloseLibrary(NIPCBase);
    if (DOSBase) CloseLibrary(DOSBase);
    if (IntuitionBase) CloseLibrary(IntuitionBase);
}

static void processmessages(struct svcbase *sb)
{
    struct Transaction *ta;
    ULONG entitysigmask = 1 << entitysignal;
    ULONG sigs;

    while (1)
    {
        /*
         *  Wir warten so lange auf Nachrichten an unsere Entity
         *  bis ein CTRL_C-Signal ankommt ...
         */
        sigs = Wait(entitysigmask | SIGBREAKF_CTRL_C);
        /*
         *  Ist eine Transaction angekommen?
         */
        if (sigs & entitysigmask)
        {
            /*
             *  Alle angekommenen Transactions bearbeiten ...
             */
            while (ta = GetTransaction(entity))
            {
                handle_transaction(ta, sb);
            }
        }
        /*
         *  wenn wir ein CTRL_C Signal bekommen, beenden wir diese
         *  Endlosschleife und somit das ganze Programm
         */
        if (sigs & SIGBREAKF_CTRL_C) return;
    }
}

static void handle_transaction(struct Transaction *ta, struct svcbase *sb)
{
    if (ta->trans_Type == TYPE_SERVICING)
    {
        /*  Hier behandeln wir alle Transactions vom Typ TYPE_SERVICING
         *  (siehe Phasen einer Transaction)
         *  Andere Typen treten in diesem einfachen Programm allerdings
         *  sowieso nicht auf.
         */

        ta->trans_RespDataActual = 0;
        ta->trans_Error = ENVOYERR_NOERROR;

        /*
         *  Je nach Kommando der Transaction machen wir was anderes ...
         */
        switch (ta->trans_Command)
        {
            case TACMD_REQUEST:
                transaction_request(ta, sb);
                break;

            case TACMD_CMDLINE:
                transaction_cmdline(ta, sb);
                break;

            case TACMD_GETTIME:
                transaction_gettime(ta, sb);
                break;

            default:
                /*
                 *  Dieses Kommando kennen wir nicht ...
                 */
                ta->trans_Error = ENVOYERR_CMDUNKNOWN;
                break;
        }
        /*
         *  Nun schicken wir die Transaction zurück.
         */
        ReplyTransaction(ta);
    }
}

static void transaction_request(struct Transaction *ta, struct svcbase *sb)
{
    struct EasyStruct easyreq =
    {
        sizeof (struct EasyStruct), /* ULONG es_StructSize      */
        0,                          /* ULONG es_Flags           */
        NULL,                       /* UBYTE *es_Title          */
        NULL,                       /* UBYTE *es_TextFormat     */
        NULL,                       /* UBYTE *es_GadgetFormat   */
    };
    unsigned char *data;
    ULONG datalen;
    LONG *response;
    ULONG responselen;

    /* Bei dieser Transaction bekommen wir Daten und wir liefern eine
     * Antwort zurück.
     */
    data = ta->trans_RequestData;           /* Daten, die wir bekommen haben */
    datalen = ta->trans_ReqDataActual;      /* Länge dieser Daten in Bytes   */
    response = ta->trans_ResponseData;      /* Speicher für unsere Antwort   */
    responselen = ta->trans_RespDataLength; /* max. Länge unserer Antwort    */

    /*  Zur Sicherheit überprüfen wir die gewonnen Daten:
     *  + data und response dürfen nicht NULL sein
     *  + datalen muß größer 3 sein, da data mindestens 3 Nullbytes
     *    enthalten muß
     *  + das letzte Zeichen in data muß ein Nullbyte sein
     *  + responselen muß mindestens 4 sein, damit das Ergebnis Platz hat
     */
    if (data && datalen >= 3 && data[datalen - 1] == '\0' &&
        response && responselen >= sizeof(LONG))
    {
        /*
         *  Für EasyRequest() müssen wir nun die einzelnen Strings für
         *  Titel, Text und Gadgets aus dem Speicherbereich, auf den data
         *  zeigt, ermitteln: in Data folgen die drei Strings hintereinander,
         *  getrennt durch Nullbytes.
         */
        easyreq.es_Title = data;            /* der Titel                    */
        data = data + strlen(data) + 1;     /* etwas Pointerarithmetik      */
        easyreq.es_TextFormat = data;       /* der Text                     */
        data = data + strlen(data) + 1;     /* noch etwas Pointerarithmetik */
        easyreq.es_GadgetFormat = data;     /* die Gadgets                  */
        /*
         *  Nun den Requester öffnen und das Ergebnis als Antwort in die
         *  Transaction eintragen.
         */
        *response = EasyRequestArgs(NULL, &easyreq, NULL, NULL);
        /*
         *  Die Länge unserer Antwort nach trans_RespDataActual.
         */
        ta->trans_RespDataActual = sizeof(LONG);
    }
}

static void transaction_cmdline(struct Transaction *ta, struct svcbase *sb)
{
    BPTR infh;
    BPTR outfh;
    unsigned char *data;
    unsigned char *command;
    unsigned char *output;
    ULONG datalen;

    /* Bei dieser Transaction bekommen wir nur Daten, liefern aber keine
     * Antwort (abgesehen von trans_Error) zurück.
     */
    data = ta->trans_RequestData;           /* Daten, die wir bekommen haben */
    datalen = ta->trans_ReqDataActual;      /* Länge dieser Daten in Bytes   */

    /*  Zur Sicherheit überprüfen wir die gewonnen Daten:
     *  + data darf nicht NULL sein
     *  + datalen muß größer 2 sein, da data mindestens zwei Nullbytes
     *    enthalten muß
     *  + das letzte Zeichen in data muß ein Nullbyte sein
     *
     *  (Antwort gibt es hier keine, abgesehen von trans_Error)
     */
    if (data && datalen >= 2 && data[datalen - 1] == '\0')
    {
        /* Die Länge unserer Antwort ist Null. */
        ta->trans_RespDataActual = 0;

        /* In Data folgen die beiden Strings für das Kommando
         * und die Ausgabedatei hintereinander, getrennt durch ein Nullbyte.
         */
        command = data;                     /* das Kommando                 */
        output = data + strlen(data) + 1;   /* die Ausgabedatei             */

        if (*output == '\0') output = "CON:";

        if (infh = Open("NIL:", MODE_NEWFILE))
        {
            if (outfh = Open(output, MODE_NEWFILE))
            {
                if (SystemTags(command, SYS_Asynch, TRUE,
                                        SYS_Input, infh,
                                        SYS_Output, outfh,
                                        TAG_END) == 0)
                {
                    /* Kein Fehler aufgetreten: zurück. */
                    return;
                }
                else ta->trans_Error = GPSERR_SYSTEMFAILED;

                /* Wenn SystemTags() fehlschlug, müssen wir die Filehandles
                 * wieder freigeben.
                 */
                Close(outfh);
            }
            else ta->trans_Error = GPSERR_OPENOUTFH;

            Close(infh);
        }
        else ta->trans_Error = GPSERR_OPENINFH;
    }
}

static void transaction_gettime(struct Transaction *ta, struct svcbase *sb)
{
    struct timeval *response;
    ULONG responselen;

    /* Bei dieser Transaction bekommen wir keine Daten, aber wir liefern eine
     * Antwort.
     */
    response = ta->trans_ResponseData;      /* Speicher für unsere Antwort   */
    responselen = ta->trans_RespDataLength; /* max. Länge unserer Antwort    */

    /*  Zur Sicherheit überprüfen wir die gewonnen Daten:
     *  + response darf nicht NULL sein
     *  + responselen == sizeof(struct timeval)
     */
    if (response && responselen == sizeof(struct timeval))
    {
        if (getsystemtime(response, sb))
        {
            /* Die Länge unserer Antwort */
            ta->trans_RespDataActual = sizeof(struct timeval);
        }
        else
        {
            /* es ist ein Fehler aufgetreten */
            ta->trans_RespDataActual = 0;
            ta->trans_Error = GPSERR_NOTIME;
        }
    }
}

static BOOL getsystemtime(struct timeval *tv, struct svcbase *sb)
{
    #define TimerBase ((struct Library *)timerio->tr_node.io_Device)
    struct timerequest *timerio;
    BOOL ok = FALSE;

    if (timerio = AllocMem(sizeof(struct timerequest), MEMF_PUBLIC | MEMF_CLEAR))
    {
        if (!OpenDevice(TIMERNAME, UNIT_MICROHZ, (struct IORequest *)timerio, 0))
        {
            GetSysTime(tv);
            ok = TRUE;
            CloseDevice((struct IORequest *)timerio);
        }
        FreeMem(timerio, sizeof(struct timerequest));
    }
    return(ok);
}
