| 
			   | 
			
| Line 1: | 
Line 1: | 
| − | == Region Init Script ==  | + | == OpenSim Init Scripts ==  | 
| − | The following is an init script that I wrote for starting, stopping, and monitoring OpenSim on Linux systems.  It makes some assumptions, and is only going to work (unmoddified) if you stay with those assumptions:
  | + |  | 
| − | * Your opensim instance is in /opt/opensim/staging/bin (that's an easy change)
  | + |  | 
| − | * You are only running 1 OpenSim.exe process per machine (that's '''not''' easy to change)
  | + |  | 
| − | * You are using mysql (that's easy to change)
  | + |  | 
| − | * Production mono is in /usr/local/bin (that's easy to change)
  | + |  | 
| − | While it may not be suitable for all environments, it has been very useful for the environments that I run.
  | + |  | 
|   |  |   |  | 
| − | <source lang="ruby">
  | + | There is now a forge project for [http://forge.opensimulator.org/gf/linuxservice|Linux Service Scripts for OpenSim] that contains init scripts for running OpenSim.  | 
| − | #!/usr/bin/ruby
  | + |  | 
| − |    | + |  | 
| − | # Start or stop OpenSim
  | + |  | 
| − | #
  | + |  | 
| − | # Sean Dague <sdague@gmail.com>
  | + |  | 
| − |    | + |  | 
| − | ### BEGIN INIT INFO
  | + |  | 
| − | # Provides:          opensim
  | + |  | 
| − | # Required-Start:    $local_fs $remote_fs $syslog $named $network $time $mysql
  | + |  | 
| − | # Required-Stop:     $local_fs $remote_fs $syslog $named $network
  | + |  | 
| − | # Default-Start:     2 3 4 5
  | + |  | 
| − | # Default-Stop:      0 1 6
  | + |  | 
| − | # Short-Description: start and stop opensim
  | + |  | 
| − | # Description:       opensim init script
  | + |  | 
| − | ### END INIT INFO
  | + |  | 
| − |    | + |  | 
| − | require "rexml/document"
  | + |  | 
| − | require "open-uri"
  | + |  | 
| − | include REXML
  | + |  | 
| − |    | + |  | 
| − | OpenSimPath = "/opt/opensim/staging/bin"
  | + |  | 
| − | Dir.chdir(OpenSimPath)
  | + |  | 
| − |    | + |  | 
| − | @host = "localhost"
  | + |  | 
| − | @port = 9000
  | + |  | 
| − | @uuid = "0"
  | + |  | 
| − |    | + |  | 
| − | def parse_config
  | + |  | 
| − |     # first we figure out the external hostname of a region on the box
  | + |  | 
| − |     Dir.new("#{OpenSimPath}/Regions").find do |f|
  | + |  | 
| − |         if f =~ /\.xml$/
  | + |  | 
| − |             doc = Document.new(File.new("#{OpenSimPath}/Regions/#{f}"))
  | + |  | 
| − |             
  | + |  | 
| − |             if doc.root.elements['Config'].attributes['external_host_name']
  | + |  | 
| − |                 @host = doc.root.elements['Config'].attributes['external_host_name']
  | + |  | 
| − |             else
  | + |  | 
| − |                 @host = doc.root.elements['Config'].elements['external_host_name'].text
  | + |  | 
| − |             end
  | + |  | 
| − |        
  | + |  | 
| − |             if doc.root.elements['Config'].attributes['sim_UUID']
  | + |  | 
| − |                 @uuid = doc.root.elements['Config'].attributes['sim_UUID'].gsub(/-/,'')
  | + |  | 
| − |             else
  | + |  | 
| − |                 @uuid = doc.root.elements['Config'].elements['sim_UUID'].text.gsub(/-/,'')
  | + |  | 
| − |             end
  | + |  | 
| − |             
  | + |  | 
| − |             break
  | + |  | 
| − |         end
  | + |  | 
| − |     end
  | + |  | 
| − |     # then we figure out the external port from opensim.ini
  | + |  | 
| − |     open("#{OpenSimPath}/OpenSim.ini") do |f|
  | + |  | 
| − |         f.each do |line|
  | + |  | 
| − |             if line =~ /^\s*http_listener_port/
  | + |  | 
| − |                 (key, equal, port) = line.split
  | + |  | 
| − |                 if port
  | + |  | 
| − |                     @port = port
  | + |  | 
| − |                 end
  | + |  | 
| − |             end
  | + |  | 
| − |         end
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def alive?
  | + |  | 
| − |     begin
  | + |  | 
| − |         open("http://#{@host}:#{@port}/index.php?method=regionImage#{@uuid}") do |f|
  | + |  | 
| − |             resp = f.read
  | + |  | 
| − |             return true
  | + |  | 
| − |         end
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |     end
  | + |  | 
| − |     return false
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def pidof(item)
  | + |  | 
| − |     IO.popen("ps auxw | grep #{item} | grep -v grep") do |line|
  | + |  | 
| − |         line.each do |l|
  | + |  | 
| − |             return l.split[1].to_i
  | + |  | 
| − |         end
  | + |  | 
| − |     end
  | + |  | 
| − |     return nil
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def opensim_pid
  | + |  | 
| − |     return pidof("OpenSim.exe")
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def status
  | + |  | 
| − |     if alive?
  | + |  | 
| − |         puts "OK"
  | + |  | 
| − |         exit 0
  | + |  | 
| − |     else
  | + |  | 
| − |         puts "FAILED"
  | + |  | 
| − |         exit 1
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def start
  | + |  | 
| − |     if alive?
  | + |  | 
| − |         puts "OpenSim already running"
  | + |  | 
| − |         exit 0
  | + |  | 
| − |     end
  | + |  | 
| − |     if opensim_pid != nil
  | + |  | 
| − |         # opensim process is running, but not responding, we're hung
  | + |  | 
| − |         stop
  | + |  | 
| − |         # this really is needed otherwise you get a wedge
  | + |  | 
| − |         system("/etc/init.d/mysql restart")
  | + |  | 
| − |         sleep 10
  | + |  | 
| − |         # purge the addin cache, this may have been the reason for the hang
  | + |  | 
| − |         system("rm -rf addin-db-*")
  | + |  | 
| − |     end
  | + |  | 
| − |     system("screen -wipe")
  | + |  | 
| − |     system("screen -d -S opensim -m /usr/local/bin/mono --debug OpenSim.exe")
  | + |  | 
| − |     
  | + |  | 
| − |     20.times do |t|
  | + |  | 
| − |         if alive?
  | + |  | 
| − |             return
  | + |  | 
| − |         end
  | + |  | 
| − |         sleep 5
  | + |  | 
| − |     end
  | + |  | 
| − |     
  | + |  | 
| − |     puts "FAILED to start region"
  | + |  | 
| − |     exit 1
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def stop
  | + |  | 
| − |     if opensim_pid == nil
  | + |  | 
| − |         puts "OpenSim not running"
  | + |  | 
| − |         exit 0
  | + |  | 
| − |     end
  | + |  | 
| − |     system("screen -wipe")
  | + |  | 
| − |     system("screen -S opensim -X eval 'stuff \"shutdown\015\"'")
  | + |  | 
| − |     puts "Shutting down... waiting 30 seconds"
  | + |  | 
| − |     30.times do |i|
  | + |  | 
| − |         if opensim_pid == nil
  | + |  | 
| − |             return 
  | + |  | 
| − |         end
  | + |  | 
| − |         $stderr.write "."
  | + |  | 
| − |         sleep 1
  | + |  | 
| − |     end
  | + |  | 
| − |     
  | + |  | 
| − |     pid = opensim_pid
  | + |  | 
| − |     if pid
  | + |  | 
| − |         puts "Process not responding, forcing shutdown"
  | + |  | 
| − |         Process.kill(9, pid)
  | + |  | 
| − |     end
  | + |  | 
| − |     
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def main
  | + |  | 
| − |     parse_config
  | + |  | 
| − |     
  | + |  | 
| − |     mode = ARGV[0]
  | + |  | 
| − |     
  | + |  | 
| − |     if mode == "start"
  | + |  | 
| − |         start
  | + |  | 
| − |     elsif mode == "stop"
  | + |  | 
| − |         stop
  | + |  | 
| − |     elsif mode == "status"
  | + |  | 
| − |         status
  | + |  | 
| − |     elsif mode == "process"
  | + |  | 
| − |         puts opensim_pid
  | + |  | 
| − |     elsif mode == "restart"
  | + |  | 
| − |         puts "called restart"
  | + |  | 
| − |     else
  | + |  | 
| − |         puts "unknown"
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | main
  | + |  | 
| − | </source>
  | + |  | 
| − |    | + |  | 
| − | == Grid Init Script ==
  | + |  | 
| − | '''WARNING''' this is untested, so it may blow up on you.
  | + |  | 
| − | <source lang="ruby">
  | + |  | 
| − | #!/usr/bin/ruby
  | + |  | 
| − |    | + |  | 
| − | # Start or stop OpenSim
  | + |  | 
| − | #
  | + |  | 
| − | # Sean Dague <sdague@gmail.com>
  | + |  | 
| − |    | + |  | 
| − | ### BEGIN INIT INFO
  | + |  | 
| − | # Provides:          opensim-grid
  | + |  | 
| − | # Required-Start:    $local_fs $remote_fs $syslog $named $network $time $mysql
  | + |  | 
| − | # Required-Stop:     $local_fs $remote_fs $syslog $named $network
  | + |  | 
| − | # Default-Start:     2 3 4 5
  | + |  | 
| − | # Default-Stop:      0 1 6
  | + |  | 
| − | # Short-Description: start and stop opensim-grid
  | + |  | 
| − | # Description:       opensim-grid init script
  | + |  | 
| − | ### END INIT INFO
  | + |  | 
| − |    | + |  | 
| − | require "rexml/document"
  | + |  | 
| − | require "open-uri"
  | + |  | 
| − | include REXML
  | + |  | 
| − |    | + |  | 
| − | OpenSimPath = "/opt/opensim/staging/bin"
  | + |  | 
| − | Dir.chdir(OpenSimPath)
  | + |  | 
| − |    | + |  | 
| − | @host = "localhost"
  | + |  | 
| − | @gridport = nil
  | + |  | 
| − | @userport = nil
  | + |  | 
| − | @invport = nil
  | + |  | 
| − | @assetport = nil
  | + |  | 
| − | @messageport = nil
  | + |  | 
| − |    | + |  | 
| − | GridExe = "OpenSim.Grid.GridServer.exe"
  | + |  | 
| − | UserExe = "OpenSim.Grid.UserServer.exe"
  | + |  | 
| − | AssetExe = "OpenSim.Grid.AssetServer.exe"
  | + |  | 
| − | InventoryExe = "OpenSim.Grid.InventoryServer.exe"
  | + |  | 
| − | MessagingExe = "OpenSim.Grid.MessagingServer.exe"
  | + |  | 
| − |    | + |  | 
| − | def parse_config
  | + |  | 
| − |     # Grid 
  | + |  | 
| − |     begin 
  | + |  | 
| − |         doc = Document.new(File.new("#{OpenSimPath}/GridServer_Config.xml"))
  | + |  | 
| − |         @gridport = doc.root.elements['Config'].attributes['http_port']
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |     end
  | + |  | 
| − |    | + |  | 
| − |     # User 
  | + |  | 
| − |     begin
  | + |  | 
| − |         doc = Document.new(File.new("#{OpenSimPath}/UserServer_Config.xml"))
  | + |  | 
| − |         @userport = doc.root.elements['Config'].attributes['http_port']
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |     end
  | + |  | 
| − |     
  | + |  | 
| − |     # Inventory 
  | + |  | 
| − |     begin
  | + |  | 
| − |         doc = Document.new(File.new("#{OpenSimPath}/InventoryServer_Config.xml"))
  | + |  | 
| − |         @invport = doc.root.elements['Config'].attributes['http_port']
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |     end
  | + |  | 
| − |     
  | + |  | 
| − |     # Asset 
  | + |  | 
| − |     begin
  | + |  | 
| − |         doc = Document.new(File.new("#{OpenSimPath}/AssetServer_Config.xml"))
  | + |  | 
| − |         @assetport = doc.root.elements['Config'].attributes['http_port']
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |     end
  | + |  | 
| − |     
  | + |  | 
| − |     # Messaging 
  | + |  | 
| − |     begin
  | + |  | 
| − |         doc = Document.new(File.new("#{OpenSimPath}/MessagingServer_Config.xml"))
  | + |  | 
| − |         @messageport = doc.root.elements['Config'].attributes['http_port']
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def grid_alive?
  | + |  | 
| − |     if not @gridport
  | + |  | 
| − |         return false
  | + |  | 
| − |     end
  | + |  | 
| − |     # we actually only care if we cause an exception, which means the grid server is bust
  | + |  | 
| − |     begin
  | + |  | 
| − |         xml = open("http://#{@host}:#{@gridport}/sims/fc036b80fed84b3096a50569b60da265").read
  | + |  | 
| − |         return true
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |         return false
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def user_alive?
  | + |  | 
| − |     if not @userport
  | + |  | 
| − |        return false 
  | + |  | 
| − |     end
  | + |  | 
| − |     # we actually only care if we cause an exception, which means the grid server is bust
  | + |  | 
| − |     begin
  | + |  | 
| − |         xml = open("http://#{@host}:#{@userport}/get_grid_info").read
  | + |  | 
| − |         return true
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |         return false
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def asset_alive?
  | + |  | 
| − |     if not @assetport
  | + |  | 
| − |         return false
  | + |  | 
| − |     end
  | + |  | 
| − |     begin
  | + |  | 
| − |         # fetch the moon
  | + |  | 
| − |         xml = open("http://#{@host}:#{@assetport}/assets/ec4b9f0b-d008-45c6-96a4-01dd947ac621").read
  | + |  | 
| − |         # we need to check that we got something, because everything returns success, even if it didn't work
  | + |  | 
| − |         if xml.size > 0
  | + |  | 
| − |             return true
  | + |  | 
| − |         else
  | + |  | 
| − |             return false
  | + |  | 
| − |         end
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |         return false
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def inv_alive?
  | + |  | 
| − |     if not @userport
  | + |  | 
| − |        return false 
  | + |  | 
| − |     end
  | + |  | 
| − |     return true
  | + |  | 
| − |     # THERE IS CURRENTLY NO EASY CALL FOR INV SERVER
  | + |  | 
| − |     # we actually only care if we cause an exception, which means the grid server is bust
  | + |  | 
| − |     begin
  | + |  | 
| − |         xml = open("http://#{@host}:#{@port}/get_grid_info").read
  | + |  | 
| − |         return true
  | + |  | 
| − |     rescue => e
  | + |  | 
| − |         return false
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def message_alive?
  | + |  | 
| − |     if not @messageport
  | + |  | 
| − |         return false
  | + |  | 
| − |     end
  | + |  | 
| − |     return true
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − |    | + |  | 
| − | def pidof(item)
  | + |  | 
| − |     IO.popen("ps auxw | grep #{item} | grep -v grep") do |line|
  | + |  | 
| − |         line.each do |l|
  | + |  | 
| − |             return l.split[1].to_i
  | + |  | 
| − |         end
  | + |  | 
| − |     end
  | + |  | 
| − |     return nil
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def grid_pid
  | + |  | 
| − |     return pidof(GridExe)
  | + |  | 
| − | end
  | + |  | 
| − | def user_pid
  | + |  | 
| − |     return pidof(UserExe)
  | + |  | 
| − | end
  | + |  | 
| − | def asset_pid
  | + |  | 
| − |     return pidof(AssetExe)
  | + |  | 
| − | end
  | + |  | 
| − | def inv_pid
  | + |  | 
| − |     return pidof(InventoryExe)
  | + |  | 
| − | end
  | + |  | 
| − | def message_pid
  | + |  | 
| − |     return pidof(MessagingExe)
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def failme(msg="")
  | + |  | 
| − |     puts "FAIL - #{msg}"
  | + |  | 
| − |     exit 1
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def status
  | + |  | 
| − |     if @gridport and (not grid_alive?)
  | + |  | 
| − |         failme("Grid server not responding")
  | + |  | 
| − |     end
  | + |  | 
| − |     if @userport and (not user_alive?)
  | + |  | 
| − |         failme("User server not responding")
  | + |  | 
| − |     end
  | + |  | 
| − |     if @assetport and (not asset_alive?)
  | + |  | 
| − |         failme("Asset server not responding")
  | + |  | 
| − |     end
  | + |  | 
| − |     if @invport and (not inv_alive?)
  | + |  | 
| − |         failme("Inventory server not responding")
  | + |  | 
| − |     end
  | + |  | 
| − |     if @messageport and (not message_alive?)
  | + |  | 
| − |         failme("Grid server not responding")
  | + |  | 
| − |     end
  | + |  | 
| − |     puts "OK"
  | + |  | 
| − |     exit 0
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def start_service(name, exe)
  | + |  | 
| − |     if pidof(exe) != nil
  | + |  | 
| − |         puts("#{exe} already running")
  | + |  | 
| − |         return
  | + |  | 
| − |     end
  | + |  | 
| − |     system("screen -d -S #{name} -m /usr/local/bin/mono --debug #{exe}")
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def start
  | + |  | 
| − |     # Grid
  | + |  | 
| − |     start_service("grid", GridExe)
  | + |  | 
| − |     10.times do |d|
  | + |  | 
| − |         if grid_alive?
  | + |  | 
| − |             break
  | + |  | 
| − |         end
  | + |  | 
| − |         sleep 2
  | + |  | 
| − |     end
  | + |  | 
| − |     if not grid_alive?
  | + |  | 
| − |         failme("Grid server did not start")
  | + |  | 
| − |     end
  | + |  | 
| − |    | + |  | 
| − |     # User
  | + |  | 
| − |     start_service("user", UserExe)
  | + |  | 
| − |     10.times do |d|
  | + |  | 
| − |         if user_alive?
  | + |  | 
| − |             break
  | + |  | 
| − |         end
  | + |  | 
| − |         sleep 2
  | + |  | 
| − |     end
  | + |  | 
| − |     if not user_alive?
  | + |  | 
| − |         failme("User server did not start")
  | + |  | 
| − |     end
  | + |  | 
| − |    | + |  | 
| − |     # Asset
  | + |  | 
| − |     start_service("asset", AssetExe)
  | + |  | 
| − |     10.times do |d|
  | + |  | 
| − |         if asset_alive?
  | + |  | 
| − |             break
  | + |  | 
| − |         end
  | + |  | 
| − |         sleep 2
  | + |  | 
| − |     end
  | + |  | 
| − |     if not asset_alive?
  | + |  | 
| − |         failme("Asset server did not start")
  | + |  | 
| − |     end
  | + |  | 
| − |    | + |  | 
| − |     # Inventory
  | + |  | 
| − |     start_service("inventory", InventoryExe)
  | + |  | 
| − |     10.times do |d|
  | + |  | 
| − |         if inv_alive?
  | + |  | 
| − |             break
  | + |  | 
| − |         end
  | + |  | 
| − |         sleep 2
  | + |  | 
| − |     end
  | + |  | 
| − |     if not inv_alive?
  | + |  | 
| − |         failme("Inventory server did not start")
  | + |  | 
| − |     end
  | + |  | 
| − |     
  | + |  | 
| − |     # sleep an extra 2 because we didn't sleep at all here
  | + |  | 
| − |     sleep 2
  | + |  | 
| − |     # Messaging
  | + |  | 
| − |     start_service("messaging", MessagingExe)
  | + |  | 
| − |     10.times do |d|
  | + |  | 
| − |         if message_alive?
  | + |  | 
| − |             break
  | + |  | 
| − |         end
  | + |  | 
| − |         sleep 2
  | + |  | 
| − |     end
  | + |  | 
| − |     if not message_alive?
  | + |  | 
| − |         failme("Messaging server did not start")
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def stop_service(name, exe)
  | + |  | 
| − |     if pidof(exe) == nil
  | + |  | 
| − |         puts "WARNING - #{exe} not running"
  | + |  | 
| − |     end
  | + |  | 
| − |     system("screen -S #{name} -X eval 'stuff \"shutdown\015\"'")
  | + |  | 
| − |     puts "Shutting down... waiting 30 seconds"
  | + |  | 
| − |     30.times do |i|
  | + |  | 
| − |         if pidof(exe) == nil
  | + |  | 
| − |             # just puts out a newline
  | + |  | 
| − |             puts ""
  | + |  | 
| − |             return 
  | + |  | 
| − |         end
  | + |  | 
| − |         $stderr.write "."
  | + |  | 
| − |         sleep 1
  | + |  | 
| − |     end
  | + |  | 
| − |     
  | + |  | 
| − |     pid = pidof(exe)
  | + |  | 
| − |     if pid
  | + |  | 
| − |         puts "Process not responding, forcing shutdown"
  | + |  | 
| − |         Process.kill(9, pid)
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def stop
  | + |  | 
| − |     stop_service("messaging",MessagingExe)
  | + |  | 
| − |     stop_service("inventory",InventoryExe)
  | + |  | 
| − |     stop_service("asset",AssetExe)
  | + |  | 
| − |     stop_service("user",UserExe)
  | + |  | 
| − |     stop_service("grid",GridExe)
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def process
  | + |  | 
| − |     puts "Grid #{grid_pid}"
  | + |  | 
| − |     puts "User #{user_pid}"
  | + |  | 
| − |     puts "Asset #{asset_pid}"
  | + |  | 
| − |     puts "Inventory #{inv_pid}"
  | + |  | 
| − |     puts "Messaging #{message_pid}"
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | def main
  | + |  | 
| − |     parse_config
  | + |  | 
| − |     
  | + |  | 
| − |     mode = ARGV[0]
  | + |  | 
| − |     
  | + |  | 
| − |     if mode == "start"
  | + |  | 
| − |         start
  | + |  | 
| − |     elsif mode == "stop"
  | + |  | 
| − |         stop
  | + |  | 
| − |     elsif mode == "status"
  | + |  | 
| − |         status
  | + |  | 
| − |     elsif mode == "process"
  | + |  | 
| − |         process
  | + |  | 
| − |     elsif mode == "restart"
  | + |  | 
| − |         puts "called restart"
  | + |  | 
| − |     else
  | + |  | 
| − |         puts "unknown"
  | + |  | 
| − |     end
  | + |  | 
| − | end
  | + |  | 
| − |    | + |  | 
| − | main
  | + |  | 
| − | </source>
  | + |  |