picnic 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/picnic/cli.rb ADDED
@@ -0,0 +1,112 @@
1
+ require 'optparse'
2
+
3
+ module Picnic
4
+ # Provides a command-line interface for your app.
5
+ # This is useful for creating a 'bin' file for launching your application.
6
+ #
7
+ # Usage example (put this in a file called 'foo'):
8
+ #
9
+ # #!/usr/bin/env ruby
10
+ #
11
+ # require 'rubygems'
12
+ # require 'picnic'
13
+ #
14
+ # require 'picnic/cli'
15
+ #
16
+ # cli = Picnic::Cli.new(
17
+ # 'foo',
18
+ # :app_path => File.expand_path(File.dirname(File.expand_path(__FILE__)))
19
+ # )
20
+ #
21
+ # cli.handle_cli_input
22
+ #
23
+ # Also see the ServiceControl class for info on how to use your cli script
24
+ # as a service.
25
+ class Cli
26
+ attr_accessor :app, :options
27
+
28
+ # Creates a new command-line interface handler.
29
+ #
30
+ # +app+:: The name of the application. This should match the name of the
31
+ # binary, which by default is expected to be in the same directory
32
+ # as the service control script.
33
+ # +options+:: A hash overriding default options. The options are:
34
+ # +app_path+:: The path to your application's main Ruby file.
35
+ # By default this is expected to be <tt>../lib/<app>.rb</tt>
36
+ # +pid_file+:: Where the app's PID file (containing the app's
37
+ # process ID) should be placed. By default this is
38
+ # <tt>/etc/<app>/<app>.pid</tt>
39
+ # +verbose+:: True if the cli handler should report
40
+ # everything that it's doing to STDOUT.
41
+ def initialize(app, options = {})
42
+ @app = app
43
+
44
+ @options = {}
45
+ @options[:app_path] ||= File.expand_path(File.dirname(File.expand_path(__FILE__))+"/../lib/#{app}.rb")
46
+ @options[:pid_file] ||= "/etc/#{app}/#{app}.pid"
47
+ @options[:conf_file] ||= nil
48
+ @options[:verbose] ||= false
49
+
50
+ @options = options
51
+ end
52
+
53
+ # Parses command line options given to the script.
54
+ def handle_cli_input
55
+ if File.exists? options[:app_path]
56
+ # try to use given app base path
57
+ $: << File.dirname(options[:app_path])
58
+ path = File.dirname(options[:app_path])+"/"
59
+ else
60
+ # fall back to using gem installation
61
+ path = ""
62
+ require 'rubygems'
63
+ gem(app)
64
+ end
65
+
66
+ $PID_FILE = "/etc/#{app}/#{app}.pid"
67
+
68
+ OptionParser.new do |opts|
69
+ opts.banner = "Usage: #{app} [options]"
70
+
71
+ opts.on("-c", "--config FILE", "Use config file (default is /etc/#{app}/config.yml)") do |c|
72
+ puts "Using config file #{c}"
73
+ $CONFIG_FILE = c
74
+ end
75
+
76
+ opts.on("-d", "--daemonize", "Run as a daemon (only when using webrick or mongrel)") do |c|
77
+ $DAEMONIZE = true
78
+ end
79
+
80
+ opts.on("-P", "--pid_file FILE", "Use pid file (default is /etc/#{app}/#{app}.pid)") do |c|
81
+ if $DAEMONIZE && !File.exists?(c)
82
+ puts "Using pid file '#{c}'"
83
+ $PID_FILE = c
84
+ elsif File.exists?(c)
85
+ puts "The pid file already exists. Is #{app} running?\n" +
86
+ "You will have to first manually remove the pid file at '#{c}' to start the server as a daemon."
87
+ exit 1
88
+ else
89
+ puts "Not running as daemon. Ignoring pid option"
90
+ end
91
+ end
92
+
93
+ opts.on_tail("-h", "--help", "Show this message") do
94
+ puts opts
95
+ exit
96
+ end
97
+
98
+ opts.on_tail("-V", "--version", "Show version number") do
99
+ require "#{path}/lib/#{app}/version"
100
+ puts "#{app}-#{VERSION::STRING}"
101
+ exit
102
+ end
103
+ end.parse!
104
+
105
+ $APP_PATH = options[:app_path]
106
+
107
+ load "#{path}/lib/#{app}.rb"
108
+ end
109
+ end
110
+ end
111
+
112
+
@@ -0,0 +1,133 @@
1
+ module Picnic
2
+ # Provides an interface for accessing your Picnic app's configuration file.
3
+ #
4
+ # Usage example:
5
+ #
6
+ # # Load the configuration from /etc/foo/config.yml
7
+ # Conf.load('foo')
8
+ #
9
+ # # The contents of config.yml is now available as follows:
10
+ # puts Conf[:server]
11
+ # puts Conf[:authentication][:username]
12
+ # # ... etc.
13
+ class Conf
14
+ $CONF ||= HashWithIndifferentAccess.new
15
+ $CONF[:log] ||= HashWithIndifferentAccess.new
16
+ $CONF[:log][:file] ||= STDOUT
17
+ $CONF[:log][:level] ||= 'DEBUG'
18
+ $CONF[:uri_path] ||= "/"
19
+
20
+ # Read a configuration option.
21
+ #
22
+ # For example:
23
+ # puts Conf[:server]
24
+ def self.[](key)
25
+ $CONF[key]
26
+ end
27
+
28
+ # Another way of reading a configuration option.
29
+ #
30
+ # The following statements are equivalent:
31
+ # puts Conf[:server]
32
+ # puts Conf.server
33
+ def self.method_missing(method, *args)
34
+ self[method]
35
+ end
36
+
37
+ # Returns the path to your application's example config file.
38
+ #
39
+ # The example config file should be in the root directory of
40
+ # your application's distribution package and should be called
41
+ # <tt>config.example.yml</tt>. This file is used as a template
42
+ # for your app's configuration, to be customized by the end
43
+ # user.
44
+ def self.example_config_file_path
45
+ if $APP_PATH
46
+ app_path = File.expand_path($APP_PATH)
47
+ else
48
+ caller.last =~ /^(.*?):\d+$/
49
+ app_path = File.dirname(File.expand_path($1))
50
+ end
51
+ app_path+'/../config.example.yml'
52
+ end
53
+
54
+ # Copies the example config file into the appropriate
55
+ # configuration directory.
56
+ #
57
+ # +app+:: The name of your application. For example: <tt>foo</tt>
58
+ # +dest_conf_file:: The path where the example conf file should be copied to.
59
+ # For example: <tt>/etc/foo/config.yml</tt>
60
+ def self.copy_example_config_file(app, dest_conf_file)
61
+ require 'fileutils'
62
+
63
+ example_conf_file = example_config_file_path
64
+
65
+ puts "\n#{app.to_s.upcase} SERVER HAS NOT YET BEEN CONFIGURED!!!\n"
66
+ puts "\nAttempting to copy sample configuration from '#{example_conf_file}' to '#{dest_conf_file}'...\n"
67
+
68
+ unless File.exists? example_conf_file
69
+ puts "\nThe example conf file does not exist! The author of #{app} may have forgotten to include it. You'll have to create the config file manually.\n"
70
+ exit 2
71
+ end
72
+
73
+ begin
74
+ dest_conf_file_dir = File.dirname(dest_conf_file)
75
+ FileUtils.mkpath(dest_conf_file_dir) unless File.exists? dest_conf_file_dir
76
+ FileUtils.cp(example_conf_file, dest_conf_file)
77
+ rescue Errno::EACCES
78
+ puts "\nIt appears that you do not have permissions to create the '#{dest_conf_file}' file. Try running this command using sudo (as root).\n"
79
+ exit 2
80
+ rescue => e
81
+ puts "\nFor some reason the '#{dest_conf_file}' file could not be created (#{e})."
82
+ puts "You'll have to copy the file manually. Use '#{example_conf_file}' as a template.\n"
83
+ exit 2
84
+ end
85
+
86
+ puts "\nA sample configuration has been created for you in '#{dest_conf_file}'. Please edit this file to" +
87
+ " suit your needs and then run #{app} again.\n"
88
+ exit 1
89
+ end
90
+
91
+ # Loads the configuration from the yaml file for the given app.
92
+ #
93
+ # <tt>app</tt> should be the name of your app; for example: <tt>foo</tt>.
94
+ #
95
+ # By default, the configuration will be loaded from <tt>/etc/<app>/config.yml</tt>.
96
+ # You can override this by setting a global <tt>$CONFIG_FILE</tt> variable.
97
+ def self.load(app)
98
+
99
+ conf_file = $CONFIG_FILE || "/etc/#{app.to_s.downcase}/config.yml"
100
+
101
+ puts "Loading configuration for #{app} from '#{conf_file}'..."
102
+
103
+ begin
104
+ conf_file = etc_conf = conf_file
105
+ unless File.exists? conf_file
106
+ # can use local config.yml file in case we're running non-gem installation
107
+ conf_file = File.dirname(File.expand_path(__FILE__))+"/../../config.yml"
108
+ end
109
+
110
+ unless File.exists? conf_file
111
+ copy_example_config_file(app, etc_conf)
112
+ end
113
+
114
+ loaded_conf = HashWithIndifferentAccess.new(YAML.load_file(conf_file))
115
+
116
+ if $CONF
117
+ $CONF = HashWithIndifferentAccess.new($CONF)
118
+ $CONF = $CONF.merge(loaded_conf)
119
+ else
120
+ $CONF = loaded_conf
121
+ end
122
+
123
+ $CONF[:log][:file] = STDOUT unless $CONF[:log][:file]
124
+
125
+ rescue
126
+ raise "Your #{app} configuration may be invalid."+
127
+ " Please double-check check your config.yml file."+
128
+ " Make sure that you are using spaces instead of tabs for your indentation!!" +
129
+ "\n\nTHE UNDERLYING ERROR WAS:\n#{$!}"
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,31 @@
1
+ module Picnic
2
+ module Controllers
3
+ # Provides a controller for serving up static content from your app's <tt>/public</tt> directory.
4
+ # This can be used to serve css, js, jpg, png, and gif files. Anything you put in your app's
5
+ # '/public' directory will be served up under the '/public' path.
6
+ #
7
+ # That is, say you have:
8
+ # /srv/www/camping/my_app/public/test.jpg
9
+ # This should be availabe at:
10
+ # http://myapp.com/public/test.jpg
11
+ #
12
+ # This controller is automatically enabled for all Picnic-enabled apps.
13
+ class Public < Camping::Controllers::R '/public/(.+)'
14
+ BASE_PATH = ("#{$APP_PATH}/.." || File.expand_path(File.dirname(__FILE__)))+'/lib/public'
15
+
16
+ MIME_TYPES = {'.css' => 'text/css', '.js' => 'text/javascript',
17
+ '.jpg' => 'image/jpeg', '.png' => 'image/png',
18
+ '.gif' => 'image/gif'}
19
+
20
+ def get(path)
21
+ @headers['Content-Type'] = MIME_TYPES[path[/\.\w+$/, 0]] || "text/plain"
22
+ unless path.include? ".." # prevent directory traversal attacks
23
+ @headers['X-Sendfile'] = "#{BASE_PATH}/#{path}"
24
+ else
25
+ @status = "403"
26
+ "403 - Invalid path"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,227 @@
1
+ module Picnic
2
+ # In Camping, "postambles" are wrappers used to make your application run under
3
+ # some given web server. They are called "postambles" because traditionally this
4
+ # was a piece of code included at the bottom of your Camping script. Picnic
5
+ # provides postambles for two popular Ruby-based web servers: webrick and mongrel.
6
+ # These postambles take care of all the complications of launching and managing
7
+ # the web server for you.
8
+ module Postambles
9
+
10
+ # Launches your Camping application using a webrick server.
11
+ #
12
+ # It is possible to run using SSL by providing an <tt>:ssl_cert</tt> path in your
13
+ # app's configuration file, pointing to a valid SSL certificate file.
14
+ #
15
+ # If $DAEMONIZE is true, the webrick server will be forked to a background process.
16
+ # Note that this may have some strange effects, since any open IOs or threads will not
17
+ # be carried over to the forked daemon process.
18
+ #
19
+ # Module#start_picnic automatically calls this method to launch your Picnic app if
20
+ # your app's <tt>:server</tt> configuration option is set to <tt>'webrick'</tt>.
21
+ #
22
+ # Usage example:
23
+ #
24
+ # require 'picnic/postambles'
25
+ # self.extend self::Postambles
26
+ # webrick
27
+ #
28
+ def webrick
29
+ require 'webrick/httpserver'
30
+ require 'webrick/https'
31
+ require 'camping/webrick'
32
+
33
+ # TODO: verify the certificate's validity
34
+ # example of how to do this is here: http://pablotron.org/download/ruri-20050331.rb
35
+
36
+ cert_path = Picnic::Conf.ssl_cert
37
+ key_path = Picnic::Conf.ssl_key || Picnic::Conf.ssl_cert
38
+ # look for the key in the ssl_cert if no ssl_key is specified
39
+
40
+ begin
41
+ s = WEBrick::HTTPServer.new(
42
+ :BindAddress => "0.0.0.0",
43
+ :Port => Picnic::Conf.port
44
+ )
45
+ rescue Errno::EACCES
46
+ puts "\nThe server could not launch. Are you running on a privileged port? (e.g. port 443) If so, you must run the server as root."
47
+ exit 2
48
+ end
49
+
50
+ self.create
51
+ s.mount "#{Picnic::Conf.uri_path}", WEBrick::CampingHandler, self
52
+
53
+ # This lets Ctrl+C shut down your server
54
+ trap(:INT) do
55
+ s.shutdown
56
+ end
57
+ trap(:TERM) do
58
+ s.shutdown
59
+ end
60
+
61
+ if $DAEMONIZE
62
+ puts "\n** #{self} will run at http://#{ENV['HOSTNAME'] || 'localhost'}:#{Picnic::Conf.port}#{Picnic::Conf.uri_path} and log to #{Picnic::Conf.log[:file].inspect}. "
63
+ puts "** Check the log file for further messages!\n\n"
64
+
65
+ logdev = $LOG.instance_variable_get(:@logdev).instance_variable_get(:@filename)
66
+ if logdev == 'STDOUT' || logdev == nil
67
+ puts "\n!!! Warning !!!\nLogging to the console (STDOUT) is not possible once the server daemonizes. "+
68
+ "You should change the logger configuration to point to a real file."
69
+ end
70
+
71
+ WEBrick::Daemon.start do
72
+ begin
73
+ write_pid_file if $PID_FILE
74
+ $LOG.info "Starting #{self} as a WEBrick daemon with process id #{Process.pid}."
75
+ self.prestart if self.respond_to? :prestart
76
+ s.start
77
+ $LOG.info "Stopping #{self} WEBrick daemon with process id #{Process.pid}."
78
+ clear_pid_file
79
+ rescue => e
80
+ $LOG.error e
81
+ raise e
82
+ end
83
+ end
84
+ else
85
+ puts "\n** #{self} is running at http://#{ENV['HOSTNAME'] || 'localhost'}:#{Picnic::Conf.port}#{Picnic::Conf.uri_path} and logging to #{Picnic::Conf.log[:file].inspect}\n\n"
86
+ self.prestart if self.respond_to? :prestart
87
+ s.start
88
+ end
89
+ end
90
+
91
+ # Launches your Camping application using a mongrel server.
92
+ #
93
+ # Mongrel is much faster than webrick, but has two limitations:
94
+ # 1. You must install the mongrel gem before you can run your server using mongrel.
95
+ # This is not necessary with webrick, since webrick is included by default with Ruby.
96
+ # 2. Unlike webrick, mongrel can't be run using SSL. You will have to use a reverse
97
+ # proxy like Pound or Apache if you want to use mongrel with SSL.
98
+ #
99
+ # If $DAEMONIZE is true, the mongrel server will be forked to a background process.
100
+ # Note that this may have some strange effects, since any open IOs or threads will not
101
+ # be carried over to the forked daemon process.
102
+ #
103
+ # Module#start_picnic automatically calls this method to launch your Picnic app if
104
+ # your app's <tt>:server</tt> configuration option is set to <tt>'mongrel'</tt>.
105
+ #
106
+ # Usage example:
107
+ #
108
+ # require 'picnic/postambles'
109
+ # self.extend self::Postambles
110
+ # mongrel
111
+ #
112
+ def mongrel
113
+ require 'rubygems'
114
+ require 'mongrel/camping'
115
+
116
+ if $DAEMONIZE
117
+ # check if log and pid are writable before daemonizing, otherwise we won't be able to notify
118
+ # the user if we run into trouble later (since once daemonized, we can't write to stdout/stderr)
119
+ check_pid_writable if $PID_FILE
120
+ check_log_writable
121
+ end
122
+
123
+ self.create
124
+
125
+ puts "\n** #{self} is starting. Look in #{Picnic::Conf.log[:file].inspect} for further notices."
126
+
127
+ settings = {:host => "0.0.0.0", :log_file => Picnic::Conf.log[:file], :cwd => $APP_PATH}
128
+
129
+ # need to close all IOs before daemonizing
130
+ $LOG.close if $DAEMONIZE
131
+
132
+ begin
133
+ app_mod = self
134
+ config = Mongrel::Configurator.new settings do
135
+ daemonize :log_file => Picnic::Conf.log[:file], :cwd => $APP_PATH if $DAEMONIZE
136
+
137
+ listener :port => Picnic::Conf.port do
138
+ uri Picnic::Conf.uri_path, :handler => Mongrel::Camping::CampingHandler.new(app_mod)
139
+ setup_signals
140
+ end
141
+ end
142
+ rescue Errno::EADDRINUSE
143
+ exit 1
144
+ end
145
+
146
+
147
+ config.run
148
+
149
+ self.init_logger
150
+ #self.init_db_logger
151
+
152
+ if $DAEMONIZE && $PID_FILE
153
+ write_pid_file
154
+ unless File.exists? $PID_FILE
155
+ $LOG.error "#{self} could not start because pid file #{$PID_FILE.inspect} could not be created."
156
+ exit 1
157
+ end
158
+ end
159
+
160
+ puts "\n** #{self} is running at http://#{ENV['HOSTNAME'] || 'localhost'}:#{Picnic::Conf.port}#{Picnic::Conf.uri_path} and logging to '#{Picnic::Conf.log[:file]}'"
161
+
162
+ self.prestart if self.respond_to? :prestart
163
+ config.join
164
+
165
+ clear_pid_file
166
+
167
+ puts "\n** #{self} is stopped (#{Time.now})"
168
+ end
169
+
170
+ # Theoretically this should launch your Picnic app as a FastCGI process.
171
+ # I say "theoretically" because this has never actually been tested.
172
+ def fastcgi
173
+ require 'camping/fastcgi'
174
+ Dir.chdir('.')
175
+
176
+ self.create
177
+ Camping::FastCGI.start(self)
178
+ end
179
+
180
+ # Theoretically this should launch your Picnic app as a CGI process.
181
+ # I say "theoretically" because this has never actually been tested.
182
+ def cgi
183
+ self.create
184
+ puts self.run
185
+ end
186
+
187
+ private
188
+ # Prints an error message to stderr if the log file is not writable.
189
+ def check_log_writable
190
+ log_file = Picnic::Conf.log['file']
191
+ begin
192
+ f = open(log_file, 'w')
193
+ rescue
194
+ $stderr.puts "Couldn't write to log file at '#{log_file}' (#{$!})."
195
+ exit 1
196
+ end
197
+ f.close
198
+ end
199
+
200
+ # Prints an error message to stderr if the pid file is not writable.
201
+ def check_pid_writable
202
+ $LOG.debug "Checking if pid file #{$PID_FILE.inspect} is writable"
203
+ begin
204
+ f = open($PID_FILE, 'w')
205
+ rescue
206
+ $stderr.puts "Couldn't write to log at #{$PID_FILE.inspect} (#{$!})."
207
+ exit 1
208
+ end
209
+ f.close
210
+ end
211
+
212
+ # Writes the pid file.
213
+ def write_pid_file
214
+ $LOG.debug "Writing pid #{Process.pid.inspect} to pid file #{$PID_FILE.inspect}"
215
+ open($PID_FILE, "w") { |file| file.write(Process.pid) }
216
+ end
217
+
218
+ # Removes the pid file.
219
+ def clear_pid_file
220
+ if $PID_FILE && File.exists?($PID_FILE)
221
+ $LOG.debug "Clearing pid file #{$PID_FILE.inspect}"
222
+ File.unlink $PID_FILE
223
+ end
224
+ end
225
+
226
+ end
227
+ end