Table of Contents
Previous Chapter
Jag vill påpeka att Spinner från och med den 9:e september 1996 heter Roxen och det är ett registrerat varumärke hos Roxen INC. Även vissa funktioner, språk och filer som nämns i den här artikeln har bytt namn. spinnerlib -> roxenlib, SPML -> RXML m.fl. Om du kör en Roxen-server måste du alltså ta beskrivningarna med en nypa salt.
Spinner är den WWW-server som Lysator använder. Det är också en WWW-server som är synnerligen utbyggbar. Servern är mestadels skriven i uLPC, se artikeln om uLPC i denna tidning.
Spinner i sig kan inte mycket, det enda det centrala programmet kan är modulhantering, det vet ingenting om filer, HTTP eller andra udda saker. Allt sådant hanteras av moduler.
När jag beskriver modulskrivande kommer jag att utgå från att du har grundläggande kunskaper i uLPC, så om du inte har det är det lämpligt att läsa lite om uLPC innan du skriver din modul.
De tre vanligaste modultyperna är helt klart location moduler, file extension moduler och parser moduler.
/cgi-bin/
. Det är också den modultyp som (oftast) hittar de filer som sedan skickas vidare till extension moduler.
Sedan finns det en hög med modultyper som inte är så vanliga att jag kommer att beskriva dem ingående här.
Varje modul kan ha en eller flera olika modultyper (i teorin kan man faktiskt ha moduler helt utan typ, men det blir lite meningslöst, eftersom en sådan modul aldrig kommer att anropas av spinner.)
I bilden nedan är 'mapping' 'object' och '-1' returtypen (och i fallet -1 även värdet) från respektive modul.
Skiss över skickandet mellan moduler
Mitt första tips är att man konfigurerar upp sin egen testserver. Om man vill använda samma filer som lysators huvudserver använder, förutom konfigurationsfilerna, skriver man:
cd /usr/www/spinner/server ./start --config-dir=$HOME/.spinner_cfg
Detta kommer att starta din aldeles egna spinner, sedan är det bara att koppla upp sig till dess konfigurationsinterface och konfigurera den. Mer om det finns att läsa på URLen
http://spinner.infovav.se/
.
Lämplig variabel att ändra på är Global Variables -> Module directory, som anger var den ska leta efter moduler, Detta är en kommaseparerad lista av directoryn. Ett lämpligt värde kan ju vara modules/, localmodules/, /users/<dittnamn>/spinner_modules/ eller liknande.
Nu kan du börja skriva din egen modul, nedan följer en sorts steg-för-steg beskrivning av hur man gör det. Jag utgår som sagt från att du har grundläggande kunskaper i uLPC programmering.
Vi börjar med ett modulskelett, de funktioner och ärvningar som alla moduler måste innehålla.
#include <module.h> inherit "module"; inherit "spinnerlib"; array register_module() { return ({ MODULTYP, "modulnamn", "moduldokumentation", 0, 0 eller 1, }); }
Först inkluderas filen 'module.h
'. Den innehåller en massa defines för konstanter, t.ex. alla modultyper. Den innehåller även en del makron som kan användas när man vill ha värdet på en viss variabel.
Sedan ärvs filen module
. Den definierar en massa funktioner av typen defvar()
, set()
och query()
, samt tillhandahåller skönsvärden till många olika statusfunktiner som man kan implementera i sin modul om man vill, men som inte behövs i de allra flesta fall.
Filen spinnerlib
som sedan ärvs inkluderar en hel massa bekvämlighetsfunktioner, och är alltså inte helt nödvändig, men det är mycket bra att ha med den. Du kan ju titta på avsnittet om 'Att returnera något' för att se varför.
Sedan kommer någonting som är intressantare, funktionen register_module.
Denna (något ointuitiva) funktion ska returnera en array som definierar modulens funktion, samt vad den heter, och om den kan ha mer än en samtidig instans aktiv i varje server.
Det första elementet i arrayen är modultypen. Detta är en bitvis-eller (|) av modultyper. Vanligast är att varje modul bara har en typ, men t.ex. htmlparse.lpc
i normaldistributionen av Spinner är en MODULE_MAIN_PARSER
, en MODULE_PARSER
och en MODULE_FILE_EXTENSION
.
MODULE_AUTH
- Directoryparsningsmodul, kan bara finnas en aktiv i taget
MODULE_DIRECTORIES
- Extension modul (före location)
MODULE_EXTENSION
- Extension modul (efter location)
MODULE_FILE_EXTENSION
- "First try" moduler
MODULE_FIRST
- "Last resort" moduler
MODULE_LAST
- Location modul, tillhandahåller filer
MODULE_LOCATION
- Parser, hanterar en eller flera "tags"
MODULE_PARSER
- URL modul, kan göra interna omriktingar av förfrågningar
MODULE_URL
Det andra elementet är modulens namn, det tredje en (hyffsat) kortfattad beskrivning av vad modulen gör, det fjärde elementet är reserverat för framtida användning och det femte elementet är '0' om det kan finnas mer än en kopia av modulen samtidigt i varje virtuell server (som filsystemsmodulen), eller 1 om det bara kan finnas en kopia i varje virtuell server (som 'tablify' modulen).
Eftersom vi i detta exempel vill skriva en modul som adderar ett par 'taggar' till SPML, väljer vi nedanstående utseende på register_module
funktionen:
array register_module() { return ({ MODULE_PARSER, "Gazonk", "The gazonk module. " "This module adds a container and a non-container: " "<foo></foo> and <bar>", 0, 1, }); }
Nu kan modulen registrera sig, men det räcker ju inte riktigt, hur får man den att verkligen hantera <foo> </foo>
och <bar>
?
Nu kommer vi till modultypsspecifika callbacksfunktioner (kom på ett bättre namn själv!).
-------------------------------------------------------------------------------------------------- Funktion Modult Förklaring yper --------------------------------------------------------------------------------------------------void create();
Alla Anropas av uLPC automatisk när objektet (instansen av din modul) skapas. Här definierar man lämpligtvis sina modullokala Spinner variabler med hjälp av 'defvar
'.void start();
Alla Anropas av spinner precis innan modulen ska vara redo att ta emot requests (requests hanteras med de modultypsspecifika call backfunktionerna).string status();
Alla Anropas av konfigurationsinterfacet i tid och otid för att få status om modulen. Ska returnera en sträng, beståendes av HTML som tål att stoppas i en<dd>
i en<dl>
lista.string info();
Alla Om man defenierar den här funktionen används resultatet istället för element tre i det somregister_module()
returnerar för att beskriva modulen i konfigurationsinterfacet.string|void Alla Om man behöver kontrollera värdet hos variabler innan de sätts check_variable(str defenitivt så gör man lämpligtvis det i den här funktionen. Oftast ing variable, behöver man ju dock inte kontrollera variablers giltighet, efter mixed som alla värden som man kan sätta dem till är giltiga. Mer infor will_be_set_to);
mation om spinnervariabler i allmänhet kommer senare.string Alla Ska returnera namnet på modulen, används istället för element query_name();
två i arrayen somregister_module()
returnerar för att beskriva modulen i konfigurationsinterfacet. Kan vara mycket användbart om man kan ha mer än en kopia av en modul. Använ daren kan dock själv sätta vilket namn som helst på modulen i konfigurationsinterfacet.array auth(array Auth Se userdb.lpc modulen. Oftast behöver man inte implementera en from); egen användardatabas och authentifieringsmodul, och det hör array inte till grunderna i spinnerprogrammering. userinfo(string username); array userlist(); array user_from_uid(int uid);
mapping Direc Givet en intern url (i
request_id->not_query
) ska den här parse_directory(ob tories funktionen returnera en directorylistning i HTML. ject request_id);array (string) Exten Returnera en array med extensions som modulen tänker hantera. query_extensions() sion Lämpligtvis implementeras enligt följande: ;
(förutsatt att variabeln extensions är definierad, som enarray (string) query_extensions() array (string) { query_file_extensi return query("extensions"); ons();
}TYPE_STRING_LIST
, se avsnittet om variabler senare).mapping Exten Returnera antingen ett resultat eller 0 (noll). Anropas av Spinner handle_extension(s sion när modulen ska hantera en extension, i fallet tring extension,
uLPC dokumentationen. Kortfattat kan man nämna att 'file' har metodenhandle_extension
innan en location modul har hittat en fil object som passar (används knappast alls, den enda modulen som finns request_id); som använder det är explicit timestamp modulen, som returnerar mapping last modification date (mtime) på en fil, om man lägger till handle_file_extens .timestamp till filnamnet.) ion(object file, Vanligtvis gör man file_extension moduler, då är det det senare string ext, object fallet som gäller. 'file' är en klon av/precompiled/file
, se request_id);read()
, kan vara lämpligt i det här fallet.mapping First Används bland annat av logger.lpc, den där modulen som loggar first_try(object requests i användarnas egna directoryn på Lysator. Om man request_id);
returnerar en response mapping så kommer det att tas som svaret. Den här funktionen anropas aldrig vid interna requests, dvs, vid<insert>
och liknande.mapping Last Används av relay modulen, om man sätter priority till last. last_resort(object Anropas bara om ingen annan modul har lyckats hitta ett match request_id);
ande dokument, och aldrig vid interna requests.string Loca Returnera vilken position i det virtuella filsystemet modulen ska query_location();
tion få. Även här använder man lämpligen värdet av en variabel, eftersom användaren då själv kan välja var modulen ska hamna.object|mapping Loca Returnera ett öppet filobjekt (see uLPC dokumentation om /pre find_file(string tion compiled/file) om det finns en fil som matchar file_name som den file_name, object här modulen hanterar, eller en response mapping (lämpligen request_id);
genererad medhttp_*
funktionerana) om man inte vill att extension moduler ska användas igenom. Mappingen kan vara mycket praktisk om man vill returnera att användaren ska skriva in ett lösenord, eller en redirect till en ny fil. En bra funktion att använda i den här funktionen kan varaopen(filename, mode)
, som returnerar just ett sådant öppet filobjekt. Observera att om modulen har location (query_location
returnerar) "/foo/
", och filen "/foo/bar/
" efterfrågas, kom mer location modulen att få "bar
/" som första argument.request_id
finns dokumenterad i en annan artikel i denna tid ning, den om skript i spinner, liksom response mapping.array Loca Returnera en array med filnamn eller noll.
0. Så om man inte vill returnera några directorylistningar kan man bara låta bli att defeniera den här funktionen.dir_name
som find_dir(string tionfile_name
hosfind_file
. dir_name, object Det finns en defaultfind_dir
imodule.lpc
som returnerar request_id);array Loca
Om man vill kan man skippa även den här funktionen, directory modulen och en del andra moduler kan dock kräva att den finns för att din modul ska fungera precis som ett "vanligt" filsystem.file_name
som hosfind_file
. stat_file(string tion Returnera resultatet av enfile_stat
(se uLPCdokumentatio file_name, object nen) eller noll. request_id);string Loca Returnera vad filen
Den här metoden är inte alls nödvändig, det kan ju t.ex. hända att dina filer egentligen inte existerar på disken, men den snabbar upp saker som cgimoduler och uLPC skript.file_name
egentligen heter (t.ex om real_file(string tion modulen sune hittar sina filer i/usr/www/foo/
, och filenbar
file_name, object efterfrågas i den, så ska den returnera/usr/www/foo/bar
). request_id);mapping Parser Anropas från main_parsern för att registrera en parse modul. query_tag_callers( Returvärdet är en mapping enligt:
([ "tag":funktion, ); ... ])
, eller en tom mapping([])
. mapping query_container_ca llers();object|mapping URL Returnerar antingen
för att göra en ny request (vilket innebär att modulen kommer att anropas igen, se upp!), eller också så kan funktionen returnera en mapping med någon avrequest_id
(med iallafall en eller annan remap_url(object variabel modifierad, förhoppningsvis), i vilket fall den används request_id);http_*
funktionerna (se nedan). Slutligen så kan man returnera noll, i vilket fall allt bara fortsät ter. --------------------------------------------------------------------------------------------------
Vi kan se i tabellen att en dylik (MODULE_PARSER
, om ni har glömt bort vilken typ det skulle vara) modul ska ha initieringsfunktionerna query_tag_callers
och query_container_callers
, men vad ska de egentligen returnera?
Funktionerna ska returnera en mapping av tag-namn till funktionspekare. Den första funktionen (query_tag_callers
) ska returnera en mapping med funktioner som ska anropas om tagen står ensam (utan avslutande </
tag-namn
>
), och den andra funktionen ska returnera en mapping med funktioner som ska anropas för containers (<
tag-namn
></
tag-namn
>
). Den största skillnaden mellan dem är att container-funktionerna även får med texten inne i tagen som ett argument.
Argumenten till funktionerna är:
Containers:
string tag_handler( string tag_name, mapping tag_arguments, string contents, object request_id, object file, mapping defines);
tag_arguments
är en mapping med argument, <
tag-name
foo=bar>
innehär att arguments->foo
blir "bar"
.
contents
är all data inne i tagen.
request_id
är, precis som vanligt, just requesten.
file
är en pekare till den (öppna) filen som används som "råmaterial" eller noll.
Tags:
Alla argument är som för containers, förutom det att det inte finns några contents.
Hela modulen:
/* A simple parse type module for spinner. This module defines two new tags, foo and bar, one is a container and the other one is a stand alone tag. */ #include <module.h> inherit "module"; inherit "spinnerlib"; array register_module() { return ({ MODULE_PARSER,"Gazonk","The gazonk module. " "This module adds a container and a non-container: " "<foo></foo> and <bar>", 0, 1,}); } /* A container gets the contents as the third argument. Example: Hi! --> tag_foo("foo","Hi!",(["bar":"gazonk"]),...); */ string tag_foo( string tag_name, string contents, mapping arguments, object request_id, object file, mapping defines) { if(arguments->lower) return lower_case(contents); if(arguments->upper) return upper_case(contents); if(arguments->reverse) return reverse(contents); } string tag_bar( string tag_name, mapping arguments, object request_id, object file, mapping defines) { int i; string res=""; if(arguments->num) i=(int)arguments->num; else i=30; #define SUNE (("abcdefghijklmnopqrstuvwxyzåäö")/"") while(i--) res += SUNE[random(sizeof(SUNE))]; #undef SUNE return res; } mapping query_tag_callers() { return ([ "bar":tag_bar,]); } mapping query_container_callers() { return ([ "foo":tag_foo,]); }
Nu har vi faktiskt en färdig parse modul i och för sig kanske inte så spännande, men den gör iallafall någonting!
Dokumentation om variabeln request_id
, och om hur man kan returnera data, finns i artikeln om uLPC skript.
Från alla moduler: returtypen är en mapping eller noll
Från location moduler: returtypen kan även vara ett objekt eller -1.
open(filnamn, "r")
returnerar.
Specialfallet URL-moduler: Ingen returtyp.
request_id
direkt.
Om du har tittat ett tag på spinners konfigurationsinterface så har du säkert sett att nästan alla moduler har en massa variabler som man kan sätta.
Hur gör man då om man vill ha några sådana i sin fina modul, för det är ju bra att användaren själv kan konfigurera?
Jo, man använder funktionen defvar
i create, och sedan query
om man vill veta vad en av variablerna har för värde.
int defvar( string name, mixed value, string long_name, int type, string documentation_string[, array choices]);
query("name")
, eller QUERY(name)
)
value
är skönskvärdet för variabeln.
long_name
är det namn som användaren ser i konfigurationsinterfacet.
type
är vilken typ variabeln har:
------------------------------------------------------------------------------------ typ förklaring ------------------------------------------------------------------------------------TYPE_STRING
En sträng. (t.ex."foo bar"
)TYPE_FILE
En fil. Ingen egentlig skillnad mot en sträng just nu, men ledtexten till användaren skiljer sig.TYPE_LOCATION
En position i det virtuella filsystemet. Hanteras precis somTYPE_STRING
, men ledtexten till användaren skiljer sig.TYPE_INT
Ett heltal.TYPE_DIR
Ett directoy. Användaren kan bara skriva in directoryn som finns, och de slutar alltid på '/'.TYPE_FLOAT
Ett flyttal.TYPE_TEXT_FIELD
Ett fält med text i, kan innehålla flera rader.TYPE_FLAG
Antingen sant eller falskt, dvs, man kan användaif(query("var_name"))
.TYPE_COLOR
En färg, som ett heltal mellan 0 och 2^24, bittar för R G och B, respektive. Användes i konfigurationsinterfacet en gång i tiden.TYPE_PASSWORD
Ett lösenord. Krypteras automatiskt när användaren sätter variabeln.TYPE_STRING_LIST
En array med strängar, dvs({ "foo", "bar", "gazonk", })
t.ex.TYPE_MULTIPLE_STRING
En av många strängar. Strängarna väljs ur den array man skickar med ichoices
fältet.TYPE_INT_LIST
En array med heltal.TYPE_MULTIPLE_INT
SomTYPE_MULTIPLE_STRING
, men heltal istället.TYPE_DIR_LIST
En array med directoryn.TYPE_FILE_LIST
En array med filer, hanteras somTYPE_STRING_LIST
just nu, men ledtexten till användaren skiljer sig. ------------------------------------------------------------------------------------
Ett litet exempel kanske kan hjälpa.
void create() { defvar( "BG", 1, "Configuration interface background", TYPE_FLAG, "Should the background be set by the " "configuration interface?"); defvar( "NumAccept", 1, "Number of accepts to attempt", TYPE_MULTIPLE_INT, "The maximum number of accepts to attempt for " "each read callback from the main socket. <p>" "Increasing this will make the server faster " "for users making many simultaneous " "connections to it, or if you have a very busy " "server.", ({ 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 })); defvar( "ConfigurationPort", 22202, "Configuration port", TYPE_INT, "the portnumber of the configuration " "interface. Anything will do, but you will " "have to be able to remember it to configure " "the server."); }
Den här kodsnutten defenierar tre variabler (de kommer egentligen från spinners centrala delar, och inte från en modul, men det fungerar precis lika dant).
Först en flagga, vars skönskvärde är på, sedan ett heltal som väljs från en lista av tal (2^n, 0 <= n <= 10), och sist ett heltal.
Så nu är det bara att sätta igång med din modul!
WWW-sidorna uppdateras kontinuerligt med nya punkter. Det som står närmast på tur är att scanna och presentera alla gamla GARB vi kan komma över.
Vi som jobbar med projektet är främst Kent Engström (huvudförfattare) och Magnus Bark (scanning av dokument). Lars Aronsson har hjälp till med bland annat WWW-design.
Url: http://www.lysator.liu.se/history/
Sedan november 1995 finns Lysators historia dokumenterad på WWW. Förutom en tidslinje som beskriver historien kronologiskt så finns en rad scannade dokument, t.ex. brevet som fixade Lysators första tigg (en D21-stordator från Datasaab) och brevet där ISY beklagar att man måste köra ner Lysator i underjorden till Pul-17.