AT - Watchtower im Atari

In der UNIX-Welt ist die Hintergrundverarbeitung von Kommandos gang und gebe. Einen wichtigen Bestandteil zur Büroautomatisation bietet der Befehl AT. Auf ihn bauen Mail-Systeme, Backup-Lösungen und andere komplexe Büroaufgaben auf. Auch auf dem Atari sollte es möglich sein, für die vielen vorhandenen Shells ein solches Tools aufzubauen. Natürlich sind dabei einige Klippen des TOS zu umschiffen.

Der Befehl AT wird meistens in ähnlicher Syntax aufgerufen, wie man einen kurzen Satz formulieren würde, z.B. „at 13:44 print teste“. In dieser Implementierung wird vor dem auszuführenden Befehl noch als eindeutige Trennung zu den Optionen ein großgeschriebenes DO eingefügt. Es soll verhindern, daß falsch geschriebene Optionen oder Zeiten als Befehl angesehen werden. Die gesamte dokumentierte Syntax steht in den Anfangskommentaren des Listings.

AT soll nun im Hintergrund warten und einen Befehl aus einer Liste ausführen, falls er zum jetzigen Zeitpunkt auszuführen ist. Dabei wird das Standard-C-Kommando System verwendet. Hier kommt auch schon die erste Einschränkung. AT benötigt eine Shell, die über die Systemvariable _shell_p das Kommando System zugänglich macht. Da der Aufruf auch in GEM-Programmen funktioniert, ist es ratsam, keine Kommandos aufrufen zu lassen, die eine Bildschirmausgabe machen, sondern entweder darauf zu verzichten, oder sie umleiten zu lassen („>“). Eine Alternative dazu ist Gemini mit der Mupfel-Shell im GEM-Fenster. Hier kann man, solange man sich in Gemini befindet, auch Bildschirmausgaben über stdout machen lassen, die dann im Mupfel-Fenster erscheinen. Dazu wird vom Wecker das auszuführende Programm über das Gemini-Protokoll, und nicht über _shell_p aufgerufen. Es ist ohne Gemini auch nicht ratsam, Programme (nicht interne Befehle) über die Shell aufzurufen, da dann Bomben am Bildschirm nicht auszuschließen sind. Man sollte seine eigene Shell darauf testen, um Fehler zu vermeiden.

Ein Problem stellt die Funktion zum Testen des Vorhandenseins von Gemini dar, da es in allen aufgerufenen Programmen fehlerhafterweise über appl_find zu finden ist. Deshalb wird über die aktuelle Basepage getestet, ob Gemini aktiv ist. Andere Shell-Besitzer können diese Teile auslassen.

Um AT von der Kommandozeile und aus Stapeldateien zu bedienen, ist es in zwei Teile getrennt, was außerdem Speicherplatz spart. Der Steuerungsteil ist eine normales TTP-Programm, das die Auswertung der Zeiteingabe übernimmt. Sinnigerweise ist dieses Tool mit „AT“ benannt. Die Zeitüberwachung und Ausführung, der zweite, nicht weniger wichtige Teil des Utilities, ist das Accessory „WECKER.ACC“, es überwacht die Liste mit den Zeitereignissen. Um sich die Zusammenarbeit der beiden Teile besser vorstellen zu können, will ich den Ablauf einer Zeiteintragung nun anhand eines Beispiels veranschaulichen: Zuerst wird vom Benutzer AT.TTP aufgerufen. Er gibt dabei zum Beispiel „at 16:55 DO copy c:*.c d:“ ein. In AT wird nun die Zeit in Sekunden seit dem 1.1.1970 GMT berechnet, und nach einer eventuellen Sicherheitsabfrage das Kommando in eine numerierte Batch-Datei in ein Wecker-Verzeichnis kopiert. Danach sendet AT eine AES-Message an WECKER und wartet mit einem Timeout auf die Empfangsbestätigung von WECKER. Tritt ein Fehler auf, wird dieser ausgegeben, und die Batch-Datei mit dem Kommando gelöscht.

WECKER empfängt eine Nachricht von AT und versucht, das Zeitereignis in seiner internen statischen Liste einzuordnen. Gelingt das, wird die Eintragsnummer zurückgegeben, ansonsten der Fehlercode. Das Einträgen des Ereignisses ist somit abgeschlossen. Nun läuft WECKER normal im Hintergrund weiter und überprüft alle 1,5 Sekunden, ob ein Ereignis in der Liste auszuführen ist. Wenn ja, wird kontrolliert, ob überhaupt eine Shell vorhanden ist. Erst dann kann das Kommando ausgeführt werden. Dazu wird einfach ein String mit dem Namen der numerierten Batch-Datei an die Shell übergeben, die diese dann ausführt.

Beim vorigen Beispiel würde die Shell also alle C-Sources von c:\ nach d: kopieren. Praktisch für ein Backup der Daten. Es hat jedoch wenig mit Automatisation zu tun, wenn man jedes auszuführende Kommando einmal mit AT aktivieren müßte. Deshalb habe ich noch die ei/e^y-Option in die Programme integriert. Gibt man nach der Zeit bzw. nach dem Datum noch die Option +1day ein, wird dieses Backup ab sofort täglich gemacht. Als Einheit hinter der Zahl kann entweder second, minute, hour, month oder year stehen. Es werden die Zahlenwerte dann dementsprechend auf Sekundenwerte mit der Einheit hochmultipliziert. Dabei ist die Dauer eines Jahres folgendermaßen definiert: alle 4 Jahre ein Schaltjahr mit 366 Tagen, das alle 16 Jahre ausfällt. Ein Monat entspricht 1/12 Jahr. Deshalb sollte es nicht verwundern, daß eine langfristige Zeitdifferenz fast immer um ein paar Stunden in die Hose geht, (und das nach Murphy’s Gesetz natürlich besonders bei wichtigen Ereignissen!).

In den beiden Programmen sind keine besonderen Techniken vorhanden. Erwähnenswert ist vielleicht der Nachrichtenaustausch über das AES, der sicher sehr zum Nachbau in eigenen Applikationen zu empfehlen ist, da er weder XBRA, XREF, XARG, ARGV oder COOKIES benötigt, wobei jedoch eine offizielle Registrierung der belegten Nachrichtennummern sicher von Nutzen wäre, um Kollisionen zu verhindern.

Auch die Speicherverwaltung stellt in Accessories eine Hürde dar, da ein einfaches Malloc zu schweren Problemen führt, weil der Speicher nicht auf Dauer zur Verfügung steht. Durch die Trennung von Verwaltung und Ausführung ist man hier jedoch mit einer statischen Liste gut bedient, da das Datenaufkommen kaum mehr als 200 Ereignisse zu je 10 Bytes beträgt. Diese 2 KBytes sind bei einer heutigen zig-MB Maschine wohl kaum mehr der Rede wert.

Im allgemeinen kann man folgende Sparmaßnahmen in TC/PC-Compilaten vornehmen: Erstens sollte man den Stack möglichst klein bemessen. 1-2 KBytes reichen bei den meisten Anwendungen vollkommen. Nur bei einigen stdio-Kommandos kann es zu Problemen kommen, auf die man aber elegant durch einen Datenverlust oder ein paar schöne Bomben aufmerksam gemacht wird. Sehr gefährlich sind hier die Funktionen printf und Pendanten, die minimal 2 KBytes benötigen.

Eine weitere Maßnahme ist das Weglassen der FP-Library, da dann automatisch die Format-Funktionen {printf & Co) fast nur noch die Hälfte des Speicherbedarfes haben.

Die statische Liste mit den Ereignissen ist geordnet und besitzt Elemente der Struktur wecke. Dabei wird der Vorteil von C genutzt, einer strukturierten Variablen eine andere zuzuweisen. Eine sehr elegante Art der Programmierung, die die Programme kürzer und übersichtlicher gestaltet.

Die Programme sind im allgemeinen schon sehr abgespeckt und verlangen deshalb nach Erweiterungen. Bitte achten Sie darauf, daß Sie keine Zugriffe auf Systemvariablen und unsichere Interrupt-Programmierung einbauen, da dann die Kompatibilität zu MultiTOS und Derivaten nicht mehr gegeben ist. Arbeiten Sie stattdessen an leistungsfähigen Shellscripts, oder, falls Sie noch keine Shell besitzen, eben an einer Shell. Informationen dazu sollte die PC-Help-Funktion zu System oder diverse andere Lektüre bereitstellen.


/*
    AT.C

    (c) 1992 MAXON Computer

    Stellt Wecker auf eine gewisse Zeit.
    Syntax:
    at [-v] [-n] [hh:mm [dd.mm.jjjj]]
       [+###{s[econd] I mi[nute] I h[our]
       [ d[ay] | motnth] I y[ear]>] DO 'kommandozeile'
    
    -v: Verbose, volle Information (nur falls -n, sonst immer)
    -n: Keine Sicherheitsabfrage

*/

#include <tos.h>
#include <ext.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <aes.h>

char usage[] = "\x1bp Anwendung: \xlbq\n"
"at [-v] C-n] [hh:mm [dd.mm.jjjj]] [+###{s[econd] | mi[nute] | h[our]\n"
"   | d[ay] I mo[nth] | y[ear]}] DO 'kommandozeile'\n"
"-Reihenfolge beachten!\n";

int appl_id = -1;

#define TRUE -1 
#define FALSE 0

int verbose = FALSE, no_req = FALSE;

#define WORK_DIR    "C:\\WECKER\\"
#define KOMM_STR    WORK_DIR "TIME%04i.MUP"

#define WECK_MSG    2422 /* Nachrichtennummer für Weckerstart */
#defrne WECK_REQ    2423 /* Nachrichtennummer für Rückgabewert *7

typedef struct 
{
    time_t  next, offset; 
    int kommando;
} wecke;

void end_error(char *str);
int msg_num(char *msg);
void del_batch_file(int kommando_nr);
int request(void);
void send_msg(wecke *msg);

main(int argc, char *argv[])
{
    time_t exec_time;
    struct tm zeit = {0,0,0,0,0,0,0,0,0};
    int comm_beg, search = 0, i;
    long offset=0L;
    char line[128]="\0";
    wecke evnt;

    /*** SUCHE BEGIN DER AUFZURUFENDEN KOMMANDOSEILE: ***/ 
    for (comm_beg=0; comm_beg<argc && strcmp(argv[comm_beg], "DO"); comm_beg++);
        comm_beg++;
    if(comm_beg >= argc || comm_beg <2) 
        end_error(usage);

    /*** WERTE ARGUMENTE AUS: ***/ 
    while(*argv[++search] == '-') 
        switch(argv[search][1])
        {
            case 'N': case 'n': no_req = TRUE;
                break;
            case 'V': case 'v': verbose = TRUE;
                break;
            default: printf("Falsche Option: %s\n", argv[search]);
                end_error(usage);
        }
    if (!no_req) verbose = TRUE;

    /*** FALLS ZUMINDEST ZEIT GEGEBEN: ***/ 
    if(strchr(argv[search]
    {
        sscanf(argv[search++],"%2i:%2i",&zeit.tm_hour,&zeit.tm_min);

        /*** FALLS AUCH DATUM DABEI: ***/ 
        if(strchr(argv[search],'.'))
        {
            sscanf(argv[search++],"%2i.%2i.%4i"
                , &zeit.tm_mday, &zeit.tm_mon,
                &zeit.tm_year); 
            zeit.tm_mon--;
            if(zeit.tm_year > 1900) zeit.tm.year -= 1900;
        }
        else
        {
            /*** SONST DATUM VON HEUTE: ***/
            struct date act_date;
            getdate(&act_date);
            zeit.tm_mday = act_date.da_day;
            zeit.tm_mon = act_date.da_mon-1;
            zeit.tm_year = act_date.da_year-1900;
        }

        /*** ZEIT DARF NICHT IN VERGANGENHEIT LIEGEN: */ 
        exec_time = mktime(&zeit); 
        if(exec_time < time(NULL)) 
            exec_time = time(NULL);
    }
    else
        /*** FALLS GAR NICHTS GEGEBEN, VERWENDE AKTUELLE ZEIT ***/ 
        exec_time = time(NULL);

    if(verbose)                 /* INFO AUSGEBEN: */
    {
        zeit = *gmtime(&exec_time); 
        printf("\x1bp AT; Eventsteuerung \x1bq\n" 
               "Zeit: %i:%02i  Datum: %i.%i.%i\n", zeit.tm.hour+1, zeit. tm_.min ,zeit.tm_mday, zeit.tm_mon+1, 
               zeit.tm_year+1900);
    }

    /*** OFFSET BERECHNEN: ***/ 
    if(argv[search][0] == '+')
    {
        char s[20];
        sscanf(argv[search]+1, "’%li%s", &offset,s); 
        strupr(s); 
        switch(*s)
        {
            case 'S': break;    /* Sekunde */ 
            case 'M':
                switch(s[1])
                {
                    case 'I': offset *= 60L; break; /* Minute */
                    case 'O': offset *= 2629350L; break; /* Monat */ 
                    default: offset = 0L;
                }; break;
            case 'H': offset *= 3600L; break;   /* Stunde */
            case 'D': offset *= 86400L; break;  /* Tag */
            case 'Y': offset *= 31552200L; break;   /* Jahr */
            default: if(*s>=32) offset = 0L;
        }
        if(offset == 0);
        {
            printf("Falsche Zeiteingabe: %s\n", argv[search]+1); 
            end_error(usage);
        }
    }

    /*** REST VON KOMMANDOZEILE ZUSAMMENFÜGEN: ***/ 
    for(strcat(line,argv[i=comm_beg]); i<(argc-1); 
        strcat(line,argv[++i])) 
        strcat(line," ");

    if(verbose) printf("Kommandozeile: %s\n", line); 
    if(verbose && offset)
        printf("Ausführung alle %li Sekunden.\n", offset);

    if((i=msg_num(line)) < 0)
        end_error("Konnte Datei in 'WORK_DIR' nicht öffnen! \n");

    /*** WECKSTRUKTUR AUSFÜLLEN: ***/ 
    evnt.next = exec_time; 
    evnt.offset = offset; 
    evnt.kommando = i;

    /*** SICHERHEITSABFRAGE: ***/ 
    if (!no_req)
    {
        printf("Sicherheitsabfrage! Falls alles stimmt, drücken Sie ':'"); 
        if(getehar() != ':')
            end_error(“Sie haben die Operation abgebrochen...\n“);
    }

    appl_id = appl_init();

    /*** NACHRICHT SENDEN: ***/ 
    send_msg(&evnt);
    if(verbose) printf("Nachricht an Wecker gesendet...");

    /*** BESTÄTIGUNG ERWARTEN: */ 
    switch(i=reguest())
    {
        case -1:
            del_batch_file(evnt.kommando); 
            end_error("Zeitpuffer voll. Andere Ereignisse Stornieren!\n");
        
        case -2:
            del_batch_file(evnt.kommando); 
            end_error("WECKER.ACC antwortet nicht!\n");

        default:
            if(verbose)
                printf("Empfang bestätigt. Noch %i Ereignisse davor.\n"
                    "Alles OK\n", i);
    }
    appl_exit(); 
    return 0;
}

void end_error(char *str)
{
    printf("%s",str);
    if(appl_id >= 0) appl_exit();
    exit(0);
}

int msg_num(char *msg)
{
    char f_name[128]; 
    int t, han;

    for(t = 0; t<10000; t++)
    {
        sprintf(f_name, KOMM_STR, t); 
        if(Fsfirst(f_name,0))
        {
            han = Fcreate(f_name,0); 
            if(han<0)
                {t=-1; break;}
            Fwrite(han, strlen(msg), msg);
            Fclose(han); 
            if(verbose)
                printf("Zum löschen der Weckzeit löschen Sie %s\n", f_name);
            break;
        }
    }
    return t;
}

void del_batch_file(int kommando_nr)
{
    char dateiname[128];
    sprintf(dateiname, KOMM_STR, kommando_nr); 
    Fdelete(dateiname);
}

int request(void)
{
    int in[8], d, e_art; 
    do 
    {
        e_art = evnt_multi(MU_TIMER | MU_MESAG, 
                0,0,0,0,0,0,0,0,0,0,0,0,0, 
                in, 3000, 0, /* Low/High-Counter Timer */
                &d,&d,&d,&d,&d,&d);

    } while(e_art != MU_TIMER && !(e_art == MU_MESAG && in[0] == WECK_REQ)); 
    if (e_art == MU_TIMER) 
        return -2; 
    return in[3];
}

void send_msg(wecke *msg)
{
    int ap_id = appl_find("WECKER  "); 
    int buf[8] = {WECK_MSG,0,0};

    if(ap_id < 0)
        end_error("WECKER.ACC muß unter diesem Namen gestartet sein!\n"); 
    buf[1] = appl_id;
    *(wecke*)(&buf[3]) = *msg; 
    appl_write(ap_id, 16, buf);
}

Harald Wörndl-Aichriedler
Aus: ST-Computer 09 / 1992, Seite 92

Links

Copyright-Bestimmungen: siehe Über diese Seite