picnic 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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