Ma Xiu Jia

Hoofdstuk 3. Interactief luik

3.1 Inleiding

Een schijnbaar onooglijke frase in de beschrijving van het doel van dit afstudeerwerk luidde: " (...) dat ook anderen met onze machine kunnen experimenteren." Het is een zinnetje dat af en toe voor kopbrekens zorgde. In wat nu volgt wordt beschreven wat precies bedoeld wordt met het omineuze "experimenteren", en wordt geprobeerd kernachtig de implementatie van dat alles uit te doeken te doen.

3.1.1 Wat wordt aangeboden?

Het is eenvoudig om breeduit te claimen dat je anderen wilt laten experimenteren. Moeilijker wordt het wanneer je tracht vast te leggen wat het precies is dat je de buitenwereld wil aanbieden. Om dat te omschrijven is iets meer achtergrondinformatie omtrent de CAM-Brain nodig. In de inleiding werd reeds gezegd dat de CAM-Brain een implementatie is in hardware van een groot digitaal neuraal netwerk. Het voorkomen van de term "brain" in de naam van de machine is niet toevallig. De architectuur van de CAM-Brain steunt stevig op inzichten uit de neurologie. Ook bij de CBM immers spreekt men, net als bij hersenen, van axonen, dendrieten en neuronen. Deze drie componenten kunnen op een veelheid aan verschillende manieren gekoppeld worden, wat aanleiding geeft tot een even groot aantal "neurale netwerken".

De CBM kan op verschillende gebruikt worden. Zo kan de machine gebruikt worden om op basis van een zogenaamd genotype of chromosoom, en een aantal inputs een neuraal netwerk te ontwikkelen. Men spreekt in dit verband van evolution mode, en daarmee wordt bedoeld dat men de computer een groot aantal uitkomsten geeft en tegelijk de opdracht om de beste manier uit te dokteren om tot die uitkomst te komen. Men spreekt in dit verband ook over evolvable hardware: afhankelijk van de aard en de uitwerking van een specifieke opdracht worden verschillende hard-wired of hardbedrade netwerken gevormd.

Een andere manier waarop de CBM gebruikt kan worden is om te onderzoeken welke output men krijgt indien men een bepaalde input door een reeds ontwikkeld neuraal netwerk te laten lopen. Men kan op die manier een bijzonder complexe schakeling eenvoudig in hardware implementeren, in plaats van die in software te simuleren, en er daarna bijzonder snel een reeks bits doorsturen. We spreken hier over de run mode. Het is deze mogelijkheid die we via de website wensen aan te bieden.

3.1.2 Hoe wordt dit geïmplementeerd?

Figuur 14: CAM-Brain in run mode

Om de CAM-Brain een taak te laten uitvoeren in run mode, zijn drie bestanden van drie verschillende types nodig. Een eerste type bestanden, de genotype/fenotype (in dit geval gaat het om fenotypes) of ".gnt" bestanden, beschrijven de configuratie van het ontwikkeld neuraal netwerk. In is het fenotype bestand te begrijpen als een beschrijving van de module links, die een specifieke koppeling voorstelt van axonen, dendrieten en neuronen. Deze module (of het neuraal netwerkje) wordt veronderstelt bevat te zijn in de rechter kubus.

Een tweede type bestanden dat nodig is, de netlist of ".ntl" bestanden, geven aan wat de verbinding is tussen het ontwikkeld neuraal netwerk en de buitenwereld. Het bestand beschrijft waar spiketrains of pulstreinen als input aangelegd moeten worden, waar output verwacht mag worden, en bovendien welke pulstreinen aan welke ingangen aangelegd moeten worden. Het derde type bestanden, de of spiketrain, pulstrein of ".spt" bestanden, zijn dan de reeksen bits die als input aan het neuraal netwerk worden aangeboden. De diamantvormige kadertjes op de kubus, rechts in , stellen de in- en uitgangen van het netwerk voor. De stippelpijlen zijn de pulstreinen die respectievelijk aangelegd worden en ontspruiten aan het netwerk. Het netlist bestand bepaalt, indien we bij het voorbeeld blijven, waar zich de diamantjes op de kubus bevinden en welke pijlen aan welke diamantjes worden aangeboden. Het spiketrain bestand bepaalt uit welke opeenvolging van bits de intredende pijlen bestaan.

De CAM-Brain kan uit drie bestanden genoeg informatie puren om een taak in run mode uit te voeren. Er moet wel rekening gehouden worden met het feit dat het afwerken van opdrachten van gebruikers in real time (op het ogenblik dat het verzoek ontvangen wordt) niet praktisch haalbaar is. Dat heeft onder andere te maken met het feit dat onderzoek met de CAM-Brain binnen de onderzoeksgroep PARIS voorrang heeft, en dat de CBM geen twee of meer verschillende opdrachten tegelijk kan uitvoeren.

Op basis van deze factoren valt dan de vraag, naar hoe de CBM via het internet wereldwijd beschikbaar kan gesteld worden, te reduceren tot de volgende reeks deelvragen, waarbij ook meteen een eerste tentatief antwoord kan geformuleerd worden.

Hoe raken de bestanden van de gebruiker tot bij de CAM-Brain?

Een aanvaardbare oplossing in dat verband bestaat erin aan de gebruiker de mogelijkheid te bieden de bestanden te uploaden, en de bestanden daarna lokaal te bewaren totdat ze verwerkt kunnen worden.

Hoe kunnen we de CAM-Brain die bestanden correct laten behandelen?

De CAM-Brain wordt normalerwijs aangestuurd door een extern programma. Het ligt dus voor de hand dat dit (of een gelijkaardig) programma wordt opgeroepen en dat het op één of andere manier duidelijk wordt gemaakt welke bestanden het moet verwerken.

Hoe kunnen we de gebruiker op de hoogte brengen van de resultaten?

De meest voor de hand liggende oplossing is in dit geval de beste. De gebruiker wordt een e-mail toegezonden met in bijlage, als attachment, het bestand dat door de CBM werd uitgevoerd (of een foutmelding indien er zich een fout voordeed).

Hoe kunnen we gegevens bijhouden en controleren?

Figuur 15: Drie pijlers van het interactief luik

De vraag suggereert een antwoord waarin de term databank geen vreemde is. Een databank is in feite ideaal om op een robuuste en correcte manier informatie door te geven tussen niet direct interagerende applicaties. Tegelijk biedt een databank de mogelijkheid om gegevens te archiveren (om bijvoorbeeld op later datum in beperkte mate aan data mining te doen. Het zou immers kunnen dat men een idee wil krijgen van het hoe, wat, waarom en wanneer van de opdrachten, of die geslaagd zijn, of gebruikers meerdere opdrachten doorstuurden, en hoeveel tijd daar gemiddeld tussen verliep, ...).

In verband met de website werd alles geconcentreerd rond een drietal pijlers. Er is aan de ene kant het PHP script "Process.html" dat zorgt voor de directe interactie met de gebruiker. Gebruikers die een taak wensen in te voeren moeten overigens eerst geregistreerd zijn, en moeten hun wachtwoord invullen. Aan de andere kant, en onafhankelijk daarvan, staat "CBMTimer", een Java classfile dat samen met enkele andere classfiles zorgt voor de interactie met de CBM. Tussen die twee in staat "cambrainDB", een MySQL databank waarin gegevens over de verschillende ingezonden taken bijgehouden worden. toont hoe deze drie pijlers fysisch doorheen de vakgroep ELIS verspreid zijn. In de volgende paragrafen worden elk van die pijlers iets uitvoeriger behandeld.

3.2 "Rites of Passage": registratie van gebruikers

Vooraleer gebuikers taken kunnen doorgeven aan de CAM-Brain, moeten zij geregistreerd zijn. De bedoeling daarvan staat volledig los van het vragen van een pecuniaire beloning voor de geleverde diensten. Het is daarentegen zo dat de registratie ervoor zorgt dat de CAM-Brain niet nodeloos ingezet wordt voor het behandelen van nep-opdrachten. Er werd in de vorige paragraaf al vermeld dat de gebruiker het resultaat van zijn opdracht toegestuurd moet krijgen via e-mail. We moeten dus op een bepaalde manier zekerheid kunnen krijgen over de vraag of een bepaald e-mailadres echt functioneel is (met name, of het adres echt is, en of de gebruiker die het invoerde er controle over heeft). Dit wordt gecontroleerd via de registratie-procedure: gebruikers registreren zich door een persoonlijk e-mailadres in te voeren. Er wordt dan naar het ingevoerde e-mailadres een e-mail verstuurd met daarin een wachtwoord. Met dit wachtwoord kan de gebruiker dan tot het script "Process.html" doordringen, waarlangs een taak voor de CAM-Brain kan worden opgegeven.

De implementatie van deze wachtwoordbeveiliging werd gespreid over een vijftal PHP scripts. Het gaat hier met name om "Authorization.html", "DIY.html", "newUser.html", "pwdRetrieve.html" en "pwdChange.html". Deze worden in de hiernavolgende alineaatjes kort toegelicht.

a) "Authorization.html": Deze bladzijde is in se niet veel meer dan een HTML formulier dat als taak heeft geregistreerde gebruikers door te laten naar "Process.html". Daartoe wordt van de gebruikers een e-mailadres (bij wijze van loginnaam) en een wachtwoord gevraagd. De door de gebruiker ingevoerde waarden worden doorgegeven aan de bladzijde "DIY.html", waar wordt bepaald of op basis van die waarden toegang verleend kan worden tot de CAM-Brain. De bladzijde biedt verder ook drie belangrijke, duidelijk zichtbare, hyperlinks. Het gaat hier om links die leiden naar een registratie-bladzijde voor nieuwe gebruikers ("newUser.html"), een bladzijde waarlangs men een vergeten wachtwoord kan opvragen ("pwdRetrieve.html") en een bladzijde waarlangs men een wachtwoord kan veranderen ("pwdChange.html").

b) "DIY.html": Deze bladzijde ligt, logischerwijs, in het verlengde van "Authorization.html". Het script ontvangt de waarden die de gebruiker in "Authorization.html" invoert. Deze waarden worden op hun echtheid gecontroleerd, aan de hand van een databank query. Het is namelijk zo dat in de databank cambrainDB een specifieke relatie of tabel ("user_table") is opgenomen met als functie e-mail/wachtwoord paren op te slaan. Indien de door de gebruiker ingevoerde gegevens correct zijn, wordt een formulier gepresenteerd aan de gebruiker waarin de gegevens, specifiek voor de CAM-Brain, kunnen ingevoerd worden. Indien de gegevens foutief zijn, of indien er zich een andere fout voordoet (zoals onbereikbaarheid van de databank) wordt een aangepaste foutmelding getoond.

c) "newUser.html": Het script "newUser.html" heeft als bedoeling een nieuwe gebruiker te registreren. Dat betekent concreet dat het e-mailadres van de gebruiker wordt gevraagd, waarna op basis van dat adres een wachtwoord wordt gegenereerd. De twee gegevens worden dan in de databank weggeschreven, en het wachtwoord wordt naar de gebruiker gemaild. Het wachtwoord wordt gegenereerd op basis van het e-mailadres van de gebruiker. De code dat daarvoor gebruikt wordt is als volgt:

$password = (substr(md5($email), 13, 5).substr("".crc32($email), 1, 3));

Eerst wordt de md5 hash berekent van het e-mailadres, dit levert een getal van 32 hexadecimale cijfers, die in een string variabele worden gestopt. Van die string worden 5 karakters geknipt vanaf positie 13, wat een nieuwe string variabele levert. Deze string wordt dan geconcateneerd met drie getallen (positie 1 tot en met 3) uit de crc32 checksum.3 Met brute rekenkracht kan dit algoritme ongetwijfeld achterhaald worden. Dit kan echter geen bezwaar zijn: er wordt immers geen vitale data bewaard in of om de website. De enige bedoeling van het gebruik van wachtwoorden is immers het vermijden van nep-opdrachten (opdrachten zonder opdrachtgever).

De databank-query die daarna wordt uitgevoerd is een standaard INSERT operatie (over de structuur en het gebruik van de databank wordt later meer gezegd). Een e-mail wordt eenvoudig gestuurd met de functie mail(String email, String subject, String message [additional headers]), beschikbaar in PHP. Deze functie verzendt automatisch een e-mail met behulp van het sendmail programma op de lokale server. Dit leidt tot het volgende eenvoudige regeltje code:

mail($email, "CBM at Ghent University", $registerMessage, "From: Hendrik.Eeckhaut@elis.rug.ac.be");

d) "pwdRetrieve.html": Het script "pwdRetrieve.html" heeft als bedoeling reeds geregistreerde gebruikers te helpen hun wachtwoord terug te vinden. Aangezien immers de wachtwoorden automatisch gegenereerd worden, en vaak niet mnemonisch van aard zijn, wordt verwacht dat die vaak vergeten worden. Er wordt, indien een gebruiker een wachtwoord is vergeten, gevraagd naar het e-mailadres van de gebruiker. Daarna wordt de databank geraadpleegd om te achterhalen of er aan het ingevoerde e-mailadres een wachtwoord is toegekend. Indien dit zo is wordt het wachtwoord gemaild naar het e-mailadres. In andere gevallen (indien bijvoorbeeld de databank onbereikbaar is, of indien het e-mailadres niet voorkomt in de databank) wordt een aangepaste foutmelding gepresenteerd.

e) "pwdChange.html": Het script "pwdChange.html" tenslotte laat gebruikers toe om hun wachtwoord te veranderen. Er werd reeds gewezen op het feit dat de automatisch gegenereerde wachtwoorden, die vrij arbitrair lijken voor het blote oog, een absoluut minimum aan mnemonische kwaliteit hebben. Daarom wordt aan geregistreerde gebruikers de mogelijkheid geboden het wachtwoord te veranderen in een meer geheugenvriendelijke variant. De werking van het script verschilt niet erg veel van de werking van de vorige scripts. Aanvankelijk wordt een formuliertje getoond waarin e-mailadres, oud wachtwoord en twee maal het nieuwe wachtwoord moeten worden ingevoerd. Het nieuwe wachtwoord wordt uiteraard twee maal gevraagd om te beschermen tegen tijpfouten; de ingevoerde karakters zijn op het scherm immers onleesbaar door gebruik van INPUT type="password" in de declaratie van het specifieke HTML FORM element. De invoer wordt dan gecontroleerd (zijn de ingevoerde nieuwe wachtwoorden gelijk?, staan e-mailadres en oud wachtwoord in de databank?) en indien aan alle controles wordt voldaan, wordt het nieuwe wachtwoord in de databank geplaatst en wordt een e-mail verzonden met daarin een bevestiging van het nieuwe wachtwoord.

3.3 "Process.html"

"Process.html" is niets minder dan de kers op de PHP-taart. Het hoofddoel van het script is zorgen voor een correcte ontvangst van verzoeken van gebruikers, maar het script doet meer dan dat. Een aantal functiebibliotheken van PHP werd aangesproken, waaronder vrij intensief de verzamelingen String functions, MySQL functions en Filesystem functions. De uitvoering van het script begint in een stuk code dat beurtelings op enkele zelfgedefinieerde functies beroep doet. Elk van deze functies retourneert een logische waarde op basis waarvan het verdere verloop van het programma bepaald wordt. Het hoofdblok van de code ziet er als volgt uit:

$email=trim($email);
if (emailIsValid($email)) {
    if ( (filenameIsValid($HTTP_POST_FILES['gntFile']['name'], '.gnt'))
        AND (filenameIsValid($HTTP_POST_FILES['ntlFile']['name'], '.ntl'))
        AND (filenameIsValid($HTTP_POST_FILES['sptFile']['name'], '.spt')) ) {
            $project=determineProjectName($projTitle, $HTTP_POST_FILES['gntFile']['name']);
            $headers=getallheaders();
            $hostIP=$headers["Host"];
            $hostIPName=gethostbyaddr($hostIP);
            $hostIPAddress=gethostbyname($hostIPName);
            process($email, $HTTP_POST_FILES, $project, $hostIPAddress, $hostIPName);
    }
}

Figuur 16: Hoofdfunctie 'Process.html'

Bij een eerste vluchtig overblikken van de code valt op dat dit blok op geen enkele plaats zorgt voor uitvoer. Daaraan ten grondslag ligt de ontwerpkeuze dat elke opgeroepen functie zelf voor eventuele foutmeldingen moet instaan: zo blijft de code overzichtelijk en logisch, en bovendien wordt het uitvoeren van erg specifieke foutmeldingen erdoor vergemakkelijkt.

Het eerste wat het PHP script "Process.html" uitvoert is een controle op het e-mail adres aan de hand van de functie emailIsValid. Deze functie werd geschreven toen nog geen sprake was van wachtwoordbeveiliging. Oorspronkelijk was het de enige manier om te voorkomen dat "spooktaken" werden uitgevoerd. De functie kijkt of er juist één @ voorkomt in het adres en of er na de @ minstens één punt voorkomt. De keuze om de functie in het script te laten is nogal aanvechtbaar. Voorlopig echter lijkt het geen exclusief slecht idee: de functie zorgt immers voor een extra controle van de ingevoerde gegevens.

Indien blijkt dat het e-mail adres geldig is wordt op elk van de drie upgeloade bestanden de functie filenameIsValid toegepast. Deze functie, die als argumenten een bestandsnaam en een extensie neemt, controleert of de bestandsnaam eindigt op de extensie. Aangezien deze taak bij elke oproep van het script verschillende malen uitgevoerd moet worden, en aangezien de code niet zó eenvoudig is dat ze op één lijn past, werd ervoor gekozen een specifieke functie te definiëren voor deze taak. Daardoor is er in principe ook wat meer plaats om de code immuun te maken tegen onregelmatigheden in de gebruikersinvoer. Zo wordt het bijvoorbeeld niet als fout aanzien indien de bestandsnaam begint of eindigt met een spatie teveel.

Een derde functie, determineProjectName is in feite niet strikt nodig. De functie is opgenomen in het script om, zoals de titel doet vermoeden, een naam te bezorgen aan een project. De nood naar een projectnaam doet zich het meest gevoelen wanneer een bestand dat door de CAM-Brain werd uitgevoerd naar de gebruiker wordt teruggestuurd. Om te vermijden dat zo"n bestand onder een nietszeggende generische naam naar de gebruiker wordt teruggestuurd, krijgt de gebruiker de kans om bij het invoeren van de gegevens ook een titel op te geven. Dit is echter niet verplicht. Het verwerken van gebruikersgegevens kan perfect verlopen zonder dat een titel opgegeven wordt. Indien de gebruiker geen titel opgeeft, wordt de titel van het genotype/fenotype (".gnt") bestand geknipt en gerecycleerd als titel van het terug te sturen resultaatbestand. De projectnaam die door dermineProjectName wordt afgeleverd wordt door de volgende functie, process, naar de databank weggeschreven.

Indien nu de verschillende functies (behalve dan determineProjectName, die een string retourneert) steeds de logische waarde waar teruggeven kan de functie process van wal steken met het zwaardere werk. Het verloop van deze functie valt wat algoritmisch te beschrijven als volgt:

1) Open een connectie met de databank cambrainDB;
2) a. Indien 1 lukt: achterhaal het volgnummer van het laatst toegevoegde record
   b. Indien 1 niet lukt: breng de gebruiker op de hoogte van de fout en verlaat de functie
3) a. Indien 2.a lukt: verplaats de door de gebruiker upgeloade bestanden uit de "./tmp" directory van de Apache-server naar de directory "./Uploads". Geef de verplaatste bestanden hierbij een nieuwe naam op basis van het volgnummer van het laatst toegevoegde record.
   b. Indien 2.a niet lukt: breng de gebruiker op de hoogte van de fout en verlaat de functie
4) a. Indien 3.a lukt: pas de databank aan.
   b. Indien 3.a niet lukt: breng de gebruiker op de hoogte van de fout en verlaat de functie.
5) a. Indien 4.a lukt: Verwijder uit de directory "Uploads" de bestanden die reeds behandeld zijn.
   b. Indien 4.a niet lukt: breng de gebruiker op de hoogte van de fout en verlaat de functie
6) Keer terug naar de oproepende code.

Het aanmaken van een connectie met de databank, cambrainDB, gebeurt in PHP in twee stappen. Eerst wordt een link gelegd met de MySQL server, via de functie MySQL_connect. Daarna wordt een bepaalde databank geselecteerd aan de hand van de functie MySQL_select_db. Deze werkwijze weerspiegelt de manier waarop men via een shell of via de DOS-prompt met MySQL werkt: het commando "MySQL" zorgt ervoor dat men een verbinding legt met de MySQL server. Het daaropvolgende commando "USE database name;" zorgt voor het leggen van een rechtstreekse verbinding met een specifieke databank.

Het achterhalen van het volgnummer of de primaire sleutel (te vinden in het veld request_id) van het laatste toegevoegde record heeft een specifieke functie. Alle upgeloade bestanden komen in dezelfde map terecht. Het zou kunnen dat bestanden uit twee opeenvolgende aanvragen dezelfde naam dragen. Om nu te vermijden dat de Apache-server, die het script uitvoert, een ouder (onverwerkt) bestand zou overschrijven met een gelijk getiteld nieuw bestand, worden de bestandsnamen die de gebruiker opgeeft bewaard in de databank, en worden de bestanden hertitelt. Daarvoor wordt een generische naam geconstrueerd volgens het sjabloon "Req_"#"."[gnt | ntl | spt] waarbij # staat voor het volgnummer van het laatst toegevoegde databank record vermeerdert met één.

Het verplaatsen van de upgeloade bestanden gebeurt met de functie move_uploaded_file(). Deze functie controleert eerst of de betrokken bestanden wel degelijk upgeloade bestanden zijn. De bedoeling daarvan is het vermijden van het bewerken van bestanden die niet door de Apache server bewerkt mogen worden, zoals bijvoorbeeld delicaat materiaal in de directory "/etc/passwd". Indien blijkt dat de bestanden geldig en upgeload zijn, worden ze verplaatst van de standaard temporary directory van de server, naar de subdirectory "./Uploads". Dat is een subdirectory binnen de directory waarin "Process.html" zich bevindt.

Indien de bestanden correct verplaatst werden (de functie move_uploaded_file() levert een logische variabele waarvan de waarde afhangt van het succes van de functie), wordt de databank aangepast. Er komt met name een record bij dat beschrijft wie welke bestanden heeft ingezonden, en wanneer en waarvandaan dat gebeurde. Het aanpassen van de databank gebeurt via dezelfde geopende connectie waarmee eerst al het volgnummer van het laatste record werd opgehaald. De query zelf ziet eruit als volgt:

INSERT INTO request_table
   (e_mail, gnt_file, ntl_file, spt_file, project_title, IP_address, IP_name)
   VALUES ('$email',
           "$HTTP_POST_FILES['gntFile']['name']',
           "$HTTP_POST_FILES['ntlFile']['name']",
           "$HTTP_POST_FILES['sptFile']['name']',
           "$project",
           "$hostIPAddress",
           "$hostIPName");

Figuur 17: INSERT statement in 'Process.html'

De laatste taak die specifiek door de functie process wordt uitgevoerd is het schoonmaken van de "./Upload" directory. Er waren oorspronkelijk problemen met permissies. De uitvoerder van het script, Apache, werd niet toegelaten bestanden te schrijven in de subdirectory "./Uploads" binnen mijn persoonlijke directory op ELIS. Om dit toch mogelijk te maken werd ervoor gekozen om Apache de eigenaar van die subdirectory te maken. Dit is mogelijk via het commando chown in een Unix shell (met dien verstande dat degene die het commando geeft zelf over de juiste permissies moet beschikken). Dit zorgde ervoor dat ik, en in extensio de buitenwereld, toegelaten wordt in de map te lezen, maar geen bestanden kan veranderen. Om nu tegen te gaan dat de directory onwelvoeglijke proporties zou aannemen, werd in "Process.html" een klein stukje code toegevoegd dat de subdirectory "./Uploads" doorloopt en per bestand kijkt of het al dan niet behoort tot een beantwoord verzoek. Indien het overkoepelende verzoek reeds behandeld is, wordt het bestand uitgeveegd. Dit is overigens niet onomkeerbaar. Zoals meteen zal uitgelegd worden, wordt van de bestanden uit de map "./Uploads" kopieën gecreëerd op de computer cambrain.elis.rug.ac.be, die niet automatisch uitgeveegd wordt. De reden daarvoor is te zoeken in de overweging dat de bestanden misschien nog van enig nut zouden kunnen blijken voor hen die met de CAM-Brain werken.

3.4 CBMTimer

CBMTimer is een programma geschreven in Java. De keuze voor Java is eenvoudig af te leiden uit het feit dat ik, en met mij enige tientallen studenten in de Aanvullende Studies Informatica, de wat grovere knepen van het programmeren aanleerde aan de hand van Java (voor de kleinere kneepjes is de tijd enigszins beperkt). De programmeertaal Java, ontwikkeld door Sun, biedt het bekende voordeel van platform-onafhankelijkheid, wat natuurlijk bijzonder mooi meegenomen was gezien de overvloed aan platformen waarop ik aan de applicatie sleutelde.

Aangezien dit afstudeerwerk een éénmansoperatie was, en bovendien het interageren met de CAM-Brain vrij complex is, werd besloten het programmeren van de directe aansturing niet op te nemen in de doelstelling van het afstudeerwerk. Hierdoor werd het duidelijker wat precies van mij verwacht werd. Wenselijk was het dat ik een programma codeerde dat een brugfunctie zou kunnen vervullen tussen de webapplicatie en de applicatie die de CBM aanstuurt.

De naam CBMTimer bestaat, zoveel is ondertussen genoegzaam bekend, uit twee delen. Het eerste deel, CBM, spreekt voor zich. De timer uit het tweede deel wijst op het cyclisch karakter van het programma. Het programma wordt eenmaal opgestart en blijft daarna actief, waarbij het cyclisch een bepaalde routine uitvoert. De routine komt er in hoofdzaak op neer dat bestanden die op behandeling wachten worden opgehaald en aan de CBM worden aangeboden, waarna op resultaat wordt gewacht. Afhankelijk van het resultaat wordt dan ofwel een mededeling omtrent het resultaat naar de gebruiker gezonden, ofwel wordt de opdracht als onafgehandeld gemarkeerd, zodat een latere cyclus de opdracht opnieuw zou kunnen behandelen.

Er rest me nog hier te vermelden dat het programma werkt op basis van een Command Line Interface, en niet op basis van een Graphical User Interface. Verschillende factoren gaven aanleiding tot die beslissing: het programma interageert nauwelijks met de gebruiker. Slechts één vorm van input is gewenst (doch niet noodzakelijk). Het betreft hier het interval tussen twee iteraties, en dat interval kan als programma-argument meegegeven worden. Ten tweede is het zo dat het programma continu loopt, en dus continu, zij het vaak slechts een fractie, systeemmiddelen zal consumeren. Het niet gebruiken van een grafische gebruikersomgeving draagt in die zin bij tot het beperken van het gebruik van systeemmiddelen tot een minimum. Ten slotte is het zo dat een Java-programmaatje steeds in de DOS-prompt of in een shell (afhankelijk van het platform waarop gewerkt wordt) zal werken, zodat toch een minimum aan feedback naar de gebruiker kan plaatsvinden.

In de paragraafjes die hierop volgen zal ik op enkele deelaspecten van het programma ingaan.

3.4.1 Het implementeren van een timer

In Java is voor het implementeren van een timer een speciale klasse Timer aanwezig. Deze klasse laat gebruikers toe het opstarten van een draad (thread) op een welbepaald ogenblik te laten plaatsvinden (zoals bijvoorbeeld vijf minuten na het opstarten van het hoofdprogramma, of op 5 juli, om 3 minuten over twaalf "s middags). De klasse voorziet daarenboven methodes die het toelaten cyclisch, met een vooraf vast te stellen interval, nieuwe gelijke draden aan te maken. Deze klasse lijkt in wezen ideaal voor de te ontwerpen applicatie. Al gauw echter werden we geconfronteerd met een probleem. Het is namelijk zo dat er geen enkele mogelijkheid is voorzien om het aanmaken van een nieuwe draad af te laten hangen van het al dan niet beëindigd zijn van de vorige draad. Dit is evenwel een eis: de CBM kan immers geen twee opdrachten tegelijk uitvoeren, en op het moment van het ontwerpen van de website was er nog geen waterdichte methode om te achterhalen of de CBM al dan niet bezig was met het uitvoeren van een opdracht.

Er werd daarom uitgeweken naar een veel eenvoudiger, en tegelijk robuuster vorm van timer. De code ziet er, ontdaan van zijn franjes, ongeveer uit als volgt:

while (true) {
   try {
        Thread.sleep(bedTime);
        if (isNotRunning) {
           timeBefore = System.currentTimeMillis();
           isNotRunning = false;
           CBMTask Lets = new CBMTask();
           Lets.go();
           loopCounter++;
           isNotRunning = true;
           timeAfter = System.currentTimeMillis();
           timeElapsed = ((int) (timeAfter - timeBefore));
           averageKeeper = averageKeeper + timeElapsed;
           bedTime = milliTime-timeElapsed;
           while (bedTime <= 0) {
                 bedTime = bedTime + milliTime;
           }
           System.out.println("**********************************************************");
           System.out.println("** The above cycle took "+timeElapsed+" milliseconds.");
           System.out.println("** Next cycle scheduled in "+(bedTime/1000)+" seconds.");
           System.out.println("** "+loopCounter+" cycles have completed so far.");
           System.out.println("** [...]"+(averageKeeper/loopCounter)+" milliseconds.");
           System.out.println("**********************************************************");
           }
   } catch (InterruptedException ie) {
           System.err.println("** FEEDBACK ** InterruptedException in method main().");
   }
}

Figuur 18: Implementatie van een timer

Meteen valt op dat een lus geïnitialiseerd wordt met een voorwaarde die altijd waar is. Het gaat dus om een oneindige lus. Dit is misschien niet de elegantste oplossing. Zeker niet als men beseft dat het programma slechts gestopt kan worden door een kill-signaal te sturen naar het drijvende proces (veelal Ctrl+c in command line interfaces). Een eleganter optie ware het controleren of de gebruiker al dan niet een bepaalde toets indrukt (zoals ESCAPE). Dit is echter moeilijk te implementeren, aangezien de methodes die in dat verband voorhanden zijn in de klassen java.io.* en javagently het verloop van de draad die de methode opriep bevriezen tot er invoer komt. Een oplossing om het programma toch via gebruikersinvoer te laten stoppen bestaat erin een aparte draad op te starten die voortdurend op gebruikersinvoer wacht. Van zodra er dan invoer komt kan de draad bijvoorbeeld een logische variable op vals zetten. Indien die logische variabele tegelijk als voorwaarde wordt genomen van de lus zal de lus dan, na het be-eindigen van de begonnen iteratie, niet meer opgestart worden en zal het programma gracieus stoppen (dit is, na het beëindigen van de taken). Hoewel de code voor dit alles van een betrekkelijke eenvoud is werd beslist om dit voorlopig niet te implementeren, aangezien het opstarten van een draad die op gebruikersinvoer wacht ongeveer 99,9% van de actieve tijd nutteloos systeemmiddelen consumeert.

Figuur 19: Iteraties in CBMTimer

Binnen de while lus wordt een sleep commando gegeven aan de lopende draad. Het is duidelijk dat de combinatie van deze twee elementen, een lus en een methode om een draad een vastgestelde periode stil te leggen, voldoende zijn om een oneindige lus te beginnen die om de x milliseconden een bepaalde taak uitvoert. Er is echter meer voorzien. Om er voor te zorgen dat de reeks taken altijd opgestart wordt op een veelvoud van de ingegeven periode, werd een primitieve manier van tijdsmeting geïmplementeerd. Dit kan eenvoudig gebeuren door de methode currentTimeMillis() van de klasse System. Deze methode levert een variabele van het type long met daarin het aantal milliseconden sinds 1 januari 1970, middernacht. Door vlak voor de aanvang en vlak na het einde van de reeks taken een variabele te initialiseren op currentTimeMillis() en die twee waarden van elkaar af te trekken, krijgen we het aantal milliseconden dat besteedt werd aan de oproep. Dat aantal wordt dan afgetrokken van de tijd die in sleep wordt doorgebracht. Op die manier is het programma écht cyclisch: het zal immers alleen op veelvouden van de opgegeven periode itereren, en dat ook in het geval dat een taak langer duurt dan de vooraf ingestelde iteratietijd. In dat geval is het verschil tussen de iteratietijd en de duur van de opdracht negatief, en worden in een lus extra iteratietijden opgeteld bij de sleep tijd tot die tijd positief is. toont hoe de iteraties van een lus steeds opgestart worden op veelvouden van het gekozen interval, terwijl de lus toch wacht op taken in uitvoering. Zo is te zien hoe op t3 het aanmaken van een nieuwe draad wordt afgeblazen om de draad, die op t2 werd aangemaakt, te laten beëindigen.

3.4.2 Het opstarten van de taak

De ubiquitaire term taak duidt op het object dat vanuit de zopas beschreven timer lus wordt aangemaakt. Om de verschillende taken, die het programma CBMTimer moet uitvoeren, te organiseren werd een klasse CBMTask geschreven. Binnen de lus in CBMTimer wordt van deze externe klasse een object aangemaakt, waarna op dat object de niet-statische methode go() wordt geïnvoceerd. Het eerste wat deze methode doet is een connectie maken met de databank cambrainDB, waarin het reeds beschreven script "Process.html" gegevens omtrent ontvangen verzoeken wegschrijft. De connectie met de databank wordt dan gebruikt om eerst datum en tijd op te vragen. Dit is in feite niet strikt nodig, maar heeft als aardige eigenschap onder andere het feit dat een positief antwoord naderhand enige oriëntatie kan bieden indien men zou willen bepalen welke opdrachten wanneer behandeld werden (dit is bijvoorbeeld handig als de periode tussen twee iteraties bijzonder lang is: de gebruiker kan meteen zien wanneer de laatste iteratie plaatsvond). Daarnaast is het zo dat een positief antwoord meteen ook aantoont dat de connectie met de databank met succes is opgezet. Dit bleek vooral handig tijdens het ontwikkelen van de applicatie, toen de enigszins enigmatische SQLException en de soms weinigzeggende methode getMessage() (om foutboodschappen op te halen) er niet in slaagden duidelijk te omschrijven wat precies fout was met de databank.

Na de query SELECT now(); en het uitschrijven van het resultaat ervan naar het scherm, volgt de query SELECT * FROM request_table WHERE status="unanswered"; Het resultaat van de query is een variabele van het type resultset waarin alle records zitten waarvan het statusveld gelijk is aan unanswered. Op basis van die resultset wordt een lus opgestart: while (rs.next()) {...} De opdracht rs.next() zorgt ervoor dat de interne pointer binnen de resultset één record vooruit wordt geplaatst (of vóór het eerste record komt te staan bij de eerste opdracht). Indien het laatste record reeds bereikt werd, levert de opdracht de logische waarde vals, zodat de while lus opgeschort wordt. Het is dus zo dat de lus één keer doorlopen wordt per onbeantwoord verzoek. In de volgende paragraaf wordt uitgelegd wat precies in de lus gebeurd.

3.4.3 Behandeling van onbeantwoorde verzoeken

Specifiek voor het behandelen van individuele verzoeken zijn een aantal methodes geschreven die opgeroepen worden vanuit de methode go() van de klasse CBMTask. Een eerste vitale methode is fileCopy(). Om deze methode op te kunnen roepen wordt eerst het request_id veld van het huidig record in de resultset uitgelezen. Op basis daarvan wordt een URL-variabele geïnitialiseerd. Het is immers zo dat PHP de upgeloade bestanden herbenoemt volgens het sjabloon "Req_" # "."[gnt | ntl | spt], waarbij het nummer (#) gelijk is aan de primaire sleutel van het overeenkomstige record in de databank. Het pad naar het bestand is onveranderd "elis.rug.ac.be/~mjdwilde/Uploads/[naam]". Het protocol dat hierbij gebruikt wordt om de bestanden op te halen is HTTP (HyperText Transfer Protocol).

Op basis dan van de URL"s (drie per verzoek) wordt driemaal de methode fileCopy opgeroepen. Deze methode kopieert de bestanden vanaf de server trappist.elis.rug.ac.be naar cambrain.elis.rug.ac.be, vanwaar ze aan de CBM kunnen aangeboden worden.

Indien het kopiëren succesvol verloopt (en de methode fileCopy een logische waarde waar teruggeeft) wordt aan het programma "Brain_Mfc" het commando gegeven de vers gekopieerde bestanden te verwerken. Als programma-argumenten worden de namen en relatieve paden van de te verwerken bestanden opgegeven, evenals de gewenste naam van het uitvoerbestand. Het programma "Brain_Mfc" is geschreven door mijn thesisbegeleider. Het programma zorgt voor de directe aansturing van de CBM aan de hand van drie bestanden. Er wordt gewacht tot het programma "Brain_Mfc" de taak beëindigd. Bij het geven van het commando aan het programma "Brain_Mfc" wordt een object van de Java-eigen klasse Process aangemaakt. Dit soort object houdt een aantal parameters bij omtrent het gecreëerde proces. Men kan die parameters aanwenden om te wachten tot het programma afloopt, en wel met name via de methode waitFor(). Het gebruik van de methode waitFor() heeft evenwel als neveneffect het feit dat het programma zal blijven hangen indien "Brain_Mfc" zou blokkeren. Indien men natuurlijk bedenkt dat sowieso geen opdrachten uitgevoerd kunnen worden zolang "Brain_Mfc" niet oproepbaar is, wordt ook duidelijk dat dát in feite geen tekortkoming is van de methode.

Na het opstarten en wachten op de beëindiging van het proces wordt de functie errorCodeTest() opgeroepen. Deze functie opent het tekstbestandje "BrainRunLog.txt", dat door "Brain_Mfc" wordt overschreven bij elke uitgevoerde opdracht. Het eerste karakter wordt uitgelezen en gecontroleerd op basis van een vooraf vastgelegde set statuscodes. Deze codes zijn:

0 - De bestanden werden zonder probleem verwerkt. Er werd een uitvoerbestand gegenereerd.

1 - Er was een probleem met de CAM-Brain. De bestanden mogen later opnieuw aangeboden worden. (vb. de CAM-Brain zit zonder stroom)

2 - Er was een probleem met de inhoud van één of meerdere bestanden, en de aanvraag kon niet behandeld worden. (vb. een bestand heeft een foute syntax)

3 - Er deed zich een ander probleem voor en de aanvraag kon niet behandeld worden. Deze code is opgenomen als vangnet voor onbekende en onverwachte fouten.

Op basis van de uitvoercodes wordt dan beslist of er al dan niet nog een vervolg moet komen aan de behandeling van de specifieke aanvraag. In het geval de code 1 is wordt de huidige iteratie van de lus zonder meer beëindigd. De bestanden zullen dan vanzelf in een volgende lus opnieuw aan bod komen. In de andere gevallen wordt de methode lastFeedBack() ingeroepen op de bestanden. De methode doet haar naam eer aan: er wordt gezorgd voor feedback aan de gebruiker, en tegelijk wordt ervoor gezorgd dat het de laatste keer is dat de bestanden behandeld werden. Dat laatste gebeurt door een update van de databank, alwaar het statusveld, afhankelijk van het eindresultaat, op answered, error2 of error3 wordt geplaatst.

De feedback aan de gebruiker wordt verzorgd door een licht herschreven versie van de klasses MIMEMail en MIMEBase64, geschreven door Real Gagnon [17]. De klassen laten toe een MIME e-mail te sturen. MIME staat voor Multi-purpose Internet Mail Extension, en wordt vooral gebruikt om niet-tekstuele informatie als "attachment" in een e-mail op te nemen. De functie lastFeedBack() maakt eerst een string variabele aan, met daarin de boodschap die als body van de e-mail zal verstuurd worden. Indien de uitvoercode in "BrainRunLog.txt" 0 was, wordt het door de CAM-Brain uitgevoerde bestand gecomprimeerd tot ZIP-bestand (met de daarvoor voorziene methode zipCompress van de klasse CBMTask). Het pad naar het ZIP-bestand wordt dan doorgegeven naar de klasse MIMEMail tijdens het aanmaken van een object van die klasse.

In de gevallen waarin zich een onregelmatigheid voordeed, en de gevonden statuscode niet toelaat de taak te bewaren om die later nogmaals uit te voeren, wordt geen attachment meegestuurd. Daarnaast wordt als boodschap een specifieke omschrijving van de opgetreden fout meegestuurd, althans in zoverre die door het programma te bepalen valt. Er werd overigens voor gezorgd dat bestanden die leiden tot een foutmelding worden vernietigd. Dit gebeurt door voor elk van de bestanden een object van de klasse File aan te maken (File genotypeFile = new File( "./Webfiles/Req_"+request+".gnt");), en er daarna de methode delete() op te invoceren.

3.4.4 Behandeling van fouten

Het is zo goed als ondoenbaar om in een programma, hoe klein ook, alle fouten te voorzien en ze in rekening te brengen. Dat is in het geval van het programma CBMTimer des te meer zo aangezien gewerkt wordt met aan de ene kant een erg wisselbare invoer van velerlei gebruikers op verschillende platformen op het WereldWijde Web, en aan de andere kant een andere applicatie met eigen specifieke eisen.

Toch werd één en ander gedaan om te vermijden dat het programma al te veel aan fouten onderhevig zou zijn. Het kernwoord in dit alles is voorzienigheid. Ik heb geprobeerd zoveel mogelijk mogelijke fouten voorzien. Trachten achterhalen in welke mate nu het programma al dan niet foutbestendig is, is koffiedik kijken. Eén ding dat ik hier in dit paragraafje echter nog wil vermelden, is een structuur die veelvuldig in het programma gebruikt wordt. Het gaat over de try {...} catch {...} blokken om zogenaamde Exceptions op te vangen. Het systeem bestaat erin om invocaties van bepaalde methodes die een Exception werpen ("throw") op te vangen en gepast te behandelen. Om dit duidelijker te maken kunnen we bijvoorbeeld denken aan de constructor File(String bestandsnaam) van de klasse File. Een uitzonderingsgeval, waarin de constructor haar werk niet naar behoren zal kunnen uitvoeren, ontstaat in dat geval waarin het bestand, aangewezen door bestandsnaam niet bestaat of gevonden wordt: er wordt een IOException (Input Output Exception) "geworpen". Om er nu voor te zorgen dat het programma niet tot een abrupt einde komt omdat de taak niet uitgevoerd kon worden, wordt gebruik gemaakt van de try {...} catch {...} syntax. Er wordt geprobeerd ("try") een methode uit te voeren die tot een bepaalde Exception zou kunnen leiden. Tegelijk wordt een actie voorzien die ondernomen moet worden als het uitzonderingsgeval zich zou voordoen. Een voorbeeld in CBMTimer ziet eruit als volgt, waarbij bijvoorbeeld in het geval zich een IOException voordoet een boodschap naar het scherm wordt geschreven, waarna het programma de uitvoering herneemt:

try {
    fileDataCharacter = fileDataReader.read();
    if (fileDataCharacter != -1) {
        toReceivingFile.write(fileDataCharacter);
    } else {
        moreData = false;
        toReceivingFile.close();
    }
} catch (EOFException eoe) {
    moreData = false;
} catch (IOException e) {
    System.out.println("** FEEDBACK ** IOException: "+e.getMessage());
}

Figuur 20: Een try {...} catch {...} blok in de klasse CBMTask

3.5 De databank "cambrainDB"

In het voorgaande is de term databank meer dan eens gevallen. Het doet de waarheid weinig geweld aan indien we stellen dat de gebruikte MySQL databank een ruggengraatfunctie vervult in het geheel van website en achterliggende verwerkingsapplicatie. De twee componenten die ervoor zorgen dat de buitenwereld kan experimenteren met de CAM-Brain, het PHP script "Process.html" en het Java programma "CBMTimer", voeren beiden meerdere queries uit op de databank "cambrainDB". De twee uitvoerbare bestanden zijn niet direct verbonden, of nog, ze communiceren niet direct. Dit is een ontwerpkeuze die verschillende achterliggende redenen heeft. Bovenal is er het feit dat onafhankelijkheid de werking van de webapplicatie ten goede komt. Zolang immers het PHP script "Process.html" werkt kunnen gebruikers hun bestanden uploaden, ook als het programma "CBMTimer", of de computer waarop het draait, cambrain.elis.rug.ac.be, niet werkt. Daarnaast is het ook zo dat het falen of bevriezen van één van beide uitvoerbare bestanden het andere niet zal beïnvloeden (hoewel PHP scripts in feite zijn beveiligd tegen bevriezen, in die zin dat in de configuratiebestanden een maximum uitvoeringsperiode bepaald wordt).

Een andere vraag die kan rijzen is die naar het waarom van MySQL. De reden dat gewerkt werd met MySQL is, toegegeven, licht arbitrair. Enerzijds wijs ik graag op het Open Source / General Public License karakter van de technologie, wat zorgt voor de afwezigheid van licentieproblemen en vaak (doch niet altijd) voor het bestaan van een ruime kern vaardige aanhangers. In het geval van MySQL is dat wel degelijk zo, met als gevolg een ruim aanbod aan informatie en fora op het internet.

De databank "cambrainDB" bestaat uit twee tafels, request_table en user_table. Conceptueel is de databank gecentreerd rond één entiteit, het verzoek of request. Deze entiteit heeft verschillende attributen, waaronder de primaire sleutel request_id en het samengesteld attribuut gebruiker of user. Het zogenaamde Entiteit-Relatie diagram ziet er, uitgetekend, uit als in . Men zou zich kunnen afvragen waarom user een samengesteld attribuut is, eerder dan een entiteit. Die keuze heeft vooral te maken met het feit dat de databank haar bestaansrecht put uit de verzoeken die het krijgt. Het bijhouden van gebruikersgegevens is daaraan secundair. Daarnaast is het zo dat bij het aanmaken van de databank, vanaf het ER diagram, er uiteindelijk geen groot verschil is tussen de beide keuzes. Zowel een zelfstandige entiteit als een samengesteld attribuut monden immers uit in een eigen relatie of tafel.

De specifieke structuur van de beide tafels blijkt dan uit figuur 21. Deze figuur toont de specifieke CREATE TABLE statements die aanleiding gaven tot de tweetafelige databank.

Figuur 21: ER Diagram van de databank cambrainDB

De tafel request_table is opgetrokken uit tien kolommen. De tafel user_table (zoals vermeld bewaart deze tafel e-mailadres/wachtwoord paren) heeft vier kolommen. Enige opmerkingen zijn uiteraard op zijn plaats.

CREATE TABLE request_table (
      request_id mediumint(24) NOT NULL auto_increment,
      e_mail tinytext NOT NULL,
      gnt_file tinytext NOT NULL,
      ntl_file tinytext NOT NULL,
      spt_file tinytext NOT NULL,
      project_title tinytext NOT NULL,
      IP_address tinytext NOT NULL,
      IP_name tinytext NOT NULL,
      status enum ('answered', 'unanswered', 'error2', 'error3') default 'unanswered',
      request_timestamp timestamp(14) NOT NULL,
      PRIMARY KEY (request_id)
      ) type = myISAM;

CREATE TABLE user_table (
      user_id mediumint(24) NOT NULL auto_increment,
      e_mail tinytext NOT NULL,
      password tinytext NOT NULL,
      register_timestamp timestamp(14) NOT NULL,
      PRIMARY KEY (user_id)
      ) type = myISAM;

Figuur 22: Create Table statements

De keuze voor een mediumint (24 bits of 16777216 ongetekend) als primaire sleutel in beide tafels is bijvoorbeeld aanvechtbaar. Genoeg ware waarschijnlijk een smallint (16 bits of 65536 ongetekend). Ik vermoed dat het waarschijnlijk hybris is van mijn kant, een gooi naar de eeuwigheid. De databank is er alleszins op voorbereid. Het is overigens ook zo dat dit slechts zorgt voor 1 extra byte informatie, waarmee de primaire sleutel 3 bytes in plaats van 2 zal innemen. De verschillende velden die tekst bevatten kunnen echter tot 255 bytes opslagruimte bevatten. Het meest voorkomende datatype is immers tinytext. De opslagruimte benodigd door het type is afhankelijk van de lengte van de string die erin opgeslagen wordt, maar kan, zoals vermeld, niet groter zijn dan (2^8-1), zijnde 255 bytes of karakters. Voor kortere strings wordt de volgende berekening gemaakt: L+1 bytes, met L de lengte van de string (maximaal 2^8 dus) en een extra byte om de lengte van de string aan te geven.

Voor het status veld in request_table werd gebruik gemaakt van het type enum, een type variabele dat in feite expliciet gebruik maakt van het uit de databank-theorie bekende domeinen. De mogelijke waarde van het attribuut status in de records van request_table is beperkt tot de elementen van het domein status, zijnde answered, error2, error3 en unanswered. Concreet zorgt dit voor een grote besparing van opslagruimte daar de mogelijke waarden van een veld a priori vastgelegd worden (ze worden weggeschreven in de databank cataloog), en nadien via enkele bits een waarde vast te leggen. Het veld status in request_table moet niet expliciet geïnitialiseerd worden bij het updaten van de databank: het zal standaard de waarde unanswered aannemen.

Als laatste opmerking wil ik aan deze paragraaf nog toevoegen dat er gedacht is aan de mogelijkheid de opgestuurde bestanden volledig in de databank in te lezen. Er werd daar echter van af gezien. Het opslaan van volledige bestanden in de databank zorgt er immers voor dat de databank bijzonder snel zal aangroeien. De situatie zoals ze nu is laat overigens fijner afstellen toe: specifieke bestanden kunnen al dan niet door de bediener van de CAM-Brain, een PHP script of het programma CBMTimer bewaard of uitgeveegd worden.