The blog of Rahoul Baruah from 3hv Ltd

What's going on?

My name is Rahoul Baruah (aka Baz) and I'm a software developer in Leeds (England).

This is a log of things I've discovered while writing software in Ruby on Rails. In other words, geek stuff.

However, I've decided to put this blog on ice - I would ask you to check out my business blog here (or subscribe here).

02 December, 2005

Setting up IIS for Rails

This article has now been updated here. It now works on Server 2003 and is much more readable than this hcak job!

These posts are all in the wrong order and I'm writing them down as much so I remember them as anything else. I also am painfully aware that the template for this site is horrible. But I'll get there in the end.

Anyway, I've done a fair amount of Ruby on Rails on Windows, using SQL Server now. I've got some patches, some of which I've submitted, some of which I'm still sitting on. But it's come to the time to try and get my application running on IIS (after wrestling with some firewalls).

First thing - you need FastCGI for IIS, Ionic's Isapi Rewrite Filter and Ruby for IIS.

The former keeps a Ruby process running over multiple requests so you do not get a process startup cost each time someone asks for something. The second takes the URLs supplied by the client browser and rewrites them, so that IIS knows to redirect the request to FastCGI. The latter fixes up some of the dispatching so that the FastCGI dispatcher doesn't simply bomb out.

The instructions detailed below set up Rails as one application per "virtual server". It is possible to set up multiple Rails applications within a server but involves a bit more mucking about with the URL rewriter and your config/routes.rb file. It also blocks off most of the other stuff on that server (again this can be bypassed with some URL rewriting) - however, if you are Server 2003 you can just set up multiple virtual sites. However, having multiple Rails applications on a single (physical) server does require some mucking about with the FastCGI setup.

First of all, install Ruby and Rails (watch out for the problems with having multiple version with Gems - I'll do another post about that one) and check that your application works OK using WEBrick. Debugging with FastCGI running is very very difficult as it does no logging - so make sure things are OK before you embark on FastCGI.

Next up, copy the FastCGI DLL and the Ionic ISAPI DLL and INI file somewhere that IIS can get at them - "C:\InetPub" will do. Open IIS Manager, select Properties on the "Default Web Site" (or whichever) and move to the Home Directory tab. Point the "Local Path" at the public folder of your Rails application and create an application. Then select "Configuration" and under Mappings, click "Add". For the Executable, select your FastCGI.DLL and for the extension, select ".fcgi". This tells IIS that any request for a .fcgi file should not be dealt with by IIS, but instead by FastCGI.DLL.

Back to the Web Site Properties - under ISAPI filters, click Add. For the filter name, enter "URL Rewriter" and for Executable, select the Ionic ISAPI DLL. This tells IIS that every request to this web site ought to be pre-processed by Ionic before being processed by IIS.

Next we need to tell Ionic what pre-processing needs to be done. Open the IsapiRewrite INI file (which needs to be in the same folder as the DLL) - keep the initial comments but you can probably get rid of the rest of the contents. Then add the following:


# Ruby on Rails
IterationLimit 0
RewriteRule ^(/[^.]+)$ /dispatch.fcgi?$1

The first line is a comment. The second tells Ionic to process the URL that IIS supplies once and once only. The third line is the rule. It uses a Regular Expression to match against the supplied URL - the ^ and $ mean match the entire string. The brackets mean "return anything that matches ..." and "/[^.]+" means "anything that starts with a / and has zero or more characters that are not .". If the URL matches this expression, the URL is rewritten to be /dispatch.fcgi?WhateverMatched

So if you are in your browser and type "http://myserver.com/controller/action/id" - IIS receives the request and passes "/controller/action/id" to Ionic. Ionic fires up the rewrite rule, which matches, so it rewrites the URL to "/dispatch.fcgi?controller/action/id". IIS then receives the rewritten URL, realises that it ends in ".fcgi" (as the part after the ? is now a query string and no longer part of the base URL) and invokes FastCGI, passing it "/dispatch.fcgi?/controller/action/id". However, at the moment, FastCGI has no idea what to do with this request.

So next up, we need to configure FastCGI. Fire up trusty RegEdit and move to Local Machine/Software. Create a Key (folder) called "FastCGI". Then underneath that, create another key called ".fcgi". In here enter three string values: "AppPath" (which equals "C:\ruby\bin\rubyw.exe" or the path to your Ruby executable), "Args" (which equals "C:\MyRailsApp\public\dispatch.fcgi" or the path to your applications FastCGI dispatcher) and "BindPath" (which equals "Rails-CGI" - I'm not quite sure what this does but it is needed).

Now, in theory, IIS knows that any requests to this site need to be rewritten by Ionic. Ionic knows to rewrite the URL into a URL for dispatcher.fcgi and a query string that is the original request. IIS then knows to pass the .fcgi request to FastCGI, which in turn knows to invoke RubyW.exe with your application's dispatcher.fcgi as a parameter.

If you try this now (restart IIS, open your browser and type "http://localhost/controller/action" - where controller and action are obviously the names of parts of your application) the browser should sit and wait for ages doing nothing. This is because this is the first time a .fcgi file has been invoked, so FastCGI needs to start up a Ruby process, which takes some time. Don't worry - the point of FastCGI is that you get this startup cost once and subsequent requests are much faster. At some point (if you look in Task Manager you will probably see RubyW.exe starting up) your browser will respond, and more likely than not, you will get a "Recognition failed for '/dispatch.fcgi'" error.

This is where my knowledge of things gets a bit murky. As I understand it, Apache, when using FastCGI, sets up an environment variable called "REQUEST_URI" that FastCGI and Rails use to figure out where to send the request. However, IIS doesn't do that - instead it sets up environment variables called "PATH_INFO", "SCRIPT_NAME" and "QUERY_STRING". In our case, "PATH_INFO" should be "/dispatch.fcgi", "SCRIPT_NAME" should be "/dispatch.fcgi" and "QUERY_STRING" should be "/controller/action". If you look in C:\Ruby\lib\ruby\gems\1.8\gems\actionpack-version\lib\action_controller\request.rb you will see a method called request_uri. This, under Apache and WEBrick, returns the "REQUEST_URI" environment variable. If it is not set, it tries to manipulate the "PATH_INFO" and "SCRIPT_NAME" environment variables to get the same result.

And this is where the problem is. What the method is expecting is a "SCRIPT_NAME" of "/dispatch.fcgi" (which is what we have) but a "PATH_INFO" of "/dispatch.fcgi/controller/action". In other words, instead of extracting the original URL and making it into a query string, it expects the original URL to be tacked onto the end of the dispatcher script. The problem with this is that if the URL looks like that, then the URL no longer ends with .fcgi so IIS does not know to ask FastCGI to process the request. Which is why Ionic rewrites the URL into a script portion and a query string.

However, as I type this up I think I have a fix.

My original fix was taken from one the links that came from this page - however, there was a patch in an earlier version of Rails that was supposed to fix this.

I have modified this fix and things seem to work OK:
Change C:\ruby\lib\ruby\gems\1.8\gems\actionpack-version\lib\action_controller\request.rb to look like this:


# Returns the request URI correctly, taking into account the idiosyncracies
# of the various servers.
def request_uri
if uri = env['REQUEST_URI']
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri # Remove domain, which webrick puts into the request_uri.
else # REQUEST_URI is blank under IIS - get this from PATH_INFO and SCRIPT_NAME
script_filename = env['SCRIPT_NAME'].to_s#.match(%r{[^/]+$})
uri = env['PATH_INFO']
uri = uri.sub("#{script_filename}", "") unless script_filename.nil?
#uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil?
#unless (env_qs = env['QUERY_STRING']).nil? || env_qs.empty?
# uri << '?' << env_qs
#end
uri << env['QUERY_STRING'] unless env['QUERY_STRING'].nil?
uri
end
end


I have left the original lines commented in, but basically I have removed the regular expression matching and used simple string matching instead. It now removes "SCRIPT_NAME" from "PATH_INFO" and then appends the "QUERY_STRING" onto the end, as if it was a raw URL, not an actual query string. I also set the permissions on the config and log folders to "Full Control" to "Everyone" - and now the raw application seems to be working OK.

However, I now get a nil object exception whenever I try and invoke the scaffolding for a web service method. Not sure why (and it works fine using the same code in WEBrick) - but at least things seem to be working OK on the main site front.

Having played a bit further, the web service stuff is a bit shaky full stop. Invoking web methods occasionally results in an AV in some DLL that looks like it is part of IIS - as soon as that happens you have to restart IIS to get any further. Not good.

3 comments:

Anonymous said...

it works! thanks, looking forward to hearing how the web services thing gets fixed (without doing it myself)

Brad said...

thanks, it works. curious to see if someone (who's not me) will fix the web services deal.

Baz said...

Glad I can help. I've got it up and running on Win2K, WinXP and 2000 Server now. My clients are going to be testing over Christmas and then I get a 2003 Server installation.

If it can handle all of that it ought to be pretty robust.

Once I'm over that I'll start looking at the web service stuff, as I want my service to work with nice and easy Ruby models.

eXTReMe Tracker