June 9th, 2008 cschneid
After much poking and prodding it looks like Sinatra won’t work on top of JRuby Rack. It’s an unintended consequence of how everything was implemented, and it looks like some (hopefully minor) changes will have to be made to Sinatra.
Steps to get to where I am:
- Install JRuby 1.1.2
- Get tomcat - I used a 6.0.14 install I had laying around
- Install the JRuby gem “warble”, which will come with jruby-rack
- Create a new directory structure, described below
- Copy jruby-complete.jar, and jruby-rack.jar to the WEB-INF/lib directory
- Create a simple Sinatra hello world app
- Create your web.xml file as described
The directory structure
jruby-sinatra/
jruby-sinatra/WEB-INF
jruby-sinatra/WEB-INF/web.xml
jruby-sinatra/WEB-INF/hello_world.rb
jruby-sinatra/WEB-INF/sinatra # full copy of sinatra goes here
jruby-sinatra/WEB-INF/lib
jruby-sinatra/WEB-INF/lib/jruby-complete-1.1.1.jar
jruby-sinatra/WEB-INF/lib/jruby-rack-0.9.jar
My (non-working) current web.xml file.
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<context-param>
<param-name>public.root</param-name>
<param-value>/</param-value>
</context-param>
<context-param>
<param-name>rackup</param-name>
<param-value>
$:.unshift '/Users/cschneid/servers/apache-tomcat-6.0.14/webapps/jruby-sinatra/WEB-INF/'
$:.unshift '/Users/cschneid/servers/apache-tomcat-6.0.14/webapps/jruby-sinatra/WEB-INF/sinatra/lib'
puts "====================="
puts "====================="
puts "====================="
puts "Current file is: #{__FILE__}"
puts "Dollar 0 is: #{$0}"
puts "List of load paths:"
puts $:.map {|a| puts a }
puts "====================="
puts "====================="
puts "====================="
puts "Requiring sinatra"
require "sinatra"
puts "Setting up the sinatra app options"
Sinatra::Application.default_options.merge!(
:run => false,
:env => :production
)
puts "About to require test"
require 'test'
puts "About to run sinatra"
run Sinatra.application
</param-value>
</context-param>
<filter>
<filter-name>RackFilter</filter-name>
<filter-class>org.jruby.rack.RackFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>RackFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.jruby.rack.RackServletContextListener</listener-class>
</listener>
</web-app>
The problem - the load fails on the Sinatra::Application.default_options.merge line. The first time you access default_options, it generates the hash. Every subsequent time, it just hands back the already created instance. Since this is the first time, it attempts to create the default_options structure. It gets a “root” of the directory tree from $0. And then later combines it with ‘/views’ and ‘/public’. The problem lies in the fact that we have a ruby script embedded inside the web.xml file. It doesn’t have a reasonable $0 to even set, so it doesn’t. If you ask for __FILE__, it just reports back with “<script>”. End result: you get a simple “can’t convert nil into String” exception thrown.
Next Steps - I don’t know, I think we should probably wean Sinatra off of $0 if at all possible. Or at least figure out how to set it to something reasonable so that default_options doesn’t blow up. If anybody has any ideas on how to go about this, let me know, since this doesn’t have an obvious fix that I see. Mainly, I don’t want to break functionality elsewhere (for plain ruby).
Posted in Programming, Ruby, Sinatra | 3 Comments »
June 8th, 2008 cschneid
I don’t like deployment via FastCGI that much. It’s just too unstable, and random for my tastes. So when I saw that Passenger was going to support Rack, I knew I had to get that working on Dreamhost.
Once I started going at it, it only took me a few minutes to make everything work. You can find documentation at the Passenger Github repository.
Steps:
- Setup your account to support mod_rails
- Create the directory structure
- Make a rackup file (config.ru)
- Create a very simple Sinatra application
1) Setting up the account
- Domains -> Manage Domains -> Edit (web hosting column)
- Enable “Ruby on Rails Passenger (mod_rails)”
- Add the public directory to the web directory box. So if you were using “gittr.com”, it would change to “gittr.com/public”
- Save your changes
2) Creating the directory structure
domain.com/
domain.com/tmp
domain.com/public
# a vendored version of sinatra - not necessary if you use the gem
domain.com/sinatra
3) Creating a rackup file
# This file goes in domain.com/config.ru
require 'sinatra/lib/sinatra.rb'
require 'rubygems'
Sinatra::Application.default_options.merge!(
:run => false,
:env => :production
)
require 'test.rb'
run Sinatra.application
4) A very simple Sinatra application
# this is test.rb referred to above
get '/' do
"Worked on dreamhost"
end
get '/foo/:bar' do
"You asked for foo/#{params[:bar]}"
end
And that’s all there is to it! Once it’s all setup, point your browser at your domain, and you should see a “Worked on Dreamhost” page. To restart the application after making changes, you need to run “touch tmp/restart.txt”.
Posted in Programming, Ruby, Sinatra, System Admin | 14 Comments »
June 3rd, 2008 cschneid
In my never ending quest to avoid doing anything useful, I decided to attempt to run Sinatra on top of JRuby. I had the original goal of getting it to run under JRuby-Rack and built into a war file that Tomcat or JBoss, or Weblogic, or… Glassfish(??) would take. Unfortunately, I didn’t get that far. What I saw was:
LocalJumpError - yield called out of block
My mini application that shows the issue:
$:.unshift File.dirname(__FILE__) + '/sinatra/lib'
require 'sinatra'
get '/' do
"Hello World"
end
get '/no_layout' do
erb :foo, :layout => false
end
get '/erb' do
erb :foo
end
get '/haml' do
haml :foo
end
Of course I have some views: - (just erb here, hamls are almost identical)
<!-- this is foo.erb -->
<p>Erb rendered</p>
<!-- this is layout.erb -->
<html>
<head></head>
<body>
<p>Layout rendered</p>
<%= yield %>
</body>
</html>
So… I filed a bug report against JRuby.. I’m impressed that Vladimir has already identified the narrow test case that I didn’t, and has written specs. I’m hoping that the next release of JRuby will have this fixed, and we can all move on with our lives.
Until then, layouts in Sinatra don’t work with JRuby.
Posted in Programming, Ruby, Sinatra | No Comments »
June 1st, 2008 cschneid
According to this post it looks like the version 2 release of Passenger will support Rack. Since Sinatra is built right on top of Rack, that means that Passenger will support Sinatra.
In practical terms, that means whenever Dreamhost (and other shared hosting providers) gets around to updating their Passenger installations, deployment of Sinatra apps will be as easy as writing a rackup file.
UPDATE: Sure enough, the newest release supports Rack, and I have a tutorial on getting sintra deployed on Dreamhost via passenger/mod_rails.
Posted in Programming, Ruby, Sinatra | No Comments »
May 30th, 2008 cschneid
To turn on sessions normally, you just have to run:
enable :sessions
# OR
set :sessions, true
Under the covers, that sets up Rack::Session::Cookie as a rack middleware component. If you were to do this manually, it would look like:
use Rack::Session::Cookie
The problem with relying on this is that the Cookie session store limits you to storing only 4k of data. It’s normally not a great idea to store a ton of data in the session, but it’s sometimes necessary. The other problem with using the cookie session stores is that the data is stored unencrypteted (but packed as base64). So you open your application up to a potential attack from a malicious user manipulated cookie.
So what we need to do in order to “use” the alternate session implementation is just to run:
use Rack::Session::Pool
# Or another session store that matches the rack API
# DON'T enable :session
# DON'T set :session, true
I haven’t seen many session implementations in Rack, but the edge Rack (and the next release presumably) will make it simple to create new implementations. Just check out Rack::Session::Abstract::Id for some helpers. Also, edge Rack does support a Memcached session adapter, which looks pretty promising as a usable session container.
Posted in Programming, Ruby, Sinatra | 1 Comment »
May 28th, 2008 cschneid
After my half-assed post about RSpec testing the Sinatra framework got several google hits, I figured I’d look into it a little further. Here’s what I found.
First, a very simple Sinatra application.
$:.unshift("../sinatra/lib")
require 'sinatra'
get '/' do
"Hello"
end
get '/:thingy' do
"Hello #{params[:thingy]}"
end
Now a test script.
$:.unshift("../sinatra/lib")
require 'sinatra'
require 'spec/interop/test'
require 'sinatra/test/unit'
describe 'Hello World' do
require 'hello'
specify "should render hello at /" do
get_it '/'
@response.should be_ok
@response.body.should == "Hello"
end
specify "should render argument at /anything" do
get_it '/foo'
@response.should be_ok
@response.body.should == "Hello foo"
get_it '/bar'
@response.should be_ok
@response.body.should == "Hello bar"
end
specify "should not respond to nested paths" do
get_it '/foo/bar'
@response.should_not be_ok
end
end
What I did - basically, all that needed to be done was to bypass the ‘sinatra/test/spec’ file, which in turn requires the test/spec library, which we need to avoid.
Instead, we require the RSpec interop library, and then get the ‘sinatra/test/unit’ file, which sets up a reasonable environment to start testing, including deferring to ‘sinatra/test/methods’ to define the helpers like ‘get_it’, ‘post_it’ and so on. The bonus is that you get to skip the require of test/spec, and get to stick to pure RSpec.
Where to go from here - I would like some input on what other helpers would be cool/useful. I haven’t worked with RSpec all that much (well.. at all), so I don’t know what’s helpful, and what’s not. Leave comments, or hit me up on IRC.
Posted in Programming, Ruby, Sinatra, Testing | 3 Comments »
May 28th, 2008 cschneid
Last night, two of my patches to Sinatra got merged into the main branch. I’ve already written about the erb_locals feature. The other one was a slightly older one to fix development reloading in situations where Sinatra was not called directly.
The situation which triggered the issue:
- $0 (the file actually run by ruby) was not the file where the Sinatra routing and application was defined
- The file that actually was $0 called ‘require’ on the Sinatra application.
- A Sinatra development environment reload happened.
Ruby prevents duplicate loading of libraries by only loading each require once per lifetime of the VM. If you really do want to reload the file, you have to use Kernel.load. Which of course is how Sinatra handles the development environment’s reloading.
Original code:
New code:
# app_file defaults to $0
Kernel.load Sinatra.options.app_file
Now that app_file is an option, it’s easily overridden in the file with your Sinatra application.
Now development reloading works easier, and Sinatra is a better embeddable framework. No changes are needed to existing applications since app_file defaults to $0, the default behavior is exactly the same as the old behavior.
Posted in Programming, Ruby, Sinatra | No Comments »
May 27th, 2008 cschneid
UPDATE: I made another post about how to do RSpec testing without having to patch sinatra.
I haven’t looked into this myself, but thanks the the enterprising jodo on #sinatra, and the mad coding skillz of avdi on github, there is a patch to enable interop between test-spec (default) and rspec.
So give it a try, and see how it goes. Report back on #sinatra, or leave a comment here.
Posted in Programming, Ruby, Sinatra | No Comments »
May 25th, 2008 cschneid
The Sinatra framework has a fairly unmapped deployment landscape. The standard answer is to use Thin or Mongrel, and have a reverse proxy (lighttpd, or nginx, or even Apache) point to your bundle of servers.
But of course that isn’t always possible. Cheap shared hosting (like Dreamhost) won’t let you run Thin or Mongrel, or setup reverse proxies (at least on the shared plan).
Luckily Rack supports various connectors, including CGI and FastCGI. Unluckily for us, FastCGI doesn’t quite work with the current Sinatra git HEAD.
To get a dumb “hello world” Sinatra application up and running on dreamhost involves pulling down the current Sinatra code, and hacking at it a bit. Don’t worry, it’s mostly just commenting out a few lines, and tweaking another line.
Files needed:
- .htaccess
- dispatch.fcgi
- Tweaked sinatra.rb
.htaccess
RewriteEngine on
AddHandler fastcgi-script .fcgi
Options +FollowSymLinks +ExecCGI
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
dispatch.fcgi
#!/usr/bin/ruby
require 'sinatra/lib/sinatra.rb'
require 'rubygems'
fastcgi_log = File.open("fastcgi.log", "a")
STDOUT.reopen fastcgi_log
STDERR.reopen fastcgi_log
STDOUT.sync = true
set :logging, false
set :server, "FastCGI"
module Rack
class Request
def path_info
@env["SCRIPT_URL"].to_s
end
def path_info=(s)
@env["SCRIPT_URL"] = s.to_s
end
end
end
load 'test.rb'
sinatra.rb - Replace this function with the new version here.
def run
begin
#puts "== Sinatra has taken the stage on port #{port} for #{env} with backup by #{server.name}"
require 'pp'
server.run(application) do |server|
trap(:INT) do
server.stop
#puts "\n== Sinatra has ended his set (crowd applauds)"
end
end
rescue Errno::EADDRINUSE => e
#puts "== Someone is already performing on port #{port}!"
end
end
Hopefully that helps somebody get up and running, check out #sinatra on freenode.org to ask questions, but be patient, we’re friendly, but work day jobs.
Posted in Programming, Ruby, Sinatra | No Comments »
May 17th, 2008 cschneid
Sinatra has a great way of defining routing. You define a route, and then define exactly how it responds right in that same spot. Just the right amount of magic! The routes have only a few basic forms, but it’s more than enough to handle most any situation. In my examples I use get, but there’s no reason you can’t use the same routing with post, put, or delete
No option route
get '/about' do
haml :about
end
Single option
get '/thing/:id' do
@thing = Thing.find(params[:id])
haml :thing_show
end
Multiple options
get '/thing/:id/subthing/:subid' do
@thing = Thing.find(params[:id])
@subthing = SubThing.find(params[:subid])
haml :subthing_show
end
Handling file formats
get '/thing.:format' do
case params[:format]
when 'xml'
render_xml
when 'json'
render_json
end
end
Handling full subfolders
This one is ugly. There’s apparently a feature request + possible patch out there (as mentioned on the mailing list, but it hasn’t been implemented yet.
get '/archive/*' do
star_depth = 1 # /archive comes before the *
splat = request.path_info.split('/')[(star_depth + 1)..-1].join('/')
splat
end
Posted in Programming, Ruby, Sinatra | 3 Comments »