Sunday, September 6, 2015

Using a webapp as a local application GUI

This is just a quick proof-of-concept for using a local webserver and a browser instance as a standalone GUI application. The idea is to use a browser and an embedded webserver as your UI toolkit, instead of hoping that the user was able to get qtbindings or shoes installed.

Http Application

The main application spawns two child processes: a local web server and a web browser instance.

In an ideal world, the application would wait for both processes to exit, and perhaps offer to restart one or the other in the event of a crash. Because of the way modern browsers work -- opening URLs in an existing process instead of starting a new one -- this has to be simplified to waiting only for the web server process to exit, and hoping that the browser process can successfully manage things.

The HttpApplication class provides a run() method that starts a webserver on localhost using the first available port, then opens a browser window/tab connected to the webserver:

class HttpApplication

  def run(host=nil, port=nil, browser=nil, path=nil)
    @server =, port, @controller)
    uri = server.uri.dup
    if path
      path = '/' + path if (! path.start_with? '/')
      uri.path = path
    end, browser)

When the webserver process exits, the application will exit. Note that this means the webapp itself must close the webserver process, either through an explicit signout, or through a Javascript on_close or on_unload event handler.


The webserver process is simply a Webrick instance with a servlet acting as a controller. This is pretty straightforward: the only interesting part is detecting the next available port so that it can be passed to the Webrick constructor.

require 'uri'
require 'webrick'
class ServerProcess
  attr_reader :host, :port, :controller, :pid, :uri, :webrick

  def initialize(host=nil, port=nil, controller=nil)
    @port = port || get_avail_port(host)
    @host = host || IPSocket.getaddress(Socket.gethostname)
    @controller = controller
  def start
    @pid = Process.fork do
      @webrick = :Port => @port )
      @webrick.mount('/', @controller) if @controller
      trap('HUP') { @webrick.stop; @webrick.start }
      trap('INT') { @webrick.shutdown }

    trap('INT') { Process.kill 'INT', @pid }
    trap('TERM') { Process.kill 'INT', @pid }
    @uri = {:host => @host, :port => @port} )
  def stop
    @webrick.shutdown if @webrick
    Process.kill('INT', @pid) if @pid

  def get_avail_port(host)
    host ||= (Socket::gethostbyname('')||['localhost'])[0]
    inf = Socket::getaddrinfo(host, nil, Socket::AF_UNSPEC,
Socket::SOCK_STREAM, 0, Socket::AI_PASSIVE)
    fam = inf.inject({}) { |h, arr| h[arr[0]]= arr[2]; h }
    sock_host = fam['AF_INET'] || fam['AF_INET6']
    sock = sock_host ?, 0) :
    port = sock.addr[1]


The browser process  is a simple launcher that opens a URL in a browser instance , in what is hopefully a platform-independent manner. One could use the launchy gem or some other suitably over-engineered solution, but there's really not much need to support more than Windows, OS X, and Linux. Let's face it, anyone who doesn't have xdg-open installed probably doesn't want to run your webserver-based app on their ideologically-pure system.

require 'shellwords'
class Browser
  if RUBY_PLATFORM =~ /darwin/
    URL_OPEN_COMMAND = 'open'
  elsif RUBY_PLATFORM =~ /linux/ or RUBY_PLATFORM =~ /bsd/
    URL_OPEN_COMMAND = 'xdg-open'

    # assume windows
    URL_OPEN_COMMAND = 'start'
  def, cmd=nil)
    pid = Process.fork do
      uri_s = Shellwords.shellescape uri.to_s
      `#{Shellwords.shellescape(cmd || URL_OPEN_COMMAND)} #{uri_s}`


The meat of the UI is in the controller object which HttpApplication uses to handle webserver routes. This particular one is based on Webrick's AbstractServlet class, and uses regex patterns to determine the handler for a particular route.

Each route consists of a pattern and a block. The pattern is used to match the URL; for best results, the ^ and $ anchors should be used, and the URL path should be absolute (i.e., starting with '/').

The block receives three arguments: query, request, and server. The query is a Hash extracted from the Webrick HTTPRequest object, the request is the complete HTTPRequest object, and the server is the Webrick HTTPServer object for the webserver (used mainly to shutdown the server). The block must return either the response body or an array [body, content_type, status]; these return values will be written to the Webrick HTTPResponse object.

class RestController < WEBrick::HTTPServlet::AbstractServlet
  DEFAULT_CONTENT_TYPE = 'text/html'
  DEFAULT_BODY = '404 : not found' # this should really be html

  @routes = []
  def self.route(pat, &block)
    @routes << [pat,]
  def self.routes; @routes; end
  def initialize(server)
    @webrick = server
    super server
  def fill_response(response, bodycontent_type=DEFAULT_CONTENT_TYPEstatus=DEFAULT_STATUS)
    response.status = status
    response['Content-Type'] = content_type
    response.body = body
  def route_request(request)
    content_type = DEFAULT_CONTENT_TYPE
    status = 404
    body = DEFAULT_BODY
    self.class.routes.each do |pat, proc_obj|
      if request.path =~ pat
        arr = [, request, @webrick) ].flatten
        body = arr[0]
        content_type = arr[1] || DEFAULT_CONTENT_TYPE
        status = arr[2] || DEFAULT_STATUS
    [body, content_type, status]
  def do_GET(request, response)
    body, content_type, status = route_request(request)
    fill_response(response, body, content_type, status)
  def do_POST(request, response)
    body, content_type, status = route_request(request)
    fill_response(response, body, content_type, status)


Here's a quick example that defines a few handlers.

def create_rest_controller
  cls = RestController

  # /test
  cls.route(/^\/test$/) { |q,r| ["test", "text/plain"] }
  cls.route(/^\/quit$/) { |q,r,s| s.shutdown; ["shutting down", "text/plain"] }


  cls.route(/^\/$/) { |q,r,s| s.shutdown; ["root!""text/plain"] }

app = create_rest_controller )

The content returned by the routes is all plaintext because hey, the blogger interface sucks for entering < and  > characters.

Note the /quit handler: this is necessary to shutdown the webserver process (otherwise it will continue to run in the background). If you are certain your users permit Javascript, you can have an event handler hit this URL when the user closes the browser tab/window.

An alternative is to have the web server manage a PID file, so that subsequent invocations of the application will connect to the webserver process running in the background (much like the browsers that forced us into this mess).

As usual, the code is available on github.

No comments:

Post a Comment