librelist archives

« back to archive

Push functionality

Push functionality

From:
Thomas Sommer
Date:
2010-01-02 @ 10:52
Hi.

First of all: Nice lib!

I managed to get Sunshowers working with one of my Sinatra apps and Chrome
(latest Mac release).

Now the reason i'm really interested in Web Sockets is the possibility to
push stuff from the server to the client. Unfortunately i wasn't able to
send something to the client w/o the client making a request. Can someone
give me any hints on how to 'pass around' the connection object (or whatever
is necessary) and make requests at any random time?

Thanks,
Tom


Heres the client side code:

if ("WebSocket" in window) {
  var ws = new WebSocket("ws://localhost:8080/ws");

  ws.onopen = function() {
    console.debug("websocket initialized")
  };

  ws.onmessage = function (evt) {
    console.debug(evt.data);
  };

  ws.onclose = function() {
    console.debug("websocket closed");
  };
}
else {
  console.debug("no browser support - sorry!");
}


And thats my handler in Ruby (combatible with Sinatra):

require 'sunshowers'

class WebsocketHandler
  def initialize(app)
    @app = app
  end

  def call(env)
    req = Sunshowers::Request.new(env)

    if req.ws?
      req.ws_handshake!
      WebsocketHandler.ws = req

      ws_io = req.ws_io
      ws_io.each do |record|
        ws_io.write("message received: #{record}")
      end
      req.ws_quit!
      WebsocketHandler.ws = nil

      [404, {}, []]
    else
      status, headers, body = @app.call(env)
      [status, headers, body]
    end
  end
end

Re: [sunshowers] Push functionality

From:
Eric Wong
Date:
2010-01-02 @ 11:17
Thomas Sommer <thomas.sommer@tricycledevelopments.com> wrote:
> Hi.
> 
> First of all: Nice lib!

Hi Thomas, thanks!

> I managed to get Sunshowers working with one of my Sinatra apps and Chrome
> (latest Mac release).
> 
> Now the reason i'm really interested in Web Sockets is the possibility to
> push stuff from the server to the client. Unfortunately i wasn't able to
> send something to the client w/o the client making a request. Can someone
> give me any hints on how to 'pass around' the connection object (or whatever
> is necessary) and make requests at any random time?

Looking at your code, you should be able to call #write/#write_utf8 at
any point, not just inside the #each block:

<snip>

> class WebsocketHandler
>   def initialize(app)
>     @app = app
>   end
> 
>   def call(env)
>     req = Sunshowers::Request.new(env)
> 
>     if req.ws?
>       req.ws_handshake!
>       WebsocketHandler.ws = req
> 
>       ws_io = req.ws_io

	ws_io.write "hello"
	ws_io.write "how are you"

>       ws_io.each do |record|
>         ws_io.write("message received: #{record}")
>       end
>       req.ws_quit!
>       WebsocketHandler.ws = nil

Let us know if that fixes things for you or if I'm misunderstanding
you...

You can't have the handshaking part without the client initiating it,
but once the handshake is done then the server can write without waiting
for the client.

Thanks for using Sunshowers!

-- 
Eric Wong

Re: [sunshowers] Push functionality

From:
Thomas Sommer
Date:
2010-01-03 @ 01:51
Hey Eric.

Thanks for the quick response!

The thing i am still not getting is, how its possible to write (push)
something from outside the WebsocketHandler. So what im trying to achieve is
to push from ie. the sinatra app itself (for example on a regular 'GET'
request) or some other class / module involved with the App.

I tried to marshal the IO struct, but because theres a socket involved, it
wont let me.

Am I misunderstanding the concept of Sunshowers / Rainbows and do i have to
take a totally different approach?!?

Thanks,
Tom


On Sat, Jan 2, 2010 at 10:17 PM, Eric Wong <normalperson@yhbt.net> wrote:

> Thomas Sommer <thomas.sommer@tricycledevelopments.com> wrote:
> > Hi.
> >
> > First of all: Nice lib!
>
> Hi Thomas, thanks!
>
> > I managed to get Sunshowers working with one of my Sinatra apps and
> Chrome
> > (latest Mac release).
> >
> > Now the reason i'm really interested in Web Sockets is the possibility to
> > push stuff from the server to the client. Unfortunately i wasn't able to
> > send something to the client w/o the client making a request. Can someone
> > give me any hints on how to 'pass around' the connection object (or
> whatever
> > is necessary) and make requests at any random time?
>
> Looking at your code, you should be able to call #write/#write_utf8 at
> any point, not just inside the #each block:
>
> <snip>
>
> > class WebsocketHandler
> >   def initialize(app)
> >     @app = app
> >   end
> >
> >   def call(env)
> >     req = Sunshowers::Request.new(env)
> >
> >     if req.ws?
> >       req.ws_handshake!
> >       WebsocketHandler.ws = req
> >
> >       ws_io = req.ws_io
>
>         ws_io.write "hello"
>        ws_io.write "how are you"
>
> >       ws_io.each do |record|
> >         ws_io.write("message received: #{record}")
> >       end
> >       req.ws_quit!
> >       WebsocketHandler.ws = nil
>
> Let us know if that fixes things for you or if I'm misunderstanding
> you...
>
> You can't have the handshaking part without the client initiating it,
> but once the handshake is done then the server can write without waiting
> for the client.
>
> Thanks for using Sunshowers!
>
> --
> Eric Wong
>
>

Re: [sunshowers] Push functionality

From:
Eric Wong
Date:
2010-01-03 @ 02:26
Thomas Sommer <thomas.sommer@tricycledevelopments.com> wrote:
> Hey Eric.
> 
> Thanks for the quick response!

Hi Thomas, no problem :)  Please don't top post when replying, thanks.

> The thing i am still not getting is, how its possible to write (push)
> something from outside the WebsocketHandler. So what im trying to achieve is
> to push from ie. the sinatra app itself (for example on a regular 'GET'
> request) or some other class / module involved with the App.
> 
> I tried to marshal the IO struct, but because theres a socket involved, it
> wont let me.
> 
> Am I misunderstanding the concept of Sunshowers / Rainbows and do i have to
> take a totally different approach?!?

You shouldn't have to marshal anything, I just wrote up the following
example and pushed it to sunshowers.git   Most of my Sinatra knowledge
is from the 0.3.x days, so there's probably a better way of doing what
I'm doing in 0.9.4.

----------------- examples/sinshowers.rb ------------------------
# Example of Sunshowers + Sinatra 0.9.4, for use with echo_client.{rb,py}

require 'sinatra/base'
require 'sunshowers'

# There's probably a better/cleaner way to do this, but we're not
# very familiar with Sinatra...
# Let us know of a better way at sunshowers@librelist.com
class Sinatra::Request < Rack::Request
  include Sunshowers::WebSocket
end

class Sinshowers < Sinatra::Base

  get "/echo" do
    if request.ws?
      request.ws_handshake!
      ws_io = request.ws_io
      ws_io.each do |record|
        ws_io.write_utf8(record)
        break if record == "Goodbye"
      end
      request.ws_quit!
    end
    "You're not using Web Sockets"
  end

end
----------------------------- 8< -----------------------------

Maybe somebody could hack up a proper Sinatra
plugin/extension/what-ever-you-call-it-these-days :)

-- 
Eric Wong

Re: [sunshowers] Push functionality

From:
Thomas Sommer
Date:
2010-01-03 @ 10:12
On Sun, Jan 3, 2010 at 1:26 PM, Eric Wong <normalperson@yhbt.net> wrote:

> Thomas Sommer <thomas.sommer@tricycledevelopments.com> wrote:
> > Hey Eric.
> >
> > Thanks for the quick response!
>
> Hi Thomas, no problem :)  Please don't top post when replying, thanks.
>

Sorry, I forgot about it...

>
> > The thing i am still not getting is, how its possible to write (push)
> > something from outside the WebsocketHandler. So what im trying to achieve
> is
> > to push from ie. the sinatra app itself (for example on a regular 'GET'
> > request) or some other class / module involved with the App.
> >
> > I tried to marshal the IO struct, but because theres a socket involved,
> it
> > wont let me.
> >
> > Am I misunderstanding the concept of Sunshowers / Rainbows and do i have
> to
> > take a totally different approach?!?
>
> You shouldn't have to marshal anything, I just wrote up the following
> example and pushed it to sunshowers.git   Most of my Sinatra knowledge
> is from the 0.3.x days, so there's probably a better way of doing what
> I'm doing in 0.9.4.
>
> ----------------- examples/sinshowers.rb ------------------------
> # Example of Sunshowers + Sinatra 0.9.4, for use with echo_client.{rb,py}
>
> require 'sinatra/base'
> require 'sunshowers'
>
> # There's probably a better/cleaner way to do this, but we're not
> # very familiar with Sinatra...
> # Let us know of a better way at sunshowers@librelist.com
> class Sinatra::Request < Rack::Request
>  include Sunshowers::WebSocket
> end
>
> class Sinshowers < Sinatra::Base
>
>  get "/echo" do
>    if request.ws?
>      request.ws_handshake!
>      ws_io = request.ws_io
>       ws_io.each do |record|
>         ws_io.write_utf8(record)
>        break if record == "Goodbye"
>      end
>      request.ws_quit!
>    end
>    "You're not using Web Sockets"
>  end
>
> end
> ----------------------------- 8< -----------------------------
>
> Maybe somebody could hack up a proper Sinatra
> plugin/extension/what-ever-you-call-it-these-days :)
>

Thanks for the example, but this is pretty much what i already have ;)
I reckon in the newer Sinatra versions you write it up as Rack App... maybe
like that:

config.ru:

require 'lib/websocket_handler'

use WebsocketHandler
run Sinatra::Application


websocket_handler.rb:

require 'sunshowers'

class WebsocketHandler
  def initialize(app)
    @app = app
  end

  def call(env)
    req = Sunshowers::Request.new(env)

    if req.ws?
      req.ws_handshake!

      ws_io = req.ws_io
      @app.configure do
        WebsocketIo.setup ws_io
      end

      ws_io.write "happy new year!"
      ws_io.each do |record|
        ws_io.write "message received: #{record}"
      end
      req.ws_quit!

      [404, {}, []]
    else
      status, headers, body = @app.call(env)
      [status, headers, body]
    end
  end
end


Thats all good and fine... with the #configure call above the IO object is
made available to the Sinatra app, so that solves my original problem.

Of course theres a new problem (actually theres more than one) now ;) When i
start up Rainbows! it:
1. Takes forever (about 30 sec) to initialize the worker... in the command
line it hangs a bit when it says 'Refreshing Gem list'.
2. Rainbows! really needs a long time to load up static content (images,
stylesheets, ...)
3. (and most important) as soon as theres a Websocket connection
initialized, all regular requests wont go through to the Sinatra app until
the worker was killed (which happens all the time after about 60 sec)

Im pretty sure this is a configuration setting when using Rainbows!, i just
couldnt figure out which one...

Thanks again for your help!
Tom


>
> --
> Eric Wong
>
>

Re: [sunshowers] Push functionality

From:
Eric Wong
Date:
2010-01-03 @ 19:50
Thomas Sommer <thomas.sommer@tricycledevelopments.com> wrote:
<snip>
> > ----------------------------- 8< -----------------------------
> >
> > Maybe somebody could hack up a proper Sinatra
> > plugin/extension/what-ever-you-call-it-these-days :)
> >
> 
> Thanks for the example, but this is pretty much what i already have ;)
> I reckon in the newer Sinatra versions you write it up as Rack App... maybe
> like that:

<snip stuff that looks alright>

> websocket_handler.rb:
> 
> require 'sunshowers'
> 
> class WebsocketHandler
>   def initialize(app)
>     @app = app
>   end
> 
>   def call(env)
>     req = Sunshowers::Request.new(env)
> 
>     if req.ws?
>       req.ws_handshake!
> 
>       ws_io = req.ws_io
> 
>       @app.configure do
>         WebsocketIo.setup ws_io
>       end

Huh?  I'm not sure you need to do @app.configure for every single
request that comes in.

> Thats all good and fine... with the #configure call above the IO object is
> made available to the Sinatra app, so that solves my original problem.

That @app.configure call appears to app-wide, but you're localizing to
a certain HTTP request which seems wrong.

> Of course theres a new problem (actually theres more than one) now ;) When i
> start up Rainbows! it:
> 1. Takes forever (about 30 sec) to initialize the worker... in the command
> line it hangs a bit when it says 'Refreshing Gem list'.

That doesn't sound right, which Rainbows! concurrency model are you using?
Of course, it could be the lack of @app.configure being called at the
top-level...

> 2. Rainbows! really needs a long time to load up static content (images,
> stylesheets, ...)

Do you cache that stuff ahead of time?  Exactly how much is loaded?
It should behave like other Rack servers.

> 3. (and most important) as soon as theres a Websocket connection
> initialized, all regular requests wont go through to the Sinatra app until
> the worker was killed (which happens all the time after about 60 sec)
> 
> Im pretty sure this is a configuration setting when using Rainbows!, i just
> couldnt figure out which one...

I'm pretty sure most of your current problems are from the fact that
@app.configure is called during your application dispatch (on every
request), not when your application is loaded/initialized.

Let us know if you figure it out.

> Thanks again for your help!

No problem :)

-- 
Eric Wong

Re: [sunshowers] Push functionality

From:
Thomas Sommer
Date:
2010-01-04 @ 09:39
>
> >
> >       @app.configure do
> >         WebsocketIo.setup ws_io
> >       end
>
> Huh?  I'm not sure you need to do @app.configure for every single
> request that comes in.
>
> > Thats all good and fine... with the #configure call above the IO object
> is
> > made available to the Sinatra app, so that solves my original problem.
>
> That @app.configure call appears to app-wide, but you're localizing to
> a certain HTTP request which seems wrong.
>

Yes, this part is not even close to being good, but that was my first (lazy)
attempt to make the WebSocket IO object available to stuff that happens
outside of the WS request.

I'll explain my situation: My Sinatra app is connected to our MPD server in
the office. The connection gets status updates from the server (eg. current
song, elapsed time etc). This connection is just a class thats required from
the Sinatra app and regular HTTP requests then poll the data from this
Proxy. What i'm ultimately trying to do, is to push out the updates (to the
browsers) from the Proxy class as soon as they arrive... but for this to
happen i have to access the WS IO (or Request) object from 'outside' without
having to be stuck between the #handshake! and #quit! calls in the
WebSocketHandler...


> > Of course theres a new problem (actually theres more than one) now ;)
> When i
> > start up Rainbows! it:
> > 1. Takes forever (about 30 sec) to initialize the worker... in the
> command
> > line it hangs a bit when it says 'Refreshing Gem list'.
>
> That doesn't sound right, which Rainbows! concurrency model are you using?
> Of course, it could be the lack of @app.configure being called at the
> top-level...
>

Its not the #configure call... that happened w/o the call as well.
How can i find out about the concurrency model im using?

>
> > 2. Rainbows! really needs a long time to load up static content (images,
> > stylesheets, ...)
>
> Do you cache that stuff ahead of time?  Exactly how much is loaded?
> It should behave like other Rack servers.
>

Theres no caching, and im pretty sure Rainbows! should be used together with
a regular web server (ie apache). I just found that it loads up the stuff
way slower in comparison to eg. thin...

>
> > 3. (and most important) as soon as theres a Websocket connection
> > initialized, all regular requests wont go through to the Sinatra app
> until
> > the worker was killed (which happens all the time after about 60 sec)
> >
> > Im pretty sure this is a configuration setting when using Rainbows!, i
> just
> > couldnt figure out which one...
>
> I'm pretty sure most of your current problems are from the fact that
> @app.configure is called during your application dispatch (on every
> request), not when your application is loaded/initialized.
>

I would guess its the concurrency setting... i really have to dive into
Rainbows! a bit more to be able to configure it properly...

Cheers,
Tom

Re: [sunshowers] Push functionality

From:
Eric Wong
Date:
2010-01-04 @ 22:18
Thomas Sommer <thomas.sommer@tricycledevelopments.com> wrote:
> > >       @app.configure do
> > >         WebsocketIo.setup ws_io
> > >       end
> >
> > Huh?  I'm not sure you need to do @app.configure for every single
> > request that comes in.
> >
> > > Thats all good and fine... with the #configure call above the IO object
> > is
> > > made available to the Sinatra app, so that solves my original problem.
> >
> > That @app.configure call appears to app-wide, but you're localizing to
> > a certain HTTP request which seems wrong.
> 
> Yes, this part is not even close to being good, but that was my first (lazy)
> attempt to make the WebSocket IO object available to stuff that happens
> outside of the WS request.
> 
> I'll explain my situation: My Sinatra app is connected to our MPD server in
> the office. The connection gets status updates from the server (eg. current
> song, elapsed time etc). This connection is just a class thats required from
> the Sinatra app and regular HTTP requests then poll the data from this
> Proxy. What i'm ultimately trying to do, is to push out the updates (to the
> browsers) from the Proxy class as soon as they arrive... but for this to
> happen i have to access the WS IO (or Request) object from 'outside' without
> having to be stuck between the #handshake! and #quit! calls in the
> WebSocketHandler...

Ah, which MPD module are you using?  Am I correct you're trying to
share the output from one MPD connection to multiple clients?

Yes, than you'd have to be registering the ws_io with some central
dispatcher...  For Sunshowers, I'd imagine you'd need a background
thread/fiber reading the MPD socket and writing:

  @@clients = []

  def self.register(ws_io)
    @@clients << ws_io
  end

  # This could be a Thread instead of a Fiber too,
  #  depending on your concurrency model
  Fiber.spawn do
    @@mpd_sock.each_line do |line|
      @@clients.delete_if do |ws_io|
        begin
          ws_io.write_utf8(line)
          false # do not delete on success
        rescue # something went wrong, client probably disconnected
          ws_io.ws_quit!
          true # delete
        end
      end
    end
  end

I'm not sure how Sunshowers will work with EventMachine/Rev/Reactor
concurrency models.  But this is probably one of those cases where
programming using an explicitly evented model is actually easier than
using a synchronous concurrency model...

> > > Of course theres a new problem (actually theres more than one) now ;)
> > When i
> > > start up Rainbows! it:
> > > 1. Takes forever (about 30 sec) to initialize the worker... in the
> > command
> > > line it hangs a bit when it says 'Refreshing Gem list'.
> >
> > That doesn't sound right, which Rainbows! concurrency model are you using?
> > Of course, it could be the lack of @app.configure being called at the
> > top-level...
> 
> Its not the #configure call... that happened w/o the call as well.
> How can i find out about the concurrency model im using?

I guess you didn't configure it at all :) In your Rainbows!/Unicorn
config file, it's just having:

      Rainbows! do
        use concurrency_model
      end

Currently, only the following concurrency models are supported

* :FiberSpawn
* :FiberPool
* :Revactor
* :ThreadSpawn
* :ThreadPool
* :RevFiberSpawn

There'll be more, and I'll update
http://rainbows.rubyforge.org/sunshowers/ to reflect that.  Keep in mind
that Ruby 1.8 can only use the Thread* models, not the Fiber ones.
Note that in the above example, with MPD, I used a Fiber.  Just replace
it with a Thread if you choose a Thread-based concurrency model because
Fibers are not shareable across Threads.

> > > 2. Rainbows! really needs a long time to load up static content (images,
> > > stylesheets, ...)
> >
> > Do you cache that stuff ahead of time?  Exactly how much is loaded?
> > It should behave like other Rack servers.
> 
> Theres no caching, and im pretty sure Rainbows! should be used together with
> a regular web server (ie apache). I just found that it loads up the stuff
> way slower in comparison to eg. thin...

It could be the lack of a configured concurrency model, but Rainbows!
shouldn't be slower than Thin for static files.  This is all on a LAN?
Apache will definitely be faster for static files (on a LAN), though it
may not matter for most use cases...  Of course outside of a LAN on slow
networks nginx/lighttpd will beat Apache (not sure if Apache mpm_event
came to anything usable...).

-- 
Eric Wong

Re: [sunshowers] Push functionality

From:
Thomas Sommer
Date:
2010-01-05 @ 05:51
>
>
> Ah, which MPD module are you using?  Am I correct you're trying to
> share the output from one MPD connection to multiple clients?
>
> Yes, than you'd have to be registering the ws_io with some central
> dispatcher...  For Sunshowers, I'd imagine you'd need a background
> thread/fiber reading the MPD socket and writing:
>
>  @@clients = []
>
>  def self.register(ws_io)
>    @@clients << ws_io
>  end
>
>  # This could be a Thread instead of a Fiber too,
>  #  depending on your concurrency model
>  Fiber.spawn do
>    @@mpd_sock.each_line do |line|
>      @@clients.delete_if do |ws_io|
>        begin
>          ws_io.write_utf8(line)
>          false # do not delete on success
>        rescue # something went wrong, client probably disconnected
>          ws_io.ws_quit!
>          true # delete
>        end
>      end
>    end
>  end
>
> I'm not sure how Sunshowers will work with EventMachine/Rev/Reactor
> concurrency models.  But this is probably one of those cases where
> programming using an explicitly evented model is actually easier than
> using a synchronous concurrency model...
>

First of all: AWESOME!!!
This mail did it for me. The main thing was the configuration part - having
Rainbows unconfigured is not a good idea ;) Heres the code i ended up with:


config.ru
(as before)


websocket_handler:

require 'sunshowers'
class WebsocketHandler
  def initialize(app)
    @app = app
  end

  def call(env)
    req = Sunshowers::Request.new(env)

    if req.ws?
      req.ws_handshake!
      WebsocketIo.register req

      ws_io = req.ws_io
      ws_io.each { } # no-op
      req.ws_quit!

      [404, {}, []]
    else
      status, headers, body = @app.call(env)
      [status, headers, body]
    end
  end
end


websocket_io:

class WebsocketIo
  @req_list = []

  class << self
    def register(req)
      @req_list << req
    end

    def write(message)
      @req_list.each do |req|
        begin
          req.ws_io.write message
        rescue Exception => e
          req.ws_quit!
        end
      end
    end
  end
end


This code still needs some sanity checking and some error handling but it
works. Now, whenever im calling WebsocketIo.write from anywhere in my app it
pushes out to all the connected Websockets.

BTW, im using librmpd for MPD, which 'pushes' updates to my proxy inside the
sinatra app.


> > > > Of course theres a new problem (actually theres more than one) now ;)
> > > When i
> > > > start up Rainbows! it:
> > > > 1. Takes forever (about 30 sec) to initialize the worker... in the
> > > command
> > > > line it hangs a bit when it says 'Refreshing Gem list'.
> > >
> > > That doesn't sound right, which Rainbows! concurrency model are you
> using?
> > > Of course, it could be the lack of @app.configure being called at the
> > > top-level...
> >
> > Its not the #configure call... that happened w/o the call as well.
> > How can i find out about the concurrency model im using?
>
> I guess you didn't configure it at all :) In your Rainbows!/Unicorn
> config file, it's just having:
>
>      Rainbows! do
>        use concurrency_model
>      end
>
> <snip>

>
> It could be the lack of a configured concurrency model, but Rainbows!
> shouldn't be slower than Thin for static files.  This is all on a LAN?
> Apache will definitely be faster for static files (on a LAN), though it
> may not matter for most use cases...  Of course outside of a LAN on slow
> networks nginx/lighttpd will beat Apache (not sure if Apache mpm_event
> came to anything usable...).
>

So, it has been all about the config. Could it be that Rainbows just handles
1 connection by default? If thats true then the Websocket did block
everything else (of course). The speed has also improved drastically!

Heres the config im using now:

worker_processes 2
Rainbows! do
  use :ThreadSpawn
  worker_connections 10
end

Are there any guidelines on how many connections one should use?

Thanks mate, I owe u a beer!
Tom

Re: [sunshowers] Push functionality

From:
Eric Wong
Date:
2010-01-05 @ 21:34
Thomas Sommer <thomas.sommer@tricycledevelopments.com> wrote:
> First of all: AWESOME!!!
> This mail did it for me. The main thing was the configuration part - having
> Rainbows unconfigured is not a good idea ;) Heres the code i ended up with:

Cool, good to know everything worked out for you :)

> So, it has been all about the config. Could it be that Rainbows just handles
> 1 connection by default? If thats true then the Websocket did block
> everything else (of course). The speed has also improved drastically!

Yup, the Rainbows! defaults are very conservative, one worker, one
connection.  I've considered setting more aggressive ones, but
given the large variety of apps out there, it's _always_ broken for
someone, and sometimes the problem wouldn't get noticed until it's
too late.

> Heres the config im using now:
> 
> worker_processes 2
> Rainbows! do
>   use :ThreadSpawn
>   worker_connections 10
> end
> 
> Are there any guidelines on how many connections one should use?

It depends on the type of traffic you get.  If it's just your
office/LAN, then just count the number of clients you expect connected
at any given time.

Keep in mind the real number of connections is
(worker_processes * worker_connections)

> Thanks mate, I owe u a beer!

No problem.  You don't owe me anything, I don't drink :)

-- 
Eric Wong