agoo 2.15.9 → 2.15.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/ext/agoo/rserver.c CHANGED
@@ -133,7 +133,7 @@ configure(agooErr err, int port, const char *root, VALUE options) {
133
133
  rb_raise(rb_eArgError, "thread_count must be between 1 and %d.", MAX_WORKERS);
134
134
  }
135
135
  }
136
- the_rserver.forker = rb_hash_lookup(options, ID2SYM(rb_intern("fork_wrap")));
136
+ the_rserver.forker = rb_hash_lookup(options, ID2SYM(rb_intern("fork_wrap")));
137
137
 
138
138
  if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("poll_timeout"))))) {
139
139
  double timeout = rb_num2dbl(v);
@@ -324,16 +324,21 @@ configure(agooErr err, int port, const char *root, VALUE options) {
324
324
  static VALUE
325
325
  rserver_init(int argc, VALUE *argv, VALUE self) {
326
326
  struct _agooErr err = AGOO_ERR_INIT;
327
- int port;
328
- const char *root;
329
- VALUE options = Qnil;
327
+ int port;
328
+ const char *root = NULL;
329
+ VALUE options = Qnil;
330
330
 
331
331
  if (argc < 2 || 3 < argc) {
332
332
  rb_raise(rb_eArgError, "Wrong number of arguments to Agoo::Server.configure.");
333
333
  }
334
334
  port = FIX2INT(argv[0]);
335
- rb_check_type(argv[1], T_STRING);
336
- root = StringValuePtr(argv[1]);
335
+ if (Qnil != argv[1]) {
336
+ rb_check_type(argv[1], T_STRING);
337
+ root = StringValuePtr(argv[1]);
338
+ if ('\0' == *root) {
339
+ root = NULL;
340
+ }
341
+ }
337
342
  if (3 <= argc) {
338
343
  options = argv[2];
339
344
  }
@@ -834,13 +839,13 @@ rserver_start(VALUE self) {
834
839
  if (AGOO_ERR_OK != setup_listen(&err)) {
835
840
  rb_raise(rb_eIOError, "%s", err.msg);
836
841
  }
837
- if (1 < the_rserver.worker_cnt && the_rserver.forker != Qnil) {
838
- ID before = rb_intern("before");
842
+ if (1 < the_rserver.worker_cnt && the_rserver.forker != Qnil) {
843
+ ID before = rb_intern("before");
839
844
 
840
- if (rb_respond_to(the_rserver.forker, before)) {
841
- rb_funcall(the_rserver.forker, before, 0);
842
- }
843
- }
845
+ if (rb_respond_to(the_rserver.forker, before)) {
846
+ rb_funcall(the_rserver.forker, before, 0);
847
+ }
848
+ }
844
849
  for (i = 1; i < the_rserver.worker_cnt; i++) {
845
850
  VALUE rpid = rb_funcall(rb_cObject, rb_intern("fork"), 0);
846
851
 
@@ -861,9 +866,9 @@ rserver_start(VALUE self) {
861
866
  the_rserver.worker_pids[i] = pid;
862
867
  }
863
868
  }
864
- if (1 < the_rserver.worker_cnt && the_rserver.forker != Qnil && rb_respond_to(the_rserver.forker, after)) {
865
- rb_funcall(the_rserver.forker, after, 0);
866
- }
869
+ if (1 < the_rserver.worker_cnt && the_rserver.forker != Qnil && rb_respond_to(the_rserver.forker, after)) {
870
+ rb_funcall(the_rserver.forker, after, 0);
871
+ }
867
872
  if (AGOO_ERR_OK != agoo_server_start(&err, "Agoo", StringValuePtr(v))) {
868
873
  rb_raise(rb_eStandardError, "%s", err.msg);
869
874
  }
@@ -983,6 +988,10 @@ rserver_shutdown(VALUE self) {
983
988
  * Registers a handler for the HTTP method and path pattern specified. The
984
989
  * path pattern follows glob like rules in that a single * matches a single
985
990
  * token bounded by the `/` character and a double ** matches all remaining.
991
+ * The handler must resolve to an object than responds to "on_request" for the
992
+ * basic handler, "call" for a Rack handler, or for a WAB handler (see
993
+ * https://github.com/ohler55/wabur), "create", "read", "update", and
994
+ * "delete". The name of a class will resolve to the class itself.
986
995
  */
987
996
  static VALUE
988
997
  handle(VALUE self, VALUE method, VALUE pattern, VALUE handler) {
data/lib/agoo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
2
  module Agoo
3
3
  # Agoo version.
4
- VERSION = '2.15.9'
4
+ VERSION = '2.15.11'
5
5
  end
data/misc/flymd.md ADDED
@@ -0,0 +1,581 @@
1
+ # A New Rack Push
2
+
3
+ Realtime websites are essential for some domains. A web site displaying stock
4
+ prices or race progress would not be very useful if the displays were not kept
5
+ up to date. It is of course possible to use constant refreshes or polling but
6
+ that puts a heavy demand on the server and it is not real time.
7
+
8
+ Fortunately there is technology to push data to a web page. WebSocket and SSE
9
+ provide a means to push data to a web page. Coupled with Javascript pages can
10
+ be dynamic and display data as it changes.
11
+
12
+ Up until now, Ruby support for WebSockets and SSE was very cumbersome,
13
+ resulting in poor performance and higher memory bloat despite well authored
14
+ gems such as Faye and ActionCable.
15
+
16
+ Using Rack as an example it was possible to hijack a connection and manage all
17
+ the interactions using raw IO calls but that is a task that is not for the
18
+ faint at heart. It is a non trivial exercise and it requires duplication of
19
+ the network handling logic. Basically it's almost like running two servers
20
+ instead of one.
21
+
22
+ Won't it be nice to have a simple way of supporting push with Ruby? Something
23
+ that works with the server rather than against it? Something that avoids the
24
+ need to run another "server" on the same process?
25
+
26
+ There is a proposal to extend the Rack spec to include an option for WebSocket
27
+ and SSE. Currently two servers support that proposed
28
+ extension. [Agoo](https://https://github.com/ohler55/agoo) and
29
+ [Iodine](https://github.com/boazsegev/iodine) both high performance web server
30
+ gems that support push and implement the [proposed Rack
31
+ extension](https://github.com/rack/rack/pull/1272). A copy of the proposed
32
+ spec in HTML format is [here](http://www.ohler.com/agoo/rack/file.SPEC.html).
33
+
34
+ ## Simple Callback Design
35
+
36
+ The Rack extension proposal uses a simple callback design. This design
37
+ replaces the need for socket hijacking, removes the hassle of socket
38
+ management and decouples the application from the network protocol layer.
39
+
40
+ All Rack handlers already have a `#call(env)` method that processes HTTP
41
+ requests. If the `env` argument to that call includes a non `nil` value for
42
+ `env['rack.upgrade?']` then the handle is expected to upgrade to a push
43
+ connection that is either WebSocket or SSE.
44
+
45
+ Acceptance of the connection upgrade is implemented by setting
46
+ `env['rack.upgrade']` to a new push handler. The push handler can implement
47
+ methods `#on_open`, `#on_close`, `#on_message`, `#on_drained`, and
48
+ `#on_shutdown`. These are all optional. A `client` object is passed as the
49
+ first argument to each method. This `client` can be used to check connection
50
+ status as well as writing messages to the connection.
51
+
52
+ The `client` has methods `#write(msg)`, `#pending`, `#open?`, and `#close`
53
+ method. The `#write(msg)` method is used to push data to web pages. ThefLyMd-mAkEr
54
+ details are handled by the server. Just call write and data appears at
55
+ browser.
56
+
57
+ ## Examples
58
+
59
+ The example is a bit more than a hello world but only enough to make it
60
+ interesting. A browser is used to connect to a Rack server that runs a clock,
61
+ On each tick of the clock the time is sent to the browser. Either an SSE and a
62
+ WebSocket page can be used. That means you can connect with your mobile device
63
+ using SSE. Try it to see how easy it is.
64
+
65
+ First some web pages will be needed. Lets call them `websocket.html` and
66
+ `sse.html`. Notice how similar they look.
67
+
68
+ ```html
69
+ <!-- websocket.html -->
70
+ <html>
71
+ <body>
72
+ <p id="status"> ... </p>
73
+ <p id="message"> ... waiting ... </p>
74
+
75
+ <script type="text/javascript">
76
+ var sock;
77
+ var url = "ws://" + document.URL.split('/')[2] + '/upgrade'
78
+ if (typeof MozWebSocket != "undefined") {
79
+ sock = new MozWebSocket(url);
80
+ } else {
81
+ sock = new WebSocket(url);
82
+ }
83
+ sock.onopen = function() {
84
+ document.getElementById("status").textContent = "connected";
85
+ }
86
+ sock.onmessage = function(msg) {
87
+ document.getElementById("message").textContent = msg.data;
88
+ }
89
+ </script>
90
+ </body>
91
+ </html>
92
+ ```
93
+
94
+ ```html
95
+ <!-- sse.html -->
96
+ <html>
97
+ <body>
98
+ <p id="status"> ... </p>
99
+ <p id="message"> ... waiting ... </p>
100
+
101
+ <script type="text/javascript">
102
+ var src = new EventSource('upgrade');
103
+ src.onopen = function() {
104
+ document.getElementById('status').textContent = 'connected';
105
+ }
106
+ src.onerror = function() {
107
+ document.getElementById('status').textContent = 'not connected';
108
+ }
109
+ function doSet(e) {
110
+ document.getElementById("message").textContent = e.data;
111
+ }
112
+ src.addEventListener('msg', doSet, false);
113
+ </script>
114
+ </body>
115
+ </html>
116
+ ```
117
+
118
+ With those two files there are a couple ways to implement the Ruby side.
119
+
120
+ A pure, `rackup` example works with any of the web server gems the support the
121
+ proposed additions to the Rack spec. Currently there are two gems that support
122
+ the additions. They are [Agoo](https://github.com/ohler55/agoo) and
123
+ [Iodine](https://github.com/boazsegev/iodine). Both are high performance
124
+ servers with some features unique to both.
125
+
126
+ One example is for Agoo only to demonstrate the use of the Agoo demultiplexing
127
+ but is otherwise identical to the pure Rack example.
128
+
129
+ The third, pub-sub example is also Agoo specific but with a few minor changes
130
+ it would be compatible with Iodine as well. The pub-sub example is a minimal
131
+ example of how publish and subscribe can be used with Rack.
132
+
133
+ ### Pure Rack Example
134
+
135
+ Lets start with the server agnostic example that is just Rack with the
136
+ proposed extensions to the spec. Of course the file is named `config.ru`. The
137
+ file includes more than is necessary just work but some options methods are
138
+ added to make it clear when they are called. Since it is a 'hello world'
139
+ example there is the obligatory handled for returning that result. Comments
140
+ have been stripped out of the code in favor of explaining the code separately.
141
+
142
+ First the file then an explanation.
143
+
144
+ ```ruby
145
+ # config.ru
146
+ require 'rack'
147
+ #require 'agoo'
148
+
149
+ class Clock
150
+ def initialize()
151
+ @clients = []
152
+ @mutex = Mutex.new
153
+ end
154
+
155
+ def on_open(client)
156
+ puts "--- on_open"
157
+ @mutex.synchronize {
158
+ @clients << client
159
+ }
160
+ end
161
+
162
+ def on_close(client)
163
+ puts "--- on_close"
164
+ @mutex.synchronize {
165
+ @clients.delete(client)
166
+ }
167
+ end
168
+
169
+ def on_drained(client)
170
+ puts "--- on_drained"
171
+ end
172
+
173
+ def on_message(client, data)
174
+ puts "--- on_message #{data}"
175
+ client.write("echo: #{data}")
176
+ end
177
+
178
+ def start
179
+ loop do
180
+ now = Time.now
181
+ msg = "%02d:%02d:%02d" % [now.hour, now.min, now.sec]
182
+ @mutex.synchronize {
183
+ @clients.each { |c|
184
+ puts "--- write failed" unless c.write(msg)
185
+ }
186
+ }
187
+ sleep(1)
188
+ end
189
+ end
190
+ end
191
+
192
+ $clock = Clock.new
193
+
194
+ class Listen
195
+ def call(env)
196
+ path = env['SCRIPT_NAME'] + env['PATH_INFO']
197
+ case path
198
+ when '/'
199
+ return [ 200, { }, [ "hello world" ] ]
200
+ when '/websocket.html'
201
+ return [ 200, { }, [ File.read('websocket.html') ] ]
202
+ when '/sse.html'
203
+ return [ 200, { }, [ File.read('sse.html') ] ]
204
+ when '/upgrade'
205
+ unless env['rack.upgrade?'].nil?
206
+ env['rack.upgrade'] = $clock
207
+ return [ 200, { }, [ ] ]
208
+ end
209
+ end
210
+ [ 404, { }, [ ] ]
211
+ end
212
+ end
213
+
214
+ Thread.new { $clock.start }
215
+ run Listen.new
216
+ ```
217
+
218
+ #### Running
219
+
220
+ The example is run with this command.
221
+
222
+ ```
223
+ $ bundle exec rackup -r agoo -s agoo
224
+ ```
225
+
226
+ The server to use must be specified to use something other than the
227
+ default. Alternatively the commented out `require` for Agoo or an alternative
228
+ can be used by uncommenting the line.
229
+
230
+ ```ruby
231
+ require 'agoo'
232
+ ```
233
+
234
+ Once started open a browser and go to `http://localhost:9292/websocket.html`
235
+ or `http://localhost:9292/sse.html` and watch the page show 'connected' and
236
+ then showing the time as it changes every second.
237
+
238
+ #### Clock
239
+
240
+ The `Clock` class is the handler for upgraded calls. The `Listener` and
241
+ `Clock` could have been combined into a single class but for clarity they are
242
+ kept separate in this example.
243
+
244
+ The `Clock` object, `$clock` maintains a list of open connections. The default
245
+ configuration uses one thread but to be safe a mutex is used so that the same
246
+ example can be used with multiple threads configured. It's a good practice
247
+ when writing asynchronous callback code to assume there will be multiple threads
248
+ invoking the callbacks.
249
+
250
+ ```ruby
251
+ def initialize()
252
+ @clients = []
253
+ @mutex = Mutex.new
254
+ end
255
+ ```
256
+
257
+ The `#on_open` callback is called when a connection has been upgraded to a
258
+ WebSocket or SSE connection. That is the time to add the connection to the
259
+ client `client` to the `@clients` list is used to implement a broadcast write
260
+ or publish to all open connections. There are other options that will be
261
+ explored later.
262
+
263
+ ```ruby
264
+ def on_open(client)
265
+ puts "--- on_open"
266
+ @mutex.synchronize {
267
+ @clients << client
268
+ }
269
+ end
270
+ ```
271
+
272
+ When a connection is closed it should be removed the the `@clients` list. The
273
+ `#on_close` callback handles that. The other callbacks merely print out a
274
+ statement that they have been invoked so that it is easier to trace what is
275
+ going on.
276
+
277
+ ```ruby
278
+ def on_close(client)
279
+ puts "--- on_close"
280
+ @mutex.synchronize {
281
+ @clients.delete(client)
282
+ }
283
+ end
284
+ ```
285
+
286
+ Finally, the clock is going to publish the time to all connections. The
287
+ `#start` method starts the clock and then every second the time is published
288
+ by writing to each connection.
289
+
290
+ ```ruby
291
+ def start
292
+ loop do
293
+ now = Time.now
294
+ msg = "%02d:%02d:%02d" % [now.hour, now.min, now.sec]
295
+ @mutex.synchronize {
296
+ @clients.each { |c|
297
+ puts "--- write failed" unless c.write(msg)
298
+ }
299
+ }
300
+ sleep(1)
301
+ end
302
+ end
303
+ ```
304
+
305
+ #### Listening
306
+
307
+ In a rackup started application, all request go to a single handler that has a
308
+ `#call(env)` method. Demultiplexing of the incoming request is done in the
309
+ `#call(env)` itself. Requests for static assets such as web pages have to be
310
+ handled.
311
+
312
+ ```ruby
313
+ def call(env)
314
+ path = env['SCRIPT_NAME'] + env['PATH_INFO']
315
+ case path
316
+ when '/'
317
+ return [ 200, { }, [ "hello world" ] ]
318
+ when '/websocket.html'
319
+ return [ 200, { }, [ File.read('websocket.html') ] ]
320
+ when '/sse.html'
321
+ return [ 200, { }, [ File.read('sse.html') ] ]
322
+ when '/upgrade'
323
+ unless env['rack.upgrade?'].nil?
324
+ env['rack.upgrade'] = $clock
325
+ return [ 200, { }, [ ] ]
326
+ end
327
+ end
328
+ [ 404, { }, [ ] ]
329
+ end
330
+ ```
331
+
332
+ The proposed feature is the detection of a connection that wants to be
333
+ upgraded by checking the `env['rack.upgrade?']` variable. If that variables is
334
+ `:websocket` of `sse` then a handle should be provided by setting
335
+ `env['rack.upgrade']` to the upgrade handler. A return code of less than 300
336
+ will indicate to the server that the connection can be upgraded.
337
+
338
+ ```ruby
339
+ when '/upgrade'
340
+ unless env['rack.upgrade?'].nil?
341
+ env['rack.upgrade'] = $clock
342
+ return [ 200, { }, [ ] ]
343
+ end
344
+ end
345
+ ```
346
+
347
+ #### Starting
348
+
349
+ The last lines start the clock is a separate thread so that it runs in the
350
+ background and then rackup starts the server with a new listener.
351
+
352
+ ```ruby
353
+ Thread.new { $clock.start }
354
+ run Listen.new
355
+ ```
356
+
357
+ ### Demultiplex
358
+
359
+ The `push.rb` is an example of using the Agoo demultiplexing feature. Allowing
360
+ the server to handle the demultiplexing simplifies `#call(env)` method and
361
+ allows each URL path to be handled by a separate object. This approach is
362
+ common among most web server in almost every language. But that is not the
363
+ only advantage. By setting a root directory for static resources Agoo can
364
+ serve up those resources without getting Ruby involved at all. This allows
365
+ those resources to be server many times faster all without creating additional
366
+ Ruby objects. Pages with significant static resources become snappier and the
367
+ whole users experience is improved.
368
+
369
+ Note: It is possible to set up the demultiplexing using arguments to
370
+ `rackup`. As an example, to set up the handler for `/myhandler` to an instance
371
+ of the `MyHandler` class start `rackup` like this.
372
+
373
+ ```
374
+ $ rackup -r agoo -s agoo -O "/myhandler=MyHandler"
375
+ ```
376
+
377
+ Now the `push.rb` file.
378
+
379
+ ```ruby
380
+ # push.rb
381
+ require 'agoo'
382
+ Agoo::Log.configure(dir: '',
383
+ console: true,
384
+ classic: true,
385
+ colorize: true,
386
+ states: {
387
+ INFO: true,
388
+ DEBUG: false,
389
+ connect: true,
390
+ request: true,
391
+ response: true,
392
+ eval: true,
393
+ push: true,
394
+ })
395
+ Agoo::Server.init(6464, '.', thread_count: 1)
396
+
397
+ class HelloHandler
398
+ def call(env)
399
+ [ 200, { }, [ "hello world" ] ]
400
+ end
401
+ end
402
+
403
+ class Clock
404
+ def initialize()
405
+ @clients = []
406
+ @mutex = Mutex.new
407
+ end
408
+
409
+ def on_open(client)
410
+ puts "--- on_open"
411
+ @mutex.synchronize {
412
+ @clients << client
413
+ }
414
+ end
415
+
416
+ def on_close(client)
417
+ puts "--- on_close"
418
+ @mutex.synchronize {
419
+ @clients.delete(client)
420
+ }
421
+ end
422
+
423
+ def on_drained(client)
424
+ puts "--- on_drained"
425
+ end
426
+
427
+ def on_message(client, data)
428
+ puts "--- on_message #{data}"
429
+ client.write("echo: #{data}")
430
+ end
431
+
432
+ def start
433
+ loop do
434
+ now = Time.now
435
+ msg = "%02d:%02d:%02d" % [now.hour, now.min, now.sec]
436
+ @mutex.synchronize {
437
+ @clients.each { |c|
438
+ puts "--- write failed" unless c.write(msg)
439
+ }
440
+ }
441
+ sleep(1)
442
+ end
443
+ end
444
+ end
445
+
446
+ $clock = Clock.new
447
+
448
+ class Listen
449
+ # Only used for WebSocket or SSE upgrades.
450
+ def call(env)
451
+ unless env['rack.upgrade?'].nil?
452
+ env['rack.upgrade'] = $clock
453
+ [ 200, { }, [ ] ]
454
+ else
455
+ [ 404, { }, [ ] ]
456
+ end
457
+ end
458
+ end
459
+
460
+ Agoo::Server.handle(:GET, "/hello", HelloHandler.new)
461
+ Agoo::Server.handle(:GET, "/upgrade", Listen.new)
462
+
463
+ Agoo::Server.start()
464
+ $clock.start
465
+ ```
466
+
467
+ The `push.rb` file is similar to the `config.ru` file with the same `Clock`
468
+ class. Running is different but still simple.
469
+
470
+ ```
471
+ $ ruby push.rb
472
+ ```
473
+
474
+ #### Logging
475
+
476
+ Agoo allows logging to be configured. The log is an asynchronous log so the
477
+ impact of logging on performance is minimal. The log is a feature based
478
+ logger. Most of the features are turned on so that it is clear what is
479
+ happening when running the example. Switch the `true` values to `false` to
480
+ turn off logging of any of the features listed.
481
+
482
+ ```ruby
483
+ Agoo::Log.configure(dir: '',
484
+ console: true,
485
+ classic: true,
486
+ colorize: true,
487
+ states: {
488
+ INFO: true,
489
+ DEBUG: false,
490
+ connect: true,
491
+ request: true,
492
+ response: true,
493
+ eval: true,
494
+ push: true,
495
+ })
496
+ ```
497
+
498
+ #### Static Assets
499
+
500
+ The second argument to the Aggo server initializer sets the root directory for
501
+ static assets.
502
+
503
+ ```ruby
504
+ Agoo::Server.init(6464, '.', thread_count: 1)
505
+ ```
506
+
507
+ #### Listening
508
+
509
+ Listening becomes simpler just handling the upgrade.
510
+
511
+ ```ruby
512
+ def call(env)
513
+ unless env['rack.upgrade?'].nil?
514
+ env['rack.upgrade'] = $clock
515
+ [ 200, { }, [ ] ]
516
+ else
517
+ [ 404, { }, [ ] ]
518
+ end
519
+ end
520
+ ```
521
+
522
+ #### Demultiplexing
523
+
524
+ It is rare to have the behavior on a URL path to change after starting so why
525
+ not let the server handle the switching. The option to let the application
526
+ handle the demultiplexing in the `#call(env)` invocation but defining the
527
+ switching in one place is much easier to follow and manage especially a large
528
+ team of developers are working on a project.
529
+
530
+ ```ruby
531
+ Agoo::Server.handle(:GET, "/hello", HelloHandler.new)
532
+ Agoo::Server.handle(:GET, "/upgrade", Listen.new)
533
+ ```
534
+
535
+ ### Simplified Pub-Sub
536
+
537
+ No reason to stop simplifying. With Agoo 2.1.0, publish and subscribe is
538
+ possible using string subjects. Instead of setting up the `@clients` array
539
+ attribute the Agoo server can take care of those details. This example is
540
+ slimmed down from the earlier example by making use of the Ruby feature that
541
+ classes are also object so the `Clock` class can act as the handler. Methods
542
+ not needed have been removed to leave just what is needed to publish time out
543
+ to all the listeners. Open a few pages with either or both the
544
+ `websocket.html` or `sse.html` files.
545
+
546
+ Note the use of `Agoo.publish` and `client.subscribe` in the `pubsub.rb`
547
+ example. Those two method are not part of the proposed Rack spec additions but
548
+ make stateless WebSocket and SSE use simple. Us this with the `websocket.html`
549
+ and `sse.html` as with the previous examples.
550
+
551
+ ```ruby
552
+ # pubsub.rb
553
+ require 'agoo'
554
+
555
+ class Clock
556
+ def self.call(env)
557
+ unless env['rack.upgrade?'].nil?
558
+ env['rack.upgrade'] = Clock
559
+ [ 200, { }, [ ] ]
560
+ else
561
+ [ 404, { }, [ ] ]
562
+ end
563
+ end
564
+
565
+ def self.on_open(client)
566
+ client.subscribe('time')
567
+ end
568
+
569
+ Thread.new {
570
+ loop do
571
+ now = Time.now
572
+ Agoo.publish('time', "%02d:%02d:%02d" % [now.hour, now.min, now.sec])
573
+ sleep(1)
574
+ end
575
+ }
576
+ end
577
+
578
+ Agoo::Server.init(6464, '.', thread_count: 0)
579
+ Agoo::Server.handle(:GET, '/upgrade', Clock)
580
+ Agoo::Server.start()
581
+ ```