├── README
├── LICENSE
├── tosqueak.sh
├── package.xml
├── auth.st
├── xml.st
├── hacks.st
├── parser.st
├── pretty.st
├── server.st
├── responses.st
├── docs
└── xml-protocol.org
├── requests.st
└── tests.st
/README:
--------------------------------------------------------------------------------
1 | This is a Shampoo implementation for GNU Smalltalk
2 | --------------------------------------------------
3 |
4 | Installation:
5 |
6 | $ gst-package -t ~/.st package.xml
7 |
8 | Do not forget to run tests to check if all is ok:
9 |
10 | $ gst-sunit -p Shampoo
11 |
12 | To create and run a basic Shampoo image, invoke:
13 |
14 | $ gst-load -iI shampoo.im Shampoo
15 | $ gst-remote -I shampoo.im --daemon
16 | $ gst-remote -e "Shampoo.ShampooServer startOn: 9092 login: 'login' pass: '1234'"
17 |
18 | Shampoo project homepage:
19 |
20 | http://dmitrymatveev.co.uk/shampoo
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2010 - 2012 Dmitry Matveev
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/tosqueak.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | FILES=$(grep filein package.xml | sed 's/<[/a-z]*>//g')
4 | OUTPUT=shampoo-squeak-$(git rev-parse --short HEAD).st
5 | TRANSCRIPT=ShampooTranscript
6 | XMLNODE=ShNode
7 | XMLTEXT=ShText
8 |
9 | gst-convert -v \
10 | -f gst \
11 | -F squeak \
12 | -C -$TRANSCRIPT -C -$XMLNODE -C -$XMLTEXT \
13 | -o $OUTPUT \
14 | $FILES
15 |
16 | if [ "$?" -ne "0" ]; then
17 | echo "Fatal error, exiting"
18 | exit 1
19 | fi
20 |
21 | sed -i \
22 | -e 's/ShampooXML\.ShNode/ShampooXMLNode/g' \
23 | -e 's/ShampooXML\.ShText/ShampooXMLText/g' \
24 | -e 's/ShNode/ShampooXMLNode/g' \
25 | -e 's/ShText/ShampooXMLText/g' \
26 | -e 's/Shampoo.ShampooTranscript install\!//g' \
27 | -e "s/methodsFor: nil/methodsFor: 'as yet unclassified'/g" \
28 | -e 's/LoginTest/ShampooLoginTest/g' \
29 | -e 's/NamespacesTest/ShampooNamespacesTest/g' \
30 | -e 's/ClassesTest/ShampooClassesTest/g' \
31 | -e 's/ClassTest/ShampooClassTest/g' \
32 | -e 's/CatsTest/ShampooCatsTest/g' \
33 | -e 's/MethodsTest/ShampooMethodsTest/g' \
34 | -e 's/MethodTest/ShampooMethodTest/g' \
35 | -e 's/CompileClassTest/ShampooCompileClassTest/g' \
36 | -e 's/CompileClassSideTest/ShampooCompileClassSideTest/g' \
37 | -e 's/CompileMethodTest/ShampooCompileMethodTest/g' \
38 | -e 's/DoItTest/ShampooDoItTest/g' \
39 | -e 's/PrintItTest/ShampooPrintItTest/g' \
40 | $OUTPUT
41 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
2 | Shampoo
3 |
4 | TCP
5 | XML
6 | XML-PullParser
7 | Digest
8 |
9 | Shampoo
10 |
11 |
12 | %1 isNil ifTrue:
13 | [^Shampoo.ShampooServer startOn: 9090 login: 'user' pass: '1234'].
14 |
15 | %1 =~ '^([A-z0-9]+)\:([A-z0-9]+):([0-9]+)' ifMatched:
16 | [:m | ^Shampoo.ShampooServer
17 | startOn: (m at: 3) asNumber login: (m at: 1) pass: (m at: 2)].
18 |
19 |
20 |
21 | %1 isNil ifTrue:
22 | [^Shampoo.ShampooServer closeAll].
23 |
24 | %1 ~ '^([0-9]+)' ifTrue:
25 | [^Shampoo.ShampooServer closeOn: %1 asNumber]
26 |
27 |
28 | xml.st
29 | hacks.st
30 | auth.st
31 | requests.st
32 | responses.st
33 | server.st
34 | parser.st
35 | pretty.st
36 |
37 | xml.st
38 | hacks.st
39 | auth.st
40 | requests.st
41 | responses.st
42 | server.st
43 | parser.st
44 | pretty.st
45 |
46 |
47 | Shampoo.LoginTest
48 | Shampoo.NamespacesTest
49 | Shampoo.ClassesTest
50 | Shampoo.ClassTest
51 | Shampoo.CatsTest
52 | Shampoo.MethodsTest
53 | Shampoo.MethodTest
54 | Shampoo.CompileClassTest
55 | Shampoo.CompileClassSideTest
56 | Shampoo.CompileMethodTest
57 | Shampoo.DoItTest
58 | Shampoo.PrintItTest
59 | Shampoo.OperationalResponseTest
60 | Shampoo.MessageParserTest
61 | Shampoo.ShampooNotAuthStateTest
62 | Shampoo.ShampooConnectionTest
63 | Shampoo.ShampooRemoveClassTest
64 | Shampoo.ShampooRemoveMethodTest
65 | Shampoo.ShampooChangeCategoryTest
66 | Shampoo.ShampooRemoveCategoryTest
67 | Shampoo.RenameCategoryTest
68 | Shampoo.MethodSourcePrettifierTest
69 | Shampoo.ShampooFileOutClassTest
70 | Shampoo.ShampooFileOutClassCategoryTest
71 | Shampoo.ShampooFileOutNamespaceTest
72 | tests.st
73 |
74 |
75 |
--------------------------------------------------------------------------------
/auth.st:
--------------------------------------------------------------------------------
1 | Namespace current: Shampoo [
2 |
3 | Object subclass: AuthInfo [
4 | | login passHash |
5 |
6 |
7 | >startOn:login:pass:). Obviously the same login and password
14 | should be specified by a client then.
15 |
16 | When a client connects to the server, the server generates a pseudo-random magic
17 | string (a number) and sends it to the client. The client then should calculate
18 | an MD5 hash of a password, concatenate it with the magic string and then take
19 | a hash of the concatenation again. This hash is then sent back to the server.
20 |
21 | Server performs the same operations and then compares the hashes. So the password
22 | is not transmitted across the network, and the server also does not store it
23 | in the plain form in the memory.'>
24 |
25 | AuthInfo class >> login: aLogin pass: aPass [
26 |
27 | ^(self new)
28 | login: aLogin pass: aPass;
29 | yourself
30 | ]
31 |
32 | login: aLogin pass: aPass [
33 |
34 | login := aLogin.
35 | passHash := (MD5 new: aPass) hexDigest
36 | ]
37 |
38 | with: aMagic [
39 |
40 | ^(LoginInfo new)
41 | login: login
42 | magicHash: (MD5 new: (aMagic printString, passHash)) hexDigest
43 | ]
44 | ]
45 |
46 | Object subclass: LoginInfo [
47 | | login magicHash |
48 |
49 |
50 |
53 |
54 | LoginInfo class >> fromXML: anXML [
55 |
56 | ^(self new)
57 | login: (anXML attrMap at: 'login')
58 | magicHash: (anXML attrMap at: 'magic');
59 | yourself
60 | ]
61 |
62 | login: aString magicHash: aHash [
63 |
64 | login := aString.
65 | magicHash := aHash
66 | ]
67 |
68 | = aLoginInfo [
69 |
70 | ^(aLoginInfo class = self class) and: [self hash = aLoginInfo hash]
71 | ]
72 |
73 | hash [
74 |
75 | ^login hash bitXor: magicHash hash
76 | ]
77 | ]
78 | ]
79 |
--------------------------------------------------------------------------------
/xml.st:
--------------------------------------------------------------------------------
1 | Namespace current: Smalltalk [
2 | Namespace current: ShampooXML [
3 | Exception subclass: Malformed [
4 |
5 | ]
6 |
7 | Object subclass: ShNode [
8 |
9 |
11 |
12 | | underlyingXML |
13 |
14 | ShNode class >> tagname: aString [
15 |
16 | ^(self new)
17 | underlyingXML: (XML.Element tag: aString);
18 | yourself
19 | ]
20 |
21 | ShNode class >> from: aString [
22 |
23 | [| doc |
24 | doc := XML.XMLParser processDocumentString: aString
25 | beforeScanDo: [:p | p validate: false].
26 | ^(self new)
27 | underlyingXML: doc root;
28 | yourself
29 | ] on: XML.MalformedSignal
30 | do: [:e | Malformed new signal]
31 | ]
32 |
33 | underlyingXML: anObject [
34 |
35 | underlyingXML := anObject
36 | ]
37 |
38 | underlyingXML [
39 |
40 | ^underlyingXML
41 | ]
42 |
43 | attrMap [
44 |
45 | ^Dictionary from:
46 | (underlyingXML attributes collect:
47 | [:each | each key type -> each value])
48 | ]
49 |
50 | addAttribute: anAttrName value: aValueString [
51 |
52 | | attr |
53 | attr := XML.Attribute name: anAttrName value: aValueString.
54 | underlyingXML addAttribute: attr
55 | ]
56 |
57 | addNode: aNode [
58 |
59 | underlyingXML addNode: aNode underlyingXML
60 | ]
61 |
62 | elementsNamed: aString [
63 |
64 | ^(underlyingXML elementsNamed: aString) collect:
65 | [:each | ShNode new underlyingXML: each]
66 | ]
67 |
68 | text [
69 |
70 | ^underlyingXML characterData
71 | ]
72 |
73 | printOn: aStream [
74 |
75 | underlyingXML printOn: aStream
76 | ]
77 | ]
78 |
79 | ShNode subclass: ShText [
80 |
81 |
83 |
84 | ShText class >> text: aString [
85 | ^(self new)
86 | underlyingXML: (XML.Text text: aString);
87 | yourself
88 | ]
89 | ]
90 | ]
91 | ]
92 |
--------------------------------------------------------------------------------
/hacks.st:
--------------------------------------------------------------------------------
1 | Object extend [
2 | namespaceChain [
3 |
4 | | ns chain |
5 | ns := self environment.
6 | chain := OrderedCollection new.
7 | [ns ~= Smalltalk] whileTrue:
8 | [chain add: ns. ns := ns environment].
9 | ^chain reverse
10 | ]
11 | ]
12 |
13 | Collection extend [
14 | elementsString [
15 |
16 | ^String join: self separatedBy: ' '
17 | ]
18 |
19 | asStringArray [
20 |
21 | "Many reflection methods return different results in various dialects.
22 | #instVarNames in GNU Smalltalk returns an IdentitySet of Symbols, the
23 | same method returns an Array of Strings in Squeak.
24 |
25 | This kludge works as an abstraction over it all"
26 | ^(self collect: [:each | each asString]) asArray
27 | ]
28 |
29 | or [
30 |
31 | self isEmpty ifTrue: [^false].
32 | ^self fold: [:a :b | a | b]
33 | ]
34 |
35 | and [
36 |
37 | self isEmpty ifTrue: [^true].
38 | ^self fold: [:a :b | a & b]
39 | ]
40 | ]
41 |
42 | BlockClosure extend [
43 | not [
44 |
45 | self numArgs ~= 1 ifTrue:
46 | [^self error: '#not is for single argument blocks only'].
47 |
48 | ^[:value | (self value: value) not]
49 | ]
50 | ]
51 |
52 | PositionableStream extend [
53 | forwardWhile: aBlock [
54 |
55 | [self atEnd not and: [aBlock value: self peek]]
56 | whileTrue: [self next]
57 | ]
58 | ]
59 |
60 | SequenceableCollection extend [
61 | drop: anInteger [
62 |
63 | anInteger > self size ifTrue: [^self class new].
64 | ^self copyFrom: anInteger + 1
65 | ]
66 |
67 | take: anInteger [
68 |
69 | anInteger > self size ifTrue: [^self copy].
70 | anInteger = 0 ifTrue: [^self class new].
71 | ^self copyFrom: 1 to: anInteger
72 | ]
73 |
74 | breakIf: aBlock [
75 |
76 | | r |
77 | r := ReadStream on: self.
78 | r forwardWhile: aBlock not.
79 | ^{ self copyFrom: 1 to: r position.
80 | r upToEnd }
81 | ]
82 |
83 | dropWhile: aBlock [
84 |
85 | | r |
86 | r := ReadStream on: self.
87 | r forwardWhile: aBlock.
88 | ^self copyFrom: r position + 1
89 | ]
90 |
91 | takeWhile: aBlock [
92 |
93 | | r |
94 | r := ReadStream on: self.
95 | r forwardWhile: aBlock.
96 | ^self copyFrom: 1 to: r position
97 | ]
98 | ]
99 |
100 | String class extend [
101 | crlf [
102 |
103 | ^String new writeStream
104 | nextPut: Character cr;
105 | nextPut: Character lf;
106 | contents
107 | ]
108 | ]
109 |
110 | String extend [
111 | isClosingBracket [
112 |
113 | ^self trimSeparators = ']'
114 | ]
115 | ]
116 |
117 | PackageLoader class extend [
118 | reloadPackage: aPackageName [
119 |
120 | | base |
121 | base := PackageLoader directoryFor: aPackageName.
122 | (PackageLoader fileInsFor: aPackageName) do:
123 | [:e | FileStream fileIn: (base / e) file name].
124 | ]
125 | ]
126 |
127 | Namespace current: Sockets [
128 | StreamSocket extend [
129 | crlf [
130 |
131 | self nextPutAll: String crlf
132 | ]
133 | ]
134 | ]
135 |
136 | Object subclass: Decorator [
137 | | underlyingObject |
138 |
139 | Decorator class >> on: anObject [
140 |
141 | ^self new
142 | underlyingObject: anObject;
143 | yourself
144 | ]
145 |
146 | underlyingObject: anObject [
147 |
148 | underlyingObject := anObject
149 | ]
150 |
151 | underlyingObject [
152 |
153 | ^underlyingObject
154 | ]
155 |
156 | doesNotUnderstand: aMessage [
157 |
158 | "Proxy an unknown message to an underlying object"
159 | ^underlyingObject
160 | perform: aMessage selector
161 | withArguments: aMessage arguments
162 | ]
163 | ]
164 |
--------------------------------------------------------------------------------
/parser.st:
--------------------------------------------------------------------------------
1 | Namespace current: Shampoo [
2 |
3 | Object subclass: MessageParserState [
4 |
5 |
8 |
9 | | parser buffer |
10 |
11 | MessageParserState class >> new: aString [
12 |
13 | ^self basicNew
14 | initialize: aString;
15 | yourself
16 | ]
17 |
18 | initialize: aString [
19 |
20 | buffer := aString
21 | ]
22 |
23 | invalidateBuffer [
24 |
25 | buffer := String new.
26 | ]
27 |
28 | parser: aParser [
29 |
30 | parser := aParser.
31 | ]
32 |
33 | consume: aString [
34 |
35 | buffer := buffer, aString
36 | ]
37 |
38 | canProcess [
39 |
40 | self subclassResponsibility
41 | ]
42 |
43 | processInto: aCollection [
44 |
45 | [self tryParse: buffer readStream into: aCollection
46 | ] on: Error do: [:e | e inspect. self handleFail]
47 | ]
48 |
49 | switchToContent: aLength rest: aString [
50 |
51 | | st |
52 | st := ParseContentState buffer: aString length: aLength.
53 | parser switchTo: st
54 | ]
55 |
56 | switchToHeader: aString [
57 |
58 | parser switchTo: (ParseHeaderState buffer: aString)
59 | ]
60 |
61 | tryParse: aStream into: aCollection [
62 |
63 | self subclassResponsibility
64 | ]
65 |
66 | fail [
67 |
68 | self error: 'Error occured during parsing'
69 | ]
70 |
71 | handleFail [
72 |
73 | self subclassResponsibility
74 | ]
75 | ]
76 |
77 | MessageParserState subclass: ParseHeaderState [
78 |
79 |
83 |
84 | ParseHeaderState class >> buffer: aString [
85 |
86 | ^(self new: aString)
87 | yourself
88 | ]
89 |
90 | canProcess [
91 |
92 | ^buffer lines size >= 2
93 | ]
94 |
95 | tryParse: aStream into: aCollection [
96 |
97 | | sz |
98 | aStream upToAll: 'Content-Length: '.
99 | aStream atEnd ifTrue: [self fail].
100 | sz := Integer readFrom: aStream.
101 | sz = 0 ifTrue: [self fail].
102 | 2 timesRepeat:
103 | [(aStream next: 2) = String crlf ifFalse: [self fail]].
104 | self switchToContent: sz rest: aStream upToEnd
105 | ]
106 |
107 | handleFail [
108 |
109 | self invalidateBuffer
110 | ]
111 | ]
112 |
113 | MessageParserState subclass: ParseContentState [
114 |
115 |
119 |
120 | | expected |
121 |
122 | ParseContentState class >> buffer: aString length: anInteger [
123 |
124 | ^(self new: aString)
125 | expected: anInteger;
126 | yourself
127 | ]
128 |
129 | expected: anInteger [
130 |
131 | expected := anInteger
132 | ]
133 |
134 | canProcess [
135 |
136 | ^buffer size >= expected
137 | ]
138 |
139 | tryParse: aStream into: aCollection [
140 |
141 | aCollection add: (aStream next: expected).
142 | self switchToHeader: aStream upToEnd
143 | ]
144 |
145 | handleFail [
146 |
147 | "Do nothing"
148 | ]
149 | ]
150 |
151 | Object subclass: MessageParser [
152 |
153 |
155 |
156 | | state |
157 |
158 | MessageParser class >> new [
159 |
160 | ^(self basicNew)
161 | initialize;
162 | yourself
163 | ]
164 |
165 | initialize [
166 |
167 | self switchTo: (ParseHeaderState buffer: '')
168 | ]
169 |
170 | process: aString [
171 |
172 | | results |
173 | results := OrderedCollection new.
174 | state consume: aString.
175 | [state canProcess] whileTrue:
176 | [state processInto: results].
177 | ^results
178 | ]
179 |
180 | switchTo: aState [
181 |
182 | state := aState.
183 | state parser: self.
184 | ]
185 | ]
186 | ]
187 |
--------------------------------------------------------------------------------
/pretty.st:
--------------------------------------------------------------------------------
1 | Namespace current: Shampoo [
2 | Object subclass: MethodSource [
3 |
4 |
5 | | selectorWithArguments body |
6 |
7 | MethodSource class >> from: aString [
8 |
9 | ^(self new)
10 | init: aString;
11 | yourself
12 | ]
13 |
14 | MethodSource >> init: aString [
15 |
16 | | r |
17 | r := ReadStream on: aString.
18 | selectorWithArguments := (r upTo: $[) trimSeparators.
19 | body := ((r upToEnd) copyUpToLast: $]) lines.
20 | ]
21 |
22 | selectorWithArguments [
23 |
24 | ^selectorWithArguments
25 | ]
26 |
27 | body [
28 |
29 | ^body
30 | ]
31 |
32 | methodIndent [
33 |
34 | | notEmpty spaceLens |
35 |
36 | "At first, drop all empty strings"
37 | notEmpty := body select:
38 | [:each | each trimSeparators isEmpty not].
39 |
40 | spaceLens := notEmpty collect:
41 | [:each | (each takeWhile: [:c | c isSeparator]) size].
42 |
43 | ^spaceLens isEmpty
44 | ifTrue: [0]
45 | ifFalse: [spaceLens fold: [:a :b | a min: b]]
46 | ]
47 |
48 | compact [
49 |
50 | | r fst |
51 |
52 | "Remove empty lines from the beginning, if any"
53 | r := ReadStream on: body.
54 | r forwardWhile: [:line | line trimSeparators isEmpty].
55 | body := body drop: r position.
56 | body isEmpty ifTrue: [^self].
57 |
58 | "Remove newline characters from the beginning of the
59 | first line"
60 | fst := body first breakIf: [:c | c isSeparator not].
61 | fst := (fst first copyWithout: Character nl), fst second.
62 | body at: 1 put: fst.
63 |
64 | "Remove empty lines from the end, if any"
65 | r := ReadStream on: body reverse.
66 | r forwardWhile: [:line | line trimSeparators isEmpty].
67 | body := body take: body size - r position.
68 | ]
69 |
70 | stripIndent [
71 |
72 | | indent |
73 | indent := self methodIndent.
74 | body := body collect: [:each | each drop: indent].
75 | ]
76 |
77 | indentWith: aString [
78 |
79 | body := body collect: [:each | aString, each].
80 | ]
81 |
82 | printOn: aStream [
83 | aStream
84 | nextPutAll: selectorWithArguments;
85 | space;
86 | nextPut: $[;
87 | nl.
88 | body do: [:line | aStream nextPutAll: line; nl].
89 | aStream nextPut: $]; nl.
90 | ]
91 |
92 | sourceString [
93 |
94 | | w |
95 | w := WriteStream on: String new.
96 | self printOn: w.
97 | ^w contents
98 | ]
99 | ]
100 |
101 | Object subclass: ClassSource [
102 |
103 |
104 | | printedClass namespaces |
105 |
106 | ClassSource class >> of: aClass [
107 |
108 | ^self new
109 | printedClass: aClass;
110 | yourself
111 | ]
112 |
113 | printedClass: aClass [
114 |
115 | printedClass := aClass.
116 | namespaces := printedClass namespaceChain.
117 | ]
118 |
119 | enclosingNamespaces: aStream do: aBlock [
120 |
121 | namespaces do:
122 | [:ns | aStream
123 | nextPutAll: 'Namespace current:';
124 | space;
125 | nextPutAll: ns name asString;
126 | space;
127 | nextPut: $[;
128 | space].
129 | aStream nl.
130 | aBlock value.
131 | namespaces size timesRepeat: [aStream nextPut: $]].
132 | aStream nl
133 | ]
134 |
135 | enclosingClassDefinition: aStream do: aBlock [
136 |
137 | | superName |
138 | superName := printedClass superclass
139 | ifNil: ['nil']
140 | ifNotNil: [printedClass superclass name asString].
141 | aStream
142 | nextPutAll: superName;
143 | space;
144 | nextPutAll: 'subclass:';
145 | space;
146 | nextPutAll: printedClass name asString;
147 | space;
148 | nextPut: $[;
149 | nl.
150 | aStream increasingIndent: [aBlock value].
151 | aStream nextPut: $]; nl
152 | ]
153 |
154 | writePragma: aPragmaName value: aString on: aStream [
155 |
156 | aStream
157 | nextPut: $<;
158 | nextPutAll: aPragmaName;
159 | nextPut: $:;
160 | space;
161 | nextPutAll: aString printString;
162 | nextPut: $>
163 | ]
164 |
165 | writeCommentsOn: aStream [
166 |
167 | printedClass category isNil ifFalse:
168 | [self writePragma: 'category'
169 | value: printedClass category
170 | on: aStream.
171 | aStream nl].
172 | printedClass comment isNil ifFalse:
173 | [self writePragma: 'comment'
174 | value: printedClass comment
175 | on: aStream.
176 | aStream nl]
177 | ]
178 |
179 | writeInstVarsOn: aStream [
180 |
181 | | instvars |
182 | instvars := printedClass instVarNames asStringArray.
183 | instvars isEmpty ifFalse:
184 | [aStream
185 | nl;
186 | nextPut: $|;
187 | space;
188 | nextPutAll: instvars elementsString;
189 | space;
190 | nextPut: $|;
191 | nl]
192 | ]
193 |
194 | writeClassVarsOn: aStream [
195 |
196 | printedClass classVarNames asStringArray do:
197 | [:each | aStream
198 | nextPutAll: each;
199 | space;
200 | nextPutAll: ':= nil.';
201 | nl]
202 | ]
203 |
204 | writeMethod: aMethod on: aStream [
205 |
206 | | ms |
207 | ms := MethodSource from: aMethod methodSourceString.
208 | ms compact; stripIndent.
209 | aStream nl.
210 | ms printOn: aStream.
211 | ]
212 |
213 | writeClassMethod: aMethod on: aStream [
214 |
215 | | ms start |
216 | ms := MethodSource from: aMethod methodSourceString.
217 | ms compact; stripIndent.
218 | aStream nl.
219 | start := aStream position.
220 | aStream
221 | nextPutAll: printedClass name asString;
222 | space;
223 | nextPutAll: 'class >> '.
224 | aStream position + ms selectorWithArguments size - start
225 | >= 67 ifTrue: [aStream nl].
226 | ms printOn: aStream.
227 | ]
228 |
229 | writeMethodsOn: aStream [
230 |
231 | printedClass methodDictionary ifNotNil:
232 | [printedClass methodDictionary values do:
233 | [:each | self writeMethod: each on: aStream]]
234 | ]
235 |
236 | writeClassMethodsOn: aStream [
237 |
238 | printedClass class methodDictionary ifNotNil:
239 | [printedClass class methodDictionary values do:
240 | [:each | self writeClassMethod: each on: aStream]]
241 | ]
242 |
243 | sourceString [
244 |
245 | | w is ns |
246 | w := WriteStream on: String new.
247 | is := IndentedStreamDecorator on: w.
248 | self enclosingNamespaces: is do:
249 | [self enclosingClassDefinition: is do:
250 | [self
251 | writeCommentsOn: is;
252 | writeInstVarsOn: is;
253 | writeClassVarsOn: is;
254 | writeClassMethodsOn: is;
255 | writeMethodsOn: is
256 | ]].
257 | ^is contents.
258 | ]
259 | ]
260 |
261 | Object subclass: PrettyPrinter [
262 |
263 |
264 | PrettyPrinter class >> prettifyMethod: aSourceString [
265 |
266 | ^(MethodSource from: aSourceString)
267 | compact;
268 | stripIndent;
269 | indentWith: ' ';
270 | sourceString
271 | ]
272 |
273 | PrettyPrinter class >> prettifyClass: aClass [
274 |
275 | ^(ClassSource of: aClass)
276 | sourceString
277 | ]
278 |
279 | PrettyPrinter class >> prettifyClasses: aCollectionOfClasses [
280 |
281 | | prettified |
282 | prettified := aCollectionOfClasses collect:
283 | [:each | self prettifyClass: each].
284 |
285 | ^String join: prettified
286 | ]
287 | ]
288 |
289 | Decorator subclass: IndentedStreamDecorator [
290 |
291 |
292 | | state indentLevel levelSpaces |
293 |
294 | prepareIndent [
295 |
296 | state := #preparingToIndent
297 | ]
298 |
299 | tryIndent [
300 |
301 | self state = #preparingToIndent ifTrue: [self indent]
302 | ]
303 |
304 | increasingIndent: aBlock [
305 |
306 | indentLevel := self indentLevel + 1.
307 | [aBlock value] ensure:
308 | [indentLevel := self indentLevel - 1]
309 | ]
310 |
311 | indent [
312 |
313 | self indentLevel * self levelSpaces timesRepeat:
314 | [self underlyingObject space].
315 | state := #indented
316 | ]
317 |
318 | nl [
319 |
320 | self prepareIndent.
321 | self underlyingObject nl
322 | ]
323 |
324 | nextPut: anObject [
325 |
326 | anObject = Character nl
327 | ifTrue: [self prepareIndent]
328 | ifFalse: [self tryIndent].
329 | self underlyingObject nextPut: anObject.
330 | ]
331 |
332 | nextPutAll: aCollection [
333 |
334 | aCollection do: [:each | self nextPut: each]
335 | ]
336 |
337 | state [
338 |
339 | ^state ifNil: [state := #preparingToIndent]
340 | ]
341 |
342 | indentLevel [
343 |
344 | ^indentLevel ifNil: [indentLevel := 0]
345 | ]
346 |
347 | levelSpaces [
348 |
349 | ^levelSpaces ifNil: [levelSpaces := 4]
350 | ]
351 | ]
352 | ]
353 |
--------------------------------------------------------------------------------
/server.st:
--------------------------------------------------------------------------------
1 | Namespace current: Shampoo [
2 |
3 | Error subclass: ClientDisconnected [
4 |
5 |
7 | ]
8 |
9 | Error subclass: FatalError [
10 |
11 |
13 | ]
14 |
15 | Object subclass: ShampooTranscript [
16 |
17 |
20 |
21 | servers := Set new.
22 | serversMutex := Semaphore forMutualExclusion.
23 | oldAssoc := nil.
24 |
25 | ShampooTranscript class >> initialize [
26 |
27 | self install
28 | ]
29 |
30 | ShampooTranscript class >> message: aString [
31 |
32 | self fallback: aString.
33 | serversMutex critical: [servers do: [:each | each echo: aString]]
34 | ]
35 |
36 | ShampooTranscript class >> fallback: aString [
37 |
38 | oldAssoc ifNotNil:
39 | [oldAssoc key perform: oldAssoc value with: aString]
40 | ]
41 |
42 | ShampooTranscript class >> add: aServer [
43 |
44 | serversMutex critical: [servers add: aServer]
45 | ]
46 |
47 | ShampooTranscript class >> remove: aServer [
48 |
49 | serversMutex critical:
50 | [servers remove: aServer ifAbsent: []]
51 | ]
52 |
53 | ShampooTranscript class >> install [
54 |
55 | | message |
56 | message := Transcript message.
57 | (message key = ShampooTranscript and: [message value = #message:])
58 | ifFalse: [oldAssoc := message.
59 | Transcript message: (ShampooTranscript -> #message:)]
60 | ]
61 | ]
62 |
63 | Object subclass: ConnectionState [
64 |
65 |
67 |
68 | | connection |
69 |
70 | ConnectionState class >> of: aConnection [
71 |
72 | ^(self new)
73 | connection: aConnection;
74 | yourself
75 | ]
76 |
77 | connection: aConnection [
78 |
79 | connection := aConnection
80 | ]
81 |
82 | switchTo: aConnectionStateClass [
83 |
84 | connection state: (aConnectionStateClass of: connection)
85 | ]
86 |
87 | process: aRequest [
88 |
89 | self subclassResponsibility
90 | ]
91 |
92 | echo: aString [
93 |
94 | "Do nothing by default. 'self subclassResponsibility' would be better?"
95 | ]
96 | ]
97 |
98 | ConnectionState subclass: NotAuthorizedState [
99 |
100 |
103 | | magic |
104 |
105 | register [
106 |
107 | "Send a magic number to a client"
108 | connection send: (MagicResponse number: self magic)
109 | ]
110 |
111 | process: aRequest [
112 |
113 | "Anti-If campaign probably hates me."
114 | aRequest class == LoginRequest
115 | ifTrue: [^self checkLogin: aRequest]
116 | ifFalse: [FatalError new signal]
117 | ]
118 |
119 | checkLogin: aRequest [
120 |
121 | "Really hates."
122 | (connection server authenticates: aRequest creds with: self magic)
123 | ifTrue: [self switchTo: AuthorizedState.
124 | ^ServerInfoResponse id: aRequest id]
125 | ifFalse: [FatalError new signal]
126 | ]
127 |
128 | magic [
129 |
130 | ^magic ifNil: [magic := Random between: 0 and: 16rFFFF]
131 | ]
132 | ]
133 |
134 | ConnectionState subclass: AuthorizedState [
135 |
136 |
139 |
140 | process: aRequest [
141 |
142 | ^aRequest execute
143 | ]
144 |
145 | echo: aString [
146 |
147 | connection send: (EchoResponse id: -1 text: aString)
148 | ]
149 | ]
150 |
151 | Object subclass: ProcessObject [
152 |
153 |
156 |
157 | | process |
158 |
159 | proc: aProcess [
160 |
161 | process := aProcess
162 | ]
163 |
164 | suspend [
165 |
166 | process ifNotNil: [process suspend]
167 | ]
168 |
169 | resume [
170 |
171 | process ifNotNil: [process resume]
172 | ]
173 | ]
174 |
175 | Object subclass: ClientConnection [
176 |
177 |
179 |
180 | | sock server disconnectedHandler state parser |
181 |
182 | ClientConnection class >> on: aSocket onDisconnect: aBlock parent: aServer [
183 |
184 | ^aSocket
185 | ifNil: [nil]
186 | ifNotNil: [self new on: aSocket onDisconnect: aBlock parent: aServer]
187 | ]
188 |
189 | on: aSocket onDisconnect: aBlock parent: aServer [
190 |
191 | sock := aSocket.
192 | server := aServer.
193 | disconnectedHandler := aBlock.
194 | state := NotAuthorizedState of: self.
195 | state register.
196 | parser := MessageParser new.
197 | ]
198 |
199 | go [
200 |
201 | [sock isPeerAlive] whileTrue:
202 | [[self fetchMessages do: [:m | self process: m]]
203 | on: ClientDisconnected
204 | do: [:e | ^self signalDisconnected]].
205 | self signalDisconnected
206 | ]
207 |
208 | fetchMessages [
209 |
210 | [sock ensureReadable] ifError: [ClientDisconnected new signal].
211 | ^parser process: (sock next: sock availableBytes)
212 | ]
213 |
214 | signalDisconnected [
215 |
216 | disconnectedHandler ifNotNil: [:handler | handler value: self]
217 | ]
218 |
219 | process: anXMLRequest [
220 |
221 | | r xml |
222 | [xml := ShampooXML.ShNode from: anXMLRequest]
223 | ifError:
224 | [^server inform:
225 | 'Shampoo: failed to parse ', anXMLRequest printString].
226 | r := Request from: xml.
227 | [(self state process: r) do: [:resp | self send: resp]]
228 | on: FatalError do: [:e | self close]
229 | ]
230 |
231 | state: aClientConnectionState [
232 |
233 | state := aClientConnectionState
234 | ]
235 |
236 | state [
237 |
238 | ^state
239 | ]
240 |
241 | echo: aString [
242 |
243 | self state echo: aString
244 | ]
245 |
246 | send: aPacket [
247 |
248 | | msg |
249 | msg := aPacket asXML printString.
250 | sock
251 | nextPutAll: 'Content-Length: ', msg size printString;
252 | crlf;
253 | crlf;
254 | nextPutAll: msg;
255 | crlf.
256 | sock flush
257 | ]
258 |
259 | close [
260 |
261 | sock close
262 | ]
263 |
264 | server [
265 |
266 | ^server
267 | ]
268 | ]
269 |
270 | ProcessObject subclass: ShampooServer [
271 |
272 |
274 |
275 | | server clients clientsMutex creds |
276 |
277 | servers := Dictionary new.
278 | serversMutex := Semaphore forMutualExclusion.
279 |
280 | clients [
281 |
282 | ^clients ifNil: [clients := OrderedCollection new]
283 | ]
284 |
285 | acceptedClient [
286 |
287 | | client |
288 | client := ClientConnection
289 | on: server accept
290 | onDisconnect: [:cl | self handleDisconnected: cl]
291 | parent: self.
292 | ^client
293 | ]
294 |
295 | authenticates: aCreds with: aMagic [
296 |
297 | ^(creds with: aMagic) = aCreds
298 | ]
299 |
300 | handleDisconnected: aClient [
301 |
302 | "This method is delegated to a client session in a block"
303 | clientsMutex critical: [self clients remove: aClient]
304 | ]
305 |
306 | startOn: aPort creds: aCreds [
307 |
308 | clientsMutex := Semaphore forMutualExclusion.
309 | server := TCP.ServerSocket port: aPort.
310 | creds := aCreds.
311 |
312 | ShampooTranscript add: self.
313 |
314 | [server isOpen] whileTrue:
315 | [| conn |
316 | [server waitForConnection]
317 | ifError: [^self inform:
318 | 'Shampoo: failed to listen for incoming connections'].
319 | conn := self acceptedClient.
320 | conn ifNotNil:
321 | [clientsMutex critical: [self clients add: conn].
322 | [conn go] fork]]
323 | ]
324 |
325 | echo: aString [
326 |
327 | clientsMutex critical:
328 | [self clients do: [:each | each echo: aString]]
329 | ]
330 |
331 | ShampooServer class >> startOn: aPort login: aLogin pass: aPass [
332 |
333 | ^self startOn: aPort creds: (AuthInfo login: aLogin pass: aPass)
334 | ]
335 |
336 | ShampooServer class >> closeAll [
337 |
338 | serversMutex critical:
339 | [servers values do: [:each | each close].
340 | servers empty]
341 | ]
342 |
343 | ShampooServer class >> closeOn: aPort [
344 |
345 | serversMutex critical:
346 | [| srv |
347 | srv := servers at: aPort ifAbsent: [^nil].
348 | servers removeKey: aPort.
349 | srv close]
350 | ]
351 |
352 | ShampooServer class >> startOn: aPort creds: aCreds [
353 |
354 | serversMutex critical:
355 | [(servers includesKey: aPort) ifFalse:
356 | [| srv |
357 | srv := self new.
358 | servers at: aPort put: srv.
359 | srv proc: [srv startOn: aPort creds: aCreds] fork]]
360 | ]
361 |
362 | ShampooServer class >> update: aspect [
363 |
364 | aspect == #aboutToSnapshot ifTrue: [^self broadcast: #suspend ].
365 | aspect == #finishedSnapshot ifTrue: [^self broadcast: #resume ].
366 | aspect == #returnFromSnapshot ifTrue: [^self broadcast: #restart ].
367 | ]
368 |
369 | ShampooServer class >> initialize [
370 |
371 | ObjectMemory addDependent: self
372 | ]
373 |
374 | ShampooServer class >> broadcast: aMessage [
375 |
376 | serversMutex critical:
377 | [servers values do: [:srv | srv perform: aMessage]]
378 | ]
379 |
380 | suspend [
381 |
382 | self closeConnections.
383 | super suspend.
384 | ]
385 |
386 | restart [
387 |
388 | self close.
389 | self proc: [self startOn: server port creds: creds] fork.
390 | ]
391 |
392 | closeConnections [
393 |
394 | clientsMutex critical:
395 | [self clients copy do: [:cl | cl close]].
396 | "And that's all. Every client connection
397 | will be removed from the set in the
398 | ShampooServer>>handleDisconnected:."
399 | ]
400 |
401 | close [
402 |
403 | [server close] ifError: [].
404 | self closeConnections.
405 | ShampooTranscript remove: self
406 | ]
407 |
408 | inform: aMessage [
409 |
410 | ShampooTranscript fallback: aMessage.
411 | ^nil
412 | ]
413 | ]
414 | ]
415 |
416 | Eval [
417 | Shampoo.ShampooTranscript initialize.
418 | Shampoo.ShampooServer initialize.
419 | ]
420 |
--------------------------------------------------------------------------------
/responses.st:
--------------------------------------------------------------------------------
1 | Namespace current: Shampoo [
2 |
3 | Object subclass: Response [
4 |
5 |
8 |
9 | | id |
10 |
11 | Response class >> id: anInteger [
12 |
13 | ^(self new)
14 | id: anInteger;
15 | yourself
16 | ]
17 |
18 | id: anInteger [
19 |
20 | id := anInteger
21 | ]
22 |
23 | type [
24 |
25 | ^self shouldNotImplement
26 | ]
27 |
28 | do: aBlock [
29 | "Evaluate a one-argument block, pass self to it.
30 | This method is actually a kludge and it will be probably
31 | removed later"
32 |
33 |
34 | ^aBlock value: self
35 | ]
36 |
37 | asXML [
38 |
39 | ^(ShampooXML.ShNode tagname: 'response')
40 | addAttribute: 'id' value: id asString;
41 | addAttribute: 'type' value: self type;
42 | yourself
43 | ]
44 | ]
45 |
46 | Response subclass: ServerInfoResponse [
47 |
48 |
50 |
51 | type [
52 |
53 | ^'Info'
54 | ]
55 |
56 | asXML [
57 |
58 | ^(super asXML)
59 | addNode: (ShampooXML.ShText text: Smalltalk version);
60 | yourself
61 | ]
62 | ]
63 |
64 | Response subclass: OperationalResponse [
65 |
66 |
68 |
69 | | success message |
70 |
71 | OperationalResponse class >> success: anInteger [
72 |
73 | ^(OperationalResponse id: anInteger)
74 | success: true;
75 | yourself
76 | ]
77 |
78 | OperationalResponse class >> failure: anInteger [
79 |
80 | ^(OperationalResponse id: anInteger)
81 | success: false;
82 | yourself
83 | ]
84 |
85 | OperationalResponse class >> failure: anInteger with: anError [
86 |
87 | ^self failure: anInteger text: anError messageText
88 | ]
89 |
90 | OperationalResponse class >> failure: anInteger text: aString [
91 |
92 | ^(OperationalResponse id: anInteger)
93 | success: false;
94 | message: aString;
95 | yourself
96 | ]
97 |
98 | statusString [
99 |
100 | ^success ifTrue: ['success'] ifFalse: ['failure']
101 | ]
102 |
103 | success: aBoolean [
104 |
105 | success := aBoolean
106 | ]
107 |
108 | message: aString [
109 |
110 | message := aString
111 | ]
112 |
113 | type [
114 |
115 | ^'OperationalResponse'
116 | ]
117 |
118 | asXML [
119 |
120 | | r |
121 | r := super asXML.
122 | r addAttribute: 'status' value: self statusString.
123 | message ifNotNil:
124 | [r addNode: (ShampooXML.ShText text: message)].
125 | ^r
126 | ]
127 | ]
128 |
129 | Decorator subclass: CollectionResponseDecorator [
130 |
131 |
137 |
138 | | items |
139 |
140 | items [
141 |
142 | ^items ifNil: [items := Dictionary new]
143 | ]
144 |
145 | itemsAt: itemTypeString put: anArray [
146 |
147 | self items at: itemTypeString put: anArray copy
148 | ]
149 |
150 | asXML [
151 |
152 | | root |
153 | root := self underlyingObject asXML.
154 |
155 | self items keysAndValuesDo:
156 | [:key :values | values do: [:each |
157 | root addNode: ((ShampooXML.ShNode tagname: key asString)
158 | addNode: (ShampooXML.ShText text: each);
159 | yourself)]].
160 |
161 | ^root
162 | ]
163 |
164 | do: aBlock [
165 | "CollectionResponseDecorator usually wraps Response objects.
166 | Response objects partly understand the collection protocol,
167 | especially a #do: message. This wrapper does not proxy this
168 | message to the underlying object."
169 |
170 | ^aBlock value: self
171 | ]
172 | ]
173 |
174 | Response subclass: NamespacesResponse [
175 |
176 |
177 |
178 | NamespacesResponse class >> id: anInteger names: anArray [
179 |
180 | ^(CollectionResponseDecorator on: (self id: anInteger))
181 | itemsAt: 'namespace' put: anArray;
182 | yourself
183 | ]
184 |
185 | type [
186 |
187 | ^'Namespaces'
188 | ]
189 | ]
190 |
191 | Response subclass: ClassesResponse [
192 |
193 |
195 |
196 | ClassesResponse class >> id: anInteger names: anArray [
197 |
198 | ^(CollectionResponseDecorator on: (self id: anInteger))
199 | itemsAt: 'class' put: anArray;
200 | yourself
201 | ]
202 |
203 | type [
204 |
205 | ^'Classes'
206 | ]
207 | ]
208 |
209 | Response subclass: ClassInfoResponse [
210 |
211 |
213 |
214 | | class attrs |
215 |
216 | ClassInfoResponse class >> id: anInteger class: aClass [
217 |
218 | ^self id: anInteger class: aClass attrs: nil
219 | ]
220 |
221 | ClassInfoResponse class >> id: anInteger class: aClass attrs: anArray [
222 |
223 | ^(self id: anInteger)
224 | class: aClass attrs: anArray
225 | yourself
226 | ]
227 |
228 | class: aClass attrs: anArray [
229 |
230 | class := aClass.
231 | anArray isNil ifFalse: [attrs := Dictionary from: anArray].
232 | ]
233 |
234 | asXML [
235 |
236 | | r |
237 | r := super asXML.
238 | r addAttribute: 'class' value: class.
239 | attrs ifNotNil:
240 | [:dict |
241 | dict keysAndValuesDo:
242 | [:key :value | r addAttribute: key value: value]].
243 | ^r
244 | ]
245 | ]
246 |
247 | ClassInfoResponse subclass: ClassResponse [
248 |
249 |
250 |
251 | ClassResponse class >>
252 | id: anInteger class: aClass superclass: aSuperClass
253 | instvars: anInstVars classvars: aClassVars poolvars: aPoolDicts
254 | category: aString [
255 |
256 | | obj |
257 | obj := self
258 | id: anInteger
259 | class: aClass
260 | attrs: {'superclass' -> aSuperClass.
261 | 'category' -> aString}.
262 | ^(CollectionResponseDecorator on: obj)
263 | itemsAt: 'instvar' put: anInstVars;
264 | itemsAt: 'classvar' put: aClassVars;
265 | itemsAt: 'poolvar' put: aPoolDicts;
266 | yourself
267 | ]
268 |
269 | type [
270 |
271 | ^'Class'
272 | ]
273 | ]
274 |
275 | ClassInfoResponse subclass: MethodCategoriesResponse [
276 |
277 |
278 |
279 | MethodCategoriesResponse class >> id: anInteger class: aClass categories: anArray [
280 |
281 | ^(CollectionResponseDecorator on: (self id: anInteger class: aClass))
282 | itemsAt: 'category' put: anArray;
283 | yourself
284 | ]
285 |
286 | type [
287 |
288 | ^'Categories'
289 | ]
290 | ]
291 |
292 | ClassInfoResponse subclass: MethodsResponse [
293 |
294 |
296 |
297 | MethodsResponse class >> id: anInteger class: aClass methods: anArray [
298 |
299 | ^(CollectionResponseDecorator on: (self id: anInteger class: aClass))
300 | itemsAt: 'method' put: anArray;
301 | yourself
302 | ]
303 |
304 | type [
305 |
306 | ^'Methods'
307 | ]
308 | ]
309 |
310 | ClassInfoResponse subclass: MethodResponse [
311 |
312 |
314 |
315 | | methodName methodSource |
316 |
317 | MethodResponse class >> id: anInteger class: aClass method: aNameString source: aSourceString [
318 |
319 | ^(self id: anInteger class: aClass)
320 | method: aNameString methodSource: aSourceString;
321 | yourself
322 | ]
323 |
324 | method: aNameString methodSource: aSourceString [
325 |
326 | methodName := aNameString.
327 | methodSource := PrettyPrinter prettifyMethod: aSourceString
328 | ]
329 |
330 | type [
331 |
332 | ^'MethodSource'
333 | ]
334 |
335 | asXML [
336 |
337 | ^(super asXML)
338 | addAttribute: 'method' value: methodName;
339 | addNode: (ShampooXML.ShText text: methodSource);
340 | yourself
341 | ]
342 | ]
343 |
344 | Response subclass: PrintItResponse [
345 |
346 |
347 |
348 | | value |
349 |
350 | PrintItResponse class >> id: anInteger value: anObject [
351 |
352 | ^(self id: anInteger)
353 | value: anObject;
354 | yourself
355 | ]
356 |
357 | value: anObject [
358 |
359 | value := anObject
360 | ]
361 |
362 | value [
363 |
364 | ^value
365 | ]
366 |
367 | type [
368 |
369 | ^'PrintIt'
370 | ]
371 |
372 | asXML [
373 |
374 | ^(super asXML)
375 | addNode: (ShampooXML.ShText text: value printString);
376 | yourself
377 | ]
378 | ]
379 |
380 | Response subclass: EchoResponse [
381 |
382 |