[squeak-dev] Re: HelpSystem & Class comments

Andreas Raab andreas.raab at gmx.de
Wed Feb 24 15:58:53 UTC 2010


Torsten Bergmann wrote:
> Andreas wrote:
>> What I'm looking for is a mechanism by which the documentation can 
>> remain in the image even when HelpSystem is absent
> 
> Why? Do you have test cases subclasses in the image when SUnit
> is absent? No.

Don't confuse cause and effect. The reason why you don't have test cases 
in the image when SUnit is absent is because you *can't* not because you 
don't want to. Documentation, in particular when it is simple text, can 
be consumed in a variety of ways.

> I think you want to have support or UI stuff removed/separated
> (like the viewer) - but a basic model should remain.
> The costs for the basic help system model support is minimal. 

Is it? Can we push it into the trunk? I'd love to have enough support in 
the trunk so that we can start providing documentation for the upcoming 
release. I've been wanting to rewrite the "What's New" section into a 
proper format, i.e., as reference for people downloading the next 
release so that we can point out the changes in the various areas, 
Compiler, Collection, Morphic, etc.

>> What I'm looking for is a mechanism by which the documentation can 
>> remain in the image even when HelpSystem is absent. So when HelpSystem 
>> is loaded it needs to discover available books. Is there some tag/marker 
>> mechanism by which one can flag those pre-existing books?
>>
>> (*hand waiving*)
> 
> Maybe we should return to a pragma based solution. I originally
> had something like this. Just load "HelpSystem-Core-tbn.4" and have a look
> at class HelpHowTo methods. They have a "help" pragram.

That's fine too. I honestly don't care, I just need *some* mechanism.

>> That's why I was saying, why not derive 
>> help books from class comments? 
> 
> How? Would you create a class for each page and use their
> class comment for the contents?

No. I've been wanting to write an entire book in a class comment with 
markup to denote sections in simple Wiki style. So the comment is the 
entire book.

Since I'm obviously I'm not making myself clear here I've attached an 
example. See what this does? All I'm asking for is to automate the 
process a little. This way I wouldn't even have to make a separate class 
I could just fetch the class comment straight from WebServer.

>> the only thing I'm asking for is just a tiny bit of additional structure >in a class comment so that one can have sections etc.
> 
> As I said - maybe we should reactivate some Scamper code and use a markup
>  language. The Parser from "Network-HTML" package on SqS.com/Scamper
> seems to work. Although it still uses _ instead of :=.
> We "just" have to rebuld a clean viewer.

Or we just use TSTTCPW. I.e., some completely trivial markup syntax. For 
class comments documentation this is advantageous because class comments 
are text already, they can contain formatting, doIts, links etc. The 
only thing they don't have at this point is structure.

>> We could start with simple Wiki syntax which can be used in 
>> class comments and other simple places. If people feel unsatisfied or 
>> need more or want to use HelpSystem for other purposes, extend it as 
>> desired.
> 
> We already discussed this and decided to first start with plain text 
> and later add different content types (markup, active bookmorphs, ...)
> after getting the tool integration and other capabilities done.
> (I already pointed to the original discussion on pharo-dev)

Like I said earlier, I'm offering my help. I think that class comments 
as books could be a major enabler for getting some documentation in.

Cheers,
   - Andreas

-------------- next part --------------
'From Squeak3.11alpha of 24 February 2010 [latest update: #9440] on 24 February 2010 at 4:49:58 pm'!
CustomHelp subclass: #WebServerHelp
	instanceVariableNames: ''
	classVariableNames: 'Default'
	poolDictionaries: ''
	category: 'Network-Web'!
!WebServerHelp commentStamp: 'ar 2/24/2010 16:49' prior: 0!
WebServer provides a simple HTTP server implementation. 

Using WebServer:

=== Running WebServer

A WebServer is started by listening on a particular port.

	(WebServer reset default)
		listenOn: 8080.

The server will persist when the image is restarted and must be stopped explicitly by sending it the #destroy message.

=== Adding Services

Once the server is running, you can point your browser to http://localhost:8080 but since we haven't told WebServer what we'd like to do all we get is a 404 (not found) response. To tell WebServer what to do we need to register a service:

	WebServer default addService: '/hello' action:[:req|
		req send200Response: 'Hello World'.
	].

The service takes a path (/hello) and an action to perform for any request that maps to this path. We can now fetch the response in a browser by going to http://localhost:8080/hello or directly using WebClient:

	(WebClient httpGet:'http://localhost:8080/hello') content.
	(WebClient httpGet:'http://localhost:8080/hello/squeak') content.

=== Service Hierarchies

More specific services are preferred over more general services.  In addition to the /hello service, we can provide a handler for /hello/squeak by adding the following service:

	WebServer default addService: '/hello/squeak' action:[:req|
		req send200Response: 'Hello to you too, Squeak!!'.
	].

	(WebClient httpGet:'http://localhost:8080/hello') content.
	(WebClient httpGet:'http://localhost:8080/hello/squeak') content.

A default handler for any kind of request can installed by using the '/' path:

	WebServer default addService: '/' action:[:req|
		req send3xxResponse: '/hello' code: 302. "temporary redirect"
	].

This will make any request that isn't handled by an explicit action redirect to /hello where it will be handled by the handler established earlier, for example:

	(WebClient httpGet:'http://localhost:8080/foobar') content.

=== Adding Actions

Let's add some real stuff that might be useful on a server:

	WebServer default addService: '/smalltalk' action:[:req| | target action |
		target := SmalltalkImage current.
		action := (req fields at: 'get' ifAbsent:['']) asSymbol.
		(target respondsTo: action) 
			ifTrue:[req send200Response: (target perform: action) asString]
			ifFalse:[req send404Response]].

We can now request some interesting things like:

	(WebClient httpGet:'http://localhost:8080/smalltalk?get=systemInformationString') content.
	(WebClient httpGet:'http://localhost:8080/smalltalk?get=platformName') content.

Obviously, this poses quite a risk for abuse. One way to limit this risk is to expose specific actions, such as here:

	#(systemInformationString platformName) do:[:symbol|
		WebServer default addService: '/info/', symbol action:[:req|
			req send200Response: (SmalltalkImage current perform: symbol) asString]].

	(WebClient httpGet:'http://localhost:8080/info/systemInformationString') content.
	(WebClient httpGet:'http://localhost:8080/info/platformName') content.

Alternatively, authentication can be used to limit access to exposed resources. 

=== Authentication

To add authentication you can use web server as follows:

	WebServer default addService: '/smalltalk' action:[:req| | target action |
		WebServer default authenticate: req realm: 'squeak' methods: #(digest basic) do:[
			target := SmalltalkImage current.
			action := (req fields at: 'get' ifAbsent:['']) asSymbol.
			(target respondsTo: action) 
				ifTrue:[req send200Response: (target perform: action) asString]
				ifFalse:[req send404Response]]].

The above supports both digest as well as basic authentication for accessing the /smalltalk service. Let's add a user so that we can access it:

	WebServer default passwordAt: 'squeak' realm: 'squeak' put: 'squeak'.

The server does NOT store plain text passwords, but rather hashes. To be precise, it stores the ha1 term used in digest authentication which is the same hash produced by htdigest. We can now access the /smalltalk service by providing user name 'squeak' and password 'squeak'.

	(WebClient httpGet:'http://localhost:8080/smalltalk?get=platformName') content.

Unfortunately, digest authentication can be slow since our MD5 implementation is rather pathetic. 

=== Cookies

Because of the performance issues with authentication, we'd like to avoid authentication for each request and instead authenticate once and use some persisten session state (cookies). So let's do that:

	WebServer default addService: '/smalltalk' action:[:req| | session |
		session := WebServer default sessionAt: (req cookieAt: 'session').
		session ifNil:[ "no session, reguire login"
			req send3xxResponse: '/login?url=', req rawUrl encodeForHTTP code: 302.
		] ifNotNil:[ | target action |
			target := SmalltalkImage current.
			action := (req fields at: 'get' ifAbsent:['']) asSymbol.
			(target respondsTo: action) 
				ifTrue:[req send200Response: (target perform: action) asString]
				ifFalse:[req send404Response]]].

And of course we now need a login service The service will require authentication and provide a session identifier for the client. It then redirects back to where the request was originally made from:

	WebServer default addService: '/login' action:[:req| | session |
		WebServer default authenticate: req realm: 'squeak' methods: #(digest) do:[ | id |
			"We have no session state for now, just remember the session id"
			WebServer default sessionAt: (id := UUID new hex) put: ''.
			"Send a redirect back to where we came from with a cookie"
			req send3xxResponse: (req fields at: 'url' ifAbsent:['/']) code: 302 
				do:[:reply| reply setCookie: 'session' value: id path: '/smalltalk']]].

=== WebClient

At this point, making a request like the following:

	(WebClient httpGet: 'http://localhost:8080/smalltalk?get=platformName') content.

Requires several roundtrips:

	1) The first GET request is redirected from /smalltalk to /login
	2) The request to /login is required to authenticate
	3) The request to /login is authenticated and a session cookie is established
	4) The request is redirected back to /smalltalk where it is finally handled.

WebClient makes dealing with these complexities easy.!


"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!

WebServerHelp class
	instanceVariableNames: ''!

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:35'!
bookName
	^'Using WebServer'! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:23'!
page1
	^self topics at: 1! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:24'!
page2
	^self topics at: 2! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:24'!
page3
	^self topics at: 3! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:24'!
page4
	^self topics at: 4! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:24'!
page5
	^self topics at: 5! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:24'!
page6
	^self topics at: 6! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:24'!
page7
	^self topics at: 7! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:24'!
page8
	^self topics at: 8! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:23'!
page9
	^self topics at: 9! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:24'!
pages
	^#(page1 page2 page3 page4 page5 page6 page7 page8 page9 ) 
			first: self topics size.! !

!WebServerHelp class methodsFor: 'accessing' stamp: 'ar 2/24/2010 16:36'!
topics
	"Compute the topics from WebServer's class comment"
	| topics comment start stop contents title |
	topics := OrderedCollection new.
	comment := self comment.
	start := stop := 0.
	[start := comment asString indexOfSubCollection: '=== ' startingAt: stop+1 ifAbsent:[0].
	start = 0] whileFalse:[
		stop := (comment asString asString 
					indexOfSubCollection: '=== ' 
					startingAt: start+4 
					ifAbsent:[comment size+1]) - 1.
		contents := comment copyFrom: start+4 to: stop.
		title := contents asString copyUpTo: Character cr.
		topics add: (HelpPage title: title contents: contents).
	].
	^topics! !


More information about the Squeak-dev mailing list