I have to say, I'm really happy with my tiny gopher multithreading #'make-load-form de/serializing gopher server. Check it out in common-lisp/i2pher.lisp after reading this post, probably. (i2pher::test3) makes a gophermap/server serving two item type 0s on 127.0.0.1 port 54321 for example. I (turn out not to have) have a serious problem with lynx, which relates to a key feature that complies with rfc1436. I am using READable lisp lists (with *read-eval* locally set to nil, thank you) as my item specifiers SO THAT my items are in general date ordered feeds of textual orgmode compatible (or markdown, basically) text documents. I purport that this conforms to rfc1436 (and it works just like I wanted). ```totally erroneous, I forgot my external gopher port was different. BUT As you may know by hitting backslash, lynx clumsily converts plain text to html format. And in the case of (:item-specifier) you end up with something like %28%aitem-specifier%29 Bizarre get-request-eze aside, this clashes with a compatibility feature I added preemptively. ``` Traditionally on some gopher browsers, such as lynx, unrelated to rfc1436, requests are arbitrarily prefixed with their item-type character, which is discarded. Now, using (let ((*read-eval* nil)) (read stream)) is important to me. Lisp has some dispensation - the poorly named #'peek-char, (peek-char #\( stream) #|) [closing that bracket in a block comment]|# which eats chars until it reads a #\(, which it then unreads. BUT lynx's gophermap no longer has any #\(s in it. It just has %28 etc. So peek-char will always discard all the characters that lynx is erroneously passing as an item- specifier [or so I quickly and wrongly assumed]. So my granfalloon (slash just netcat) works (on (i2pher::test3)) ; printf "(:map)\n" | nc localhost 54321 Oh, hang on, am I making a dumb mistake. Oh yeah I was. In my debugging I was using a local port different to the advertised port (for a level of indirection / especially with i2pd). Lynx in fact understands its htmleze %28 as meaning to send a #\(. Disregard me. ALL IS WELL AND GOOD THEN with the following: In one term: (using rlwrap, which could be left out) rlwrap ecl --load i2pher.lisp --eval '(i2pher::test3)' and in another term: lynx gopher://localhost:54321/1\(:map\) (to get to the gophermap(s) specified by including the keyword :map) And lynx's browsing works normally. Note the escaped brackets etc in ksh. Now I know there are lots of small gopher servers, many of which did their connect(2)s themselves rather than farming them out to openbsd netcat, even though they probably have less dab multiprocessing. (The kyoto lisp tradition calls threads processes, clashing with the unix notion, which is the process being run in the process). My real killer feature is this ; YOU DON'T NEED TO HAVE A DEEP UNDERSTANDING : ; OF THIS METHOD BUT IT IS OBVIOUSLY USEFUL ```ecl (load #p"i2pher.lisp") (in-package i2pher) ;; (setq *goma* (make-instance 'gophermap :item-specifier '(:map :toplevel) :item-description "The toplevel gophermap" :filename #p"my-gophermap.lisp")) ;;And lets add a serving process. (add-thread *goma*) ``` which we can look at with any/all of these: ```ksh ## somewhere else lynx gopher://1\(:map\) lynx 'gopher://1(:map :toplevel)' lynx gopher://F\(:toplevel\ :map\) ``` etc. Note that the item-type character is ignored (by lynx and/or me). Back in the lisp terminal: ```ecl ;;;Just cleaning up that serving process ;; I've commented this out, since it would only be necessary if you are ;; experiencing certain race conditions. ;; Oh, actually, taking (lock *goma*) would make this safe; it's safe now anyway ;; (mapcar 'mp:process-kill *threads*) ;; (mp:process-join (pop *threads*)) ;; ;;;Let's spawn some new gophers. ;;;Tbh this is pretty ugly, but it works for now. (spawn *goma* 0 "First spawn description" '(:child :first :item-type-0) " All the text that is going to go in here should probably be read from a file. But I'm not going to re-explain my fantastic sam-d.lisp reader macro here. Focus.") ;; (spawn *goma* 0 "Second spawn" '(:child :second :item-type-0) "shorter.") ``` let's have a quick look that lynx was, in fact, working.. (or you could just refresh lynx and have a look around) ```ksh lynx 'gopher://localhost:54321/0(:map)' ## Navigate to either, but now for something lynx isn't expecting ## (in lynx:) ## g gopher://localhost:54321/0(:child%20:item-type-0) #similarly printf "(:first :child)\n" | nc localhost 54321 ``` Haha, there we go. We have to write %20 instead of hitting #\space . : That gets a reverse-chronological list of everything hitting its keywords. ;Lisp aside: The comparison is (remove-if-not ; like (lambda (x) (subsetp keywords ; (item-specifier x) ; :test 'equalp)) ; gopher-list) ; So :keywords isn't important. The test is an 'equalp 'subsetp. But this level of interaction would suck if common lisp didn't have a canonical way for us to get code that would instantiate the objects we have created in it. ```ecl (make-load-form *goma*) ``` Yep, that's a lot of lisp-formatter idiomatic capital letter lisp code. My #'save just puts it in (filename *goma*) we set to my-gophermap.lisp earlier. ```ecl (save *goma*) (si:quit) ``` Take a look at ```ksh cat my-gophermap.lisp #Start a new lisp repl rlwrap ecl ``` ```ecl (load #p"i2pher.lisp") (in-package i2pher) (setq *goma-is-back* (with-open-file (in #p"my-gophermap.lisp") (eval (read in)))) ;;;And we're in business! (princ (in-memory *goma-is-back*)) (add-thread *goma-is-back*) ``` and refresh lynx, say. I'm pretty sure nested gophermaps work as well. (It's recursive). Pretty cool . About 'make-load-form. The Lisp spec gauruntees that there is a method for the generic 'make-load-form-saving-slots that always exists and works on any object. It also specifies the generic (make-load-form (object &optional environment)) since what constitutes a useful form of what make-load-form-saving-slots is able to guarantee normally always needs some massaging. I did that massage here. .