Merge lp:~widelands-dev/widelands-metaserver/ircbridge into lp:widelands-metaserver
- ircbridge
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 47 | ||||
Proposed branch: | lp:~widelands-dev/widelands-metaserver/ircbridge | ||||
Merge into: | lp:widelands-metaserver | ||||
Diff against target: |
291 lines (+127/-15) 6 files modified
.bzrignore (+1/-0) wlms/client.go (+1/-0) wlms/ircbridge.go (+71/-0) wlms/main.go (+14/-3) wlms/server.go (+34/-11) wlms/server_test.go (+6/-1) |
||||
To merge this branch: | bzr merge lp:~widelands-dev/widelands-metaserver/ircbridge | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tino | Needs Resubmitting | ||
SirVer | Needs Fixing | ||
Review via email: mp+203277@code.launchpad.net |
Commit message
Description of the change
A first draft of a irce bridge. Lets the metaserver connect to a irc channel an mirrors messages between the channel and the metaserver lobby.
SirVer (sirver) wrote : | # |
SirVer (sirver) : | # |
Tino (tino79) wrote : | # |
Thanks for the comments, of course i only wanted a review, not a merge in the current state.
This are my first steps with go, so i am just glad that it works at all ;).
- i will add tests
- the irc library handles disconnects itself, and because of the use of a callback function for irc->lobby it is already decoupled. I disconnected from the internet to test and it automatically reconnected and the lobby was still usable. But i will look into using a routine and buffers...
- pointer recievers:
Here i am really unsure, but i've found:
A method is a function with an implicit first argument, called a receiver.
So when you call a method:
- if the receiver is a pointer to int, it copies the pointer to int.
- if the receiver is a struct, it copies the struct.
- if the receiver is a pointer to struct, it copies the pointer to struct.
So even if i don't want to modify the struct i would think avoiding a copy operation by passing a pointer to the struct should be faster? But here speaks only 1 day go experience... ;)
SirVer (sirver) wrote : | # |
> But i will look into using a routine and buffers...
I understand your comment, but I still think it is a good idea. First, because it is more idiomatic go (as far as I see it right now :)) and secondly goroutines and channels are essential building blocks that somehow set the language apart - you'll find them really interesting I think.
> So even if i don't want to modify the struct i would think avoiding a copy operation by passing a pointer to the struct should be faster?
While this is correct, go optimizes these very strongly. Sofar not using a pointer has not been a performance bottleneck in go code for me. And not using a pointer signifies to the programmer that the method will not change the receiver - which is nice documentation. So only use a pointer receiver when you need to (i.e. when you need to modify the receiver) or when you measured that it impacts performance by benchmarking.
- 49. By Tino
-
revert to non pointer recievers when the struct does not get manipulated
- 50. By Tino
-
read irc configuration from file
- 51. By Tino
-
IRCBridge "interfaced", does compile and run again
- 52. By Tino
-
pushing messages to irc via channel
- 53. By Tino
-
go channels 2way irc<->lobby
- 54. By Tino
-
channel refactoring
tests are ok again (no testing of irc functionality right now) - 55. By Tino
-
non blocking sending
Tino (tino79) wrote : | # |
Ok, channels are in place.
Next up are tests, but first i need to understand go tests. The current tests starts dozens of server instances in a few seconds, i need to use only one irc bridge instance for all tests or have to implement my own tests with active irc bridge.
SirVer (sirver) wrote : | # |
I do not fully understand your question.
If you only want to test the behavior of the server you can just hand in two channels and fake messages on those in your server tests (i.e. no IRC Bridge is created at all). I think this is the most reasonable approach to take since you cannot rely on IRC being up when you want to test your scripts.
You can also choose not to test your new implementations, but you must at least change the setup procedure so that not necessarily an IRC connection is made when a server is created.
SirVer (sirver) wrote : | # |
And progress on this? Anything I can help you with? If you are stuck we can also remote pair on the code for a bit, maybe we come up with something together.
Tino (tino79) wrote : | # |
The current state is:
- it is working, communication between ircbridge and metaserver via nonblocking channels
- tests are working, result ok
- no tests regarding the ircbridge itself or communication implemented
I am a bit busy atm, i think i won't have time to implement further test before next week.
SirVer (sirver) wrote : | # |
no pressure - just wondered if you still want to work on this or if it should be merged around now.
Tino (tino79) wrote : | # |
Ok, perhaps we can merge now?
I does work(tm), only the testcase i've drafted seems to do nothing?
SirVer (sirver) wrote : | # |
looks good to me. I merged and deployed on the server - thanks for your work on this!
Preview Diff
1 | === added file '.bzrignore' |
2 | --- .bzrignore 1970-01-01 00:00:00 +0000 |
3 | +++ .bzrignore 2014-03-17 19:05:27 +0000 |
4 | @@ -0,0 +1,1 @@ |
5 | +*.exe |
6 | |
7 | === modified file 'wlms/client.go' |
8 | --- wlms/client.go 2014-02-03 21:14:19 +0000 |
9 | +++ wlms/client.go 2014-03-17 19:05:27 +0000 |
10 | @@ -277,6 +277,7 @@ |
11 | |
12 | if len(receiver) == 0 { |
13 | server.BroadcastToConnectedClients("CHAT", client.Name(), message, "public") |
14 | + server.BroadcastToIrc(client.Name() + ": " + message) |
15 | } else { |
16 | recv_client := server.HasClient(receiver) |
17 | if recv_client != nil { |
18 | |
19 | === added file 'wlms/ircbridge.go' |
20 | --- wlms/ircbridge.go 1970-01-01 00:00:00 +0000 |
21 | +++ wlms/ircbridge.go 2014-03-17 19:05:27 +0000 |
22 | @@ -0,0 +1,71 @@ |
23 | +package main |
24 | + |
25 | +import ( |
26 | + "log" |
27 | + |
28 | + "github.com/thoj/go-ircevent" |
29 | +) |
30 | + |
31 | +type IRCBridger interface { |
32 | + Connect(chan Message, chan Message) bool |
33 | + Quit() |
34 | +} |
35 | + |
36 | +type IRCBridge struct { |
37 | + connection *irc.Connection |
38 | + nick, user, channel, server string |
39 | + useTLS bool |
40 | +} |
41 | + |
42 | +type Message struct { |
43 | + message, nick string |
44 | +} |
45 | + |
46 | +func NewIRCBridge(server, realname, nickname, channel string, tls bool) *IRCBridge { |
47 | + return &IRCBridge{ |
48 | + server: server, |
49 | + user: realname, |
50 | + nick: nickname, |
51 | + channel: channel, |
52 | + useTLS: tls, |
53 | + } |
54 | +} |
55 | + |
56 | +func (bridge *IRCBridge) Connect(messagesIn chan Message, messagesOut chan Message) bool { |
57 | + //Create new connection |
58 | + bridge.connection = irc.IRC(bridge.nick, bridge.user) |
59 | + //Set options |
60 | + bridge.connection.UseTLS = bridge.useTLS |
61 | + //connection.TLSOptions //set ssl options |
62 | + //connection.Password = "[server password]" |
63 | + //Commands |
64 | + err := bridge.connection.Connect(bridge.server) //Connect to server |
65 | + if err != nil { |
66 | + log.Fatal("Can't connect %s", bridge.server) |
67 | + return false |
68 | + } |
69 | + bridge.connection.Join(bridge.channel) |
70 | + bridge.connection.AddCallback("PRIVMSG", func(event *irc.Event) { |
71 | + //e.Message contains the message |
72 | + //e.Nick Contains the sender |
73 | + //e.Arguments[0] Contains the channel |
74 | + select { |
75 | + case messagesOut <- Message{nick: event.Nick, |
76 | + message: event.Message(), |
77 | + }: |
78 | + default: |
79 | + log.Println("Message Queue full.") |
80 | + } |
81 | + |
82 | + }) |
83 | + go func() { |
84 | + for m := range messagesIn { |
85 | + bridge.connection.Privmsg(bridge.channel, m.message) |
86 | + } |
87 | + }() |
88 | + return true |
89 | +} |
90 | + |
91 | +func (bridge *IRCBridge) Quit() { |
92 | + bridge.connection.Quit() |
93 | +} |
94 | |
95 | === modified file 'wlms/main.go' |
96 | --- wlms/main.go 2013-12-30 12:20:31 +0000 |
97 | +++ wlms/main.go 2014-03-17 19:05:27 +0000 |
98 | @@ -8,7 +8,8 @@ |
99 | ) |
100 | |
101 | type Config struct { |
102 | - Database, User, Password, Table string |
103 | + Database, User, Password, Table, Backend, IRCServer, Nickname, Realname, Channel string |
104 | + UseTLS bool |
105 | } |
106 | |
107 | func (l *Config) ConfigFrom(path string) error { |
108 | @@ -25,16 +26,26 @@ |
109 | flag.Parse() |
110 | |
111 | var db UserDb |
112 | + var ircbridge IRCBridger |
113 | if config != "" { |
114 | var cfg Config |
115 | if err := cfg.ConfigFrom(config); err != nil { |
116 | log.Fatalf("Could not parse config file: %v", err) |
117 | } |
118 | - db = NewMySqlDatabase(cfg.Database, cfg.User, cfg.Password, cfg.Table) |
119 | + if cfg.Backend == "mysql" { |
120 | + db = NewMySqlDatabase(cfg.Database, cfg.User, cfg.Password, cfg.Table) |
121 | + } else { |
122 | + db = NewInMemoryDb() |
123 | + } |
124 | + ircbridge = NewIRCBridge(cfg.IRCServer, cfg.Realname, cfg.Nickname, cfg.Channel, cfg.UseTLS) |
125 | } else { |
126 | db = NewInMemoryDb() |
127 | } |
128 | defer db.Close() |
129 | |
130 | - RunServer(db) |
131 | + messagesToIrc := make(chan Message, 50) |
132 | + messagesToLobby := make(chan Message, 50) |
133 | + ircbridge.Connect(messagesToIrc, messagesToLobby) |
134 | + RunServer(db, messagesToLobby, messagesToIrc) |
135 | + |
136 | } |
137 | |
138 | === modified file 'wlms/server.go' |
139 | --- wlms/server.go 2014-02-08 20:48:04 +0000 |
140 | +++ wlms/server.go 2014-03-17 19:05:27 +0000 |
141 | @@ -33,10 +33,11 @@ |
142 | gamePingTimeout time.Duration |
143 | |
144 | clientForgetTimeout time.Duration |
145 | - gamePingerFactory GampPingerFactory |
146 | + gamePingerFactory GamePingerFactory |
147 | + messagesOut chan Message |
148 | } |
149 | |
150 | -type GampPingerFactory interface { |
151 | +type GamePingerFactory interface { |
152 | New(client *Client, timeout time.Duration) *GamePinger |
153 | } |
154 | |
155 | @@ -100,6 +101,7 @@ |
156 | } |
157 | |
158 | func (s *Server) AddClient(client *Client) { |
159 | + s.BroadcastToIrc(client.Name() + " has joined the lobby.") |
160 | s.clients.PushBack(client) |
161 | } |
162 | |
163 | @@ -121,11 +123,15 @@ |
164 | if e.Value.(*Client) == client { |
165 | log.Printf("Removing client %s.", client.Name()) |
166 | s.clients.Remove(e) |
167 | + s.messagesOut <- Message{ |
168 | + message: client.Name() + " has left the lobby.", |
169 | + nick: client.Name(), |
170 | + } |
171 | } |
172 | } |
173 | } |
174 | |
175 | -func (s *Server) HasClient(name string) *Client { |
176 | +func (s Server) HasClient(name string) *Client { |
177 | for e := s.clients.Front(); e != nil; e = e.Next() { |
178 | client := e.Value.(*Client) |
179 | if client.Name() == name { |
180 | @@ -135,7 +141,7 @@ |
181 | return nil |
182 | } |
183 | |
184 | -func (s *Server) NrActiveClients() int { |
185 | +func (s Server) NrActiveClients() int { |
186 | count := 0 |
187 | for e := s.clients.Front(); e != nil; e = e.Next() { |
188 | if e.Value.(*Client).State() == CONNECTED { |
189 | @@ -158,6 +164,7 @@ |
190 | func (s *Server) AddGame(game *Game) { |
191 | s.games.PushBack(game) |
192 | s.BroadcastToConnectedClients("GAMES_UPDATE") |
193 | + s.BroadcastToIrc("A new game " + game.Name() + " was opened by " + game.Host()) |
194 | } |
195 | |
196 | func (s *Server) RemoveGame(game *Game) { |
197 | @@ -170,7 +177,7 @@ |
198 | } |
199 | } |
200 | |
201 | -func (s *Server) HasGame(name string) *Game { |
202 | +func (s Server) HasGame(name string) *Game { |
203 | for e := s.games.Front(); e != nil; e = e.Next() { |
204 | game := e.Value.(*Game) |
205 | if game.Name() == name { |
206 | @@ -180,7 +187,7 @@ |
207 | return nil |
208 | } |
209 | |
210 | -func (s *Server) NrGames() int { |
211 | +func (s Server) NrGames() int { |
212 | return s.games.Len() |
213 | } |
214 | |
215 | @@ -199,7 +206,18 @@ |
216 | } |
217 | } |
218 | |
219 | -func RunServer(db UserDb) { |
220 | +func (s Server) BroadcastToIrc(message string) { |
221 | + select { |
222 | + case s.messagesOut <- Message{ |
223 | + message: message, |
224 | + }: |
225 | + default: |
226 | + log.Println("Message Queue full.") |
227 | + } |
228 | + |
229 | +} |
230 | + |
231 | +func RunServer(db UserDb, messagesIn chan Message, messagesOut chan Message) { |
232 | ln, err := net.Listen("tcp", ":7395") |
233 | if err != nil { |
234 | log.Fatal(err) |
235 | @@ -216,8 +234,7 @@ |
236 | C <- conn |
237 | } |
238 | }() |
239 | - |
240 | - CreateServerUsing(C, db).WaitTillShutdown() |
241 | + CreateServerUsing(C, db, messagesIn, messagesOut).WaitTillShutdown() |
242 | } |
243 | |
244 | type RealGamePingerFactory struct { |
245 | @@ -255,11 +272,11 @@ |
246 | return pinger |
247 | } |
248 | |
249 | -func (server *Server) InjectGamePingerFactory(gpf GampPingerFactory) { |
250 | +func (server *Server) InjectGamePingerFactory(gpf GamePingerFactory) { |
251 | server.gamePingerFactory = gpf |
252 | } |
253 | |
254 | -func CreateServerUsing(acceptedConnections chan ReadWriteCloserWithIp, db UserDb) *Server { |
255 | +func CreateServerUsing(acceptedConnections chan ReadWriteCloserWithIp, db UserDb, messagesIn chan Message, messagesOut chan Message) *Server { |
256 | server := &Server{ |
257 | acceptedConnections: acceptedConnections, |
258 | shutdownServer: make(chan bool), |
259 | @@ -272,8 +289,14 @@ |
260 | pingCycleTime: time.Second * 15, |
261 | clientSendingTimeout: time.Minute * 2, |
262 | clientForgetTimeout: time.Minute * 5, |
263 | + messagesOut: messagesOut, |
264 | } |
265 | server.gamePingerFactory = RealGamePingerFactory{server} |
266 | + go func() { |
267 | + for m := range messagesIn { |
268 | + server.BroadcastToConnectedClients("CHAT", m.nick+"(IRC)", m.message, "public") |
269 | + } |
270 | + }() |
271 | go server.mainLoop() |
272 | return server |
273 | } |
274 | |
275 | === modified file 'wlms/server_test.go' |
276 | --- wlms/server_test.go 2014-02-08 20:48:04 +0000 |
277 | +++ wlms/server_test.go 2014-03-17 19:05:27 +0000 |
278 | @@ -40,7 +40,12 @@ |
279 | cons[i] = NewFakeConn(c) |
280 | acceptingConnections <- cons[i] |
281 | } |
282 | - return CreateServerUsing(acceptingConnections, db), cons |
283 | + |
284 | + //irc := NewIRCBridge("chat.freenode.net:7000", "wltest", "wltest", "#widelands-test", true) |
285 | + toIrc := make(chan Message, 100) |
286 | + fromIrc := make(chan Message, 100) |
287 | + //irc.Connect(toIrc, fromIrc) |
288 | + return CreateServerUsing(acceptingConnections, db, fromIrc, toIrc), cons |
289 | } |
290 | |
291 | type Matching string |
Nice. That is a really good idea and I think the implementation looks reasonable too. It is a bit drafty still, though (see comments below). Was this the first time you did something with go? How did you like it?
Some comments:
- func NewIRCBridge() IRCBridge should read its configuration from the same json file where we read the data from right now too (see main.go).
- I think the design is a little fragile - if something goes wrong with the IRC connection the whole metaserver comes down. How about running the IRC bridge in a go routine that gets a (buffered) channel of stuff to send and also sends stuff into a channel that the metaserver then displays? That way if the bridge is disconnected it can just discard the messages and the whole design is a bit decoupled. Also you can fake some IRC messages in a test and test the whole thing.
- +func (s *Server) Motd() string - I do not agree with this change. When a method is not changing a struct, it should not be a pointer receiver. There are only a few exceptions to this rule as far as I understood go so far (not an expert by any means).
- Please add some tests. Feel free to add end to end tests, there are already a few in the metaserver.