public
Description: A very fast & simple Ruby web server
Homepage: http://code.macournoyer.com/thin/
Clone URL: git://github.com/macournoyer/thin.git
commit  ce6d00751644715c82044d1194a34fc61ca0823d
tree    f9bac5195ab9a402bf250f621d9b13635dbfbce4
parent  863978d258b5890cf4883eed8b1a81d549352276
thin / lib / thin / controllers / controller.rb
100644 169 lines (139 sloc) 5.759 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
require 'yaml'
 
module Thin
  # Error raised that will abort the process and print not backtrace.
  class RunnerError < RuntimeError; end
  
  # Raised when a mandatory option is missing to run a command.
  class OptionRequired < RunnerError
    def initialize(option)
      super("#{option} option required")
    end
  end
  
  # Raised when an option is not valid.
  class InvalidOption < RunnerError; end
  
  # Build and control Thin servers.
  # Hey Controller pattern is not only for web apps yo!
  module Controllers
    # Controls one Thin server.
    # Allow to start, stop, restart and configure a single thin server.
    class Controller
      include Logging
    
      # Command line options passed to the thin script
      attr_accessor :options
    
      def initialize(options)
        @options = options
        
        if @options[:socket]
          @options.delete(:address)
          @options.delete(:port)
        end
      end
    
      def start
        # Constantize backend class
        @options[:backend] = eval(@options[:backend], TOPLEVEL_BINDING) if @options[:backend]
        
        server = Server.new(@options[:socket] || @options[:address], # Server detects kind of socket
                            @options[:port], # Port ignored on UNIX socket
                            @options)
        
        # Set options
        server.pid_file = @options[:pid]
        server.log_file = @options[:log]
        server.timeout = @options[:timeout]
        server.maximum_connections = @options[:max_conns]
        server.maximum_persistent_connections = @options[:max_persistent_conns]
        server.threaded = @options[:threaded]
 
        # Detach the process, after this line the current process returns
        server.daemonize if @options[:daemonize]
 
        # +config+ must be called before changing privileges since it might require superuser power.
        server.config
        
        server.change_privilege @options[:user], @options[:group] if @options[:user] && @options[:group]
 
        # If a Rack config file is specified we eval it inside a Rack::Builder block to create
        # a Rack adapter from it. Or else we guess which adapter to use and load it.
        if @options[:rackup]
          server.app = load_rackup_config
        else
          server.app = load_adapter
        end
 
        # If a prefix is required, wrap in Rack URL mapper
        server.app = Rack::URLMap.new(@options[:prefix] => server.app) if @options[:prefix]
 
        # If a stats URL is specified, wrap in Stats adapter
        server.app = Stats::Adapter.new(server.app, @options[:stats]) if @options[:stats]
 
        # Register restart procedure which just start another process with same options,
        # so that's why this is done here.
        server.on_restart { Command.run(:start, @options) }
 
        server.start
      end
    
      def stop
        raise OptionRequired, :pid unless @options[:pid]
      
        tail_log(@options[:log]) do
          Server.kill(@options[:pid], @options[:timeout] || 60)
          wait_for_file :deletion, @options[:pid]
        end
      end
    
      def restart
        raise OptionRequired, :pid unless @options[:pid]
      
        tail_log(@options[:log]) do
          Server.restart(@options[:pid])
          wait_for_file :creation, @options[:pid]
        end
      end
    
      def config
        config_file = @options.delete(:config) || raise(OptionRequired, :config)
 
        # Stringify keys
        @options.keys.each { |o| @options[o.to_s] = @options.delete(o) }
 
        File.open(config_file, 'w') { |f| f << @options.to_yaml }
        log ">> Wrote configuration to #{config_file}"
      end
      
      protected
        # Wait for a pid file to either be created or deleted.
        def wait_for_file(state, file)
          case state
          when :creation then sleep 0.1 until File.exist?(file)
          when :deletion then sleep 0.1 while File.exist?(file)
          end
        end
        
        # Tail the log file of server +number+ during the execution of the block.
        def tail_log(log_file)
          if log_file
            tail_thread = tail(log_file)
            yield
            tail_thread.kill
          else
            yield
          end
        end
        
        # Acts like GNU tail command. Taken from Rails.
        def tail(file)
          cursor = File.exist?(file) ? File.size(file) : 0
          last_checked = Time.now
          tail_thread = Thread.new do
            Thread.pass until File.exist?(file)
            File.open(file, 'r') do |f|
              loop do
                f.seek cursor
                if f.mtime > last_checked
                  last_checked = f.mtime
                  contents = f.read
                  cursor += contents.length
                  print contents
                  STDOUT.flush
                end
                sleep 0.1
              end
            end
          end
          sleep 1 if File.exist?(file) # HACK Give the thread a little time to open the file
          tail_thread
        end
 
      private
        def load_adapter
          adapter = @options[:adapter] || Rack::Adapter.guess(@options[:chdir])
          log ">> Using #{adapter} adapter"
          Rack::Adapter.for(adapter, @options)
        rescue Rack::AdapterNotFound => e
          raise InvalidOption, e.message
        end
        
        def load_rackup_config
          rackup_code = File.read(@options[:rackup])
          eval("Rack::Builder.new {( #{rackup_code}\n )}.to_app", TOPLEVEL_BINDING, @options[:rackup])
        end
    end
  end
end