root / trunk / lib / camping / reloader.rb

Revision 228, 5.4 kB (checked in by zimbatm, 10 months ago)

Command-line fixes. Ticket #93 solved.

Line 
1module Camping
2# == The Camping Reloader
3#
4# Camping apps are generally small and predictable.  Many Camping apps are
5# contained within a single file.  Larger apps are split into a handful of
6# other Ruby libraries within the same directory.
7#
8# Since Camping apps (and their dependencies) are loaded with Ruby's require
9# method, there is a record of them in $LOADED_FEATURES.  Which leaves a
10# perfect space for this class to manage auto-reloading an app if any of its
11# immediate dependencies changes.
12#
13# == Wrapping Your Apps
14#
15# Since bin/camping and the Camping::FastCGI class already use the Reloader,
16# you probably don't need to hack it on your own.  But, if you're rolling your
17# own situation, here's how.
18#
19# Rather than this:
20#
21#   require 'yourapp'
22#
23# Use this:
24#
25#   require 'camping/reloader'
26#   Camping::Reloader.new('/path/to/yourapp.rb')
27#
28# The reloader will take care of requiring the app and monitoring all files
29# for alterations.
30class Reloader
31    attr_accessor :klass, :mtime, :mount, :requires
32
33    # Creates the reloader, assigns a +script+ to it and initially loads the
34    # application.  Pass in the full path to the script, otherwise the script
35    # will be loaded relative to the current working directory.
36    def initialize(script)
37        @script = File.expand_path(script)
38        @mount = File.basename(script, '.rb')
39        @requires = nil
40        load_app
41    end
42
43    # Find the application, based on the script name.
44    def find_app(title)
45        @klass = Object.const_get(Object.constants.grep(/^#{title}$/i)[0]) rescue nil
46    end
47
48    # If the file isn't found, if we need to remove the app from the global
49    # namespace, this will be sure to do so and set @klass to nil.
50    def remove_app
51        Object.send :remove_const, @klass.name if @klass
52        @klass = nil
53    end
54
55    # Loads (or reloads) the application.  The reloader will take care of calling
56    # this for you.  You can certainly call it yourself if you feel it's warranted.
57    def load_app
58        title = File.basename(@script)[/^([\w_]+)/,1].gsub /_/,''
59        begin
60            all_requires = $LOADED_FEATURES.dup
61            load @script
62            @requires = ($LOADED_FEATURES - all_requires).select do |req|
63                req.index(File.basename(@script) + "/") == 0 || req.index(title + "/") == 0
64            end
65        rescue Exception => e
66            puts "!! trouble loading #{title.inspect}: [#{e.class}] #{e.message}"
67            puts e.backtrace.join("\n")
68            find_app title
69            remove_app
70            return
71        end
72
73        @mtime = mtime
74        find_app title
75        unless @klass and @klass.const_defined? :C
76            puts "!! trouble loading #{title.inspect}: not a Camping app, no #{title.capitalize} module found"
77            remove_app
78            return
79        end
80       
81        Reloader.conditional_connect
82        @klass.create if @klass.respond_to? :create
83        puts "** #{title.inspect} app loaded"
84        @klass
85    end
86
87    # The timestamp of the most recently modified app dependency.
88    def mtime
89        ((@requires || []) + [@script]).map do |fname|
90            fname = fname.gsub(/^#{Regexp::quote File.dirname(@script)}\//, '')
91            begin
92                File.mtime(File.join(File.dirname(@script), fname))
93            rescue Errno::ENOENT
94                remove_app
95                @mtime
96            end
97        end.reject{|t| t > Time.now }.max
98    end
99
100    # Conditional reloading of the app.  This gets called on each request and
101    # only reloads if the modification times on any of the files is updated.
102    def reload_app
103        return if @klass and @mtime and mtime <= @mtime
104
105        if @requires
106            @requires.each { |req| $LOADED_FEATURES.delete(req) }
107        end
108        k = @klass
109        Object.send :remove_const, k.name if k
110        load_app
111    end
112
113    # Conditionally reloads (using reload_app.)  Then passes the request through
114    # to the wrapped Camping app.
115    def run(*a)
116        reload_app
117        if @klass
118            @klass.run(*a)
119        else
120            Camping.run(*a)
121        end
122    end
123
124    # Returns source code for the main script in the application.
125    def view_source
126        File.read(@script)
127    end
128
129    class << self
130        def database=(db)
131            @database = db
132        end
133        def log=(log)
134            @log = log
135        end
136        def conditional_connect
137            # If database models are present, `autoload?` will return nil.
138            unless Camping::Models.autoload? :Base
139                if @database and @database[:adapter] == 'sqlite3'
140                    begin
141                        require 'sqlite3_api'
142                    rescue LoadError
143                        puts "!! Your SQLite3 adapter isn't a compiled extension."
144                        abort "!! Please check out http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for tips."
145                    end
146                end
147
148                case @log
149                when Logger
150                    Camping::Models::Base.logger = @log
151                when String
152                    require 'logger'
153                    Camping::Models::Base.logger = Logger.new(@log == "-" ? STDOUT : @log)
154                end
155
156                Camping::Models::Base.establish_connection @database if @database
157
158                if Camping::Models.const_defined?(:Session)
159                  Camping::Models::Session.create_schema
160                end
161            end
162        end
163    end
164end
165end
Note: See TracBrowser for help on using the browser.