kirk 0.1.0-java

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/LICENSE ADDED
File without changes
@@ -0,0 +1,102 @@
1
+ # Kirk: a JRuby Rack server based on Jetty
2
+
3
+ Kirk is a wrapper around Jetty that hides all of the insanity and wraps your
4
+ Rack application in a loving embrace. Also, Kirk is probably the least HTTP
5
+ retarded ruby rack server out there.
6
+
7
+ ### TL;DR
8
+
9
+ gem install kirk
10
+ rackup -s Kirk config.ru
11
+
12
+ ### Features
13
+
14
+ Here is a brief highlight of some of the features available.
15
+
16
+ * 0 Downtime deploys: Deploy new versions of your rack application without
17
+ losing a single request. The basic strategy is similar to Passenger.
18
+ Touch a magic file in the application root and Kirk will reload the
19
+ application without dropping a single request... It's just magic.
20
+
21
+ * Request body streaming: Have a large request body to handle? Why wait until
22
+ receiving the entire thing before starting the work?
23
+
24
+ * HTTP goodness: `Transfer-Encoding: chunked`, `Content-Encoding: gzip` (and
25
+ deflate) support on the request.
26
+
27
+ * Concurrency: As it turns out, it's nice to not block your entire application
28
+ while waiting for an HTTP request to finish. It's also nice to be able to
29
+ handle more than 50 concurrent requests without burning over 5GB of RAM. Sure,
30
+ RAM is cheap, but using Kirk is cheaper ;)
31
+
32
+ * Run on the JVM: I, for one, am a fan of having a predictable GC, a JIT
33
+ compiler, and other goodness.
34
+
35
+ ### Getting Started
36
+
37
+ To take advantage of the zero downtime redeploy features, you will need to
38
+ create a configuration file that describes to Kirk how to start and watch the
39
+ rack application. You can create the file anywhere. For example, let's say that
40
+ we are going to put the following configuration file at `/path/to/Kirkfile`.
41
+
42
+ # Set the log level to ALL.
43
+ log :level => :all
44
+
45
+ rack "/path/to/my/rackup/config.ru" do
46
+ # Set the host and / or port that this rack application will
47
+ # be available on. This defaults to "0.0.0.0:9090"
48
+ listen 80
49
+
50
+ # Set the host names that this rack application wll be available
51
+ # on. This defaults to "*"
52
+ hosts "example.com", "*.example.org"
53
+
54
+ # Set the file that controls the redeploys. This is relative to
55
+ # the applications root (the directory that the rackup file lives
56
+ # in). Touch this file to redepoy the application.
57
+ watch "REVISION"
58
+ end
59
+
60
+ rack "/path/to/another/rackup/config.ru" do
61
+ # More settings here
62
+ end
63
+
64
+ Once you have Kirk configured, start it up with the following command:
65
+
66
+ kirk -c /path/to/Kirkfile
67
+
68
+ ... and you're off.
69
+
70
+ ### Daemonizing Kirk
71
+
72
+ Use your OS features. For example, write an upstart script or use
73
+ `start-stop-daemon`.
74
+
75
+ ### Logging to a file or syslog
76
+
77
+ Kirk just dumps logs to stdout, so just pipe Kirk to `logger`.
78
+
79
+ ### Caveats
80
+
81
+ This is still a pretty new project and a lot of settings that should be
82
+ abstracted are still hardcoded in the source.
83
+
84
+ * Kirk requires JRuby 1.6.0 RC 1 or greater. This is due to a JRuby bug that
85
+ was fixed in the 1.6 branch but never backported to older versions of JRuby.
86
+ Crazy stuff happens without the bug fix.
87
+
88
+ * The JDBC drivers will keep connections open unless they are explicitly
89
+ cleaned up, something which Rack applications do not do. A future release of
90
+ the jdbc ruby drivers will correctly clean up after the JDBC garbage, but for
91
+ now, you will have to manually do it. If you are running a Rails app, add the
92
+ following to the config.ru file:
93
+
94
+ at_exit { ActiveRecord::Base.clear_all_connections! }
95
+
96
+ ### Getting Help
97
+
98
+ Ping me (carllerche) on Freenode in #carlhuda
99
+
100
+ ### Reporting Bugs
101
+
102
+ Just post them to the Github repository's issue tracker.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'kirk/cli'
3
+ Kirk::CLI.start(ARGV)
@@ -0,0 +1,55 @@
1
+ module Kirk
2
+ # Make sure that the version of JRuby is new enough
3
+ unless (JRUBY_VERSION.split('.')[0..2].map(&:to_i) <=> [1, 6, 0]) >= 0
4
+ raise "Kirk requires JRuby 1.6.0 RC 1 or greater. This is due to " \
5
+ "a bug that was fixed in the 1.6 line but not backported to " \
6
+ "older versions of JRuby. If you want to use Kirk with older " \
7
+ "versions of JRuby, bug headius."
8
+ end
9
+
10
+ require 'java'
11
+ require 'kirk/native'
12
+ require 'kirk/jetty'
13
+ require 'kirk/version'
14
+
15
+ import "java.util.concurrent.LinkedBlockingQueue"
16
+ import "java.util.concurrent.TimeUnit"
17
+ import "java.util.logging.Logger"
18
+ import "java.util.logging.Level"
19
+ import "java.util.logging.ConsoleHandler"
20
+
21
+ module Native
22
+ import "com.strobecorp.kirk.ApplicationConfig"
23
+ import "com.strobecorp.kirk.HotDeployableApplication"
24
+ import "com.strobecorp.kirk.LogFormatter"
25
+ end
26
+
27
+ module Applications
28
+ autoload :Config, 'kirk/applications/config'
29
+ autoload :DeployWatcher, 'kirk/applications/deploy_watcher'
30
+ autoload :HotDeployable, 'kirk/applications/hot_deployable'
31
+ autoload :Rack, 'kirk/applications/rack'
32
+ end
33
+
34
+ require 'kirk/builder'
35
+ require 'kirk/handler'
36
+ require 'kirk/input_stream'
37
+ require 'kirk/server'
38
+
39
+ # Configure the logger
40
+ def self.logger
41
+ @logger ||= begin
42
+ logger = Logger.get_logger("org.eclipse.jetty.util.log")
43
+ logger.set_use_parent_handlers(false)
44
+ logger.add_handler logger_handler
45
+ logger
46
+ end
47
+ end
48
+
49
+ def self.logger_handler
50
+ ConsoleHandler.new.tap do |handler|
51
+ handler.set_output_stream(java::lang::System.out)
52
+ handler.set_formatter(Native::LogFormatter.new)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ module Kirk
2
+ module Applications
3
+ class Config
4
+ include Native::ApplicationConfig
5
+
6
+ attr_accessor :hosts,
7
+ :listen,
8
+ :watch,
9
+ :rackup,
10
+ :application_path,
11
+ :bootstrap_path
12
+
13
+ def initialize
14
+ @hosts = []
15
+ @listen = listen
16
+ end
17
+
18
+ def application_path
19
+ @application_path || File.dirname(rackup)
20
+ end
21
+
22
+ # Handle the java interface
23
+ alias getApplicationPath application_path
24
+ alias getRackupPath rackup
25
+ alias getBootstrapPath bootstrap_path
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,138 @@
1
+ module Kirk
2
+ class Applications::DeployWatcher
3
+ include Jetty::LifeCycle::Listener
4
+
5
+ def initialize
6
+ @apps, @magic_telephone, @workers = [], LinkedBlockingQueue.new, 0
7
+ @info = {}
8
+ @thread = Thread.new { run_loop }
9
+ @thread.abort_on_exception = true
10
+ end
11
+
12
+ def stop
13
+ @magic_telephone.put :halt
14
+ @thread.join
15
+ end
16
+
17
+ def life_cycle_started(app)
18
+ @magic_telephone.put [ :watch, app ]
19
+ end
20
+
21
+ def life_cycle_stopping(app)
22
+ @magic_telephone.put [ :unwatch, app ]
23
+ end
24
+
25
+ # Rest of the java interface
26
+ def life_cycle_failure(app, cause) ; end
27
+ def life_cycle_starting(app) ; end
28
+ def life_cycle_stopped(app) ; end
29
+
30
+ private
31
+
32
+ def run_loop
33
+ while true
34
+ # First, pull off messages
35
+ while msg = @magic_telephone.poll(50, TimeUnit::MILLISECONDS)
36
+ return if msg == :halt
37
+ handle_message(*msg)
38
+ end
39
+
40
+ check_apps
41
+ end
42
+
43
+ until @workers == 0
44
+ msg = @magic_telephone.take
45
+ handle_message(*msg)
46
+ end
47
+
48
+ cleanup
49
+ end
50
+
51
+ def handle_message(action, *args)
52
+ case action
53
+ when :watch
54
+
55
+ app, _ = *args
56
+
57
+ @apps |= [app]
58
+
59
+ @info[app] ||= {
60
+ :standby => LinkedBlockingQueue.new,
61
+ :last_modified => app.last_modified
62
+ }
63
+
64
+ warmup_standby_deploy(app)
65
+
66
+ when :unwatch
67
+
68
+ app, _ = *args
69
+ @apps.delete(app)
70
+
71
+ when :worker_complete
72
+
73
+ @workers -= 1
74
+
75
+ else
76
+
77
+ raise "Unknown action `#{action}`"
78
+
79
+ end
80
+ end
81
+
82
+ def check_apps
83
+ @apps.each do |app|
84
+ new_last_modified = app.last_modified
85
+ info = @info[app]
86
+
87
+ if new_last_modified > info[:last_modified]
88
+ Kirk.logger.info("Reloading `#{app.application_path}`")
89
+ redeploy(app, new_last_modified)
90
+ end
91
+ end
92
+ end
93
+
94
+ def redeploy(app, mtime)
95
+ if key = app.key
96
+
97
+ queue = @info[app][:standby]
98
+ deploy = queue.poll
99
+
100
+ if deploy && deploy.key == key
101
+ app.deploy(deploy)
102
+ else
103
+ deploy.terminate if deploy
104
+ app.redeploy
105
+ end
106
+
107
+ warmup_standby_deploy(app)
108
+ else
109
+ app.redeploy
110
+ end
111
+
112
+ @info[app][:last_modified] = mtime
113
+ end
114
+
115
+ def warmup_standby_deploy(app)
116
+ queue = @info[app][:standby]
117
+ background { queue.put(app.build_deploy) }
118
+ end
119
+
120
+ def cleanup
121
+ @apps.each do |app|
122
+ queue = @info[app][:standby]
123
+
124
+ while deploy = queue.poll
125
+ deploy.terminate
126
+ end
127
+ end
128
+ end
129
+
130
+ def background
131
+ @workers += 1
132
+ Thread.new do
133
+ yield
134
+ @magic_telephone.put [ :worker_complete ]
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,50 @@
1
+ require 'digest/sha1'
2
+
3
+ module Kirk
4
+ module Applications
5
+ # This class extends a native Java class
6
+ class HotDeployable < Native::HotDeployableApplication
7
+
8
+ def initialize(config)
9
+ super(config)
10
+ redeploy
11
+ end
12
+
13
+ def key
14
+ gemfile = "#{application_path}/Gemfile"
15
+ lockfile = "#{application_path}/Gemfile.lock"
16
+
17
+ if File.exist?(gemfile) && File.exist?(lockfile)
18
+ str = File.read(gemfile) + File.read(lockfile)
19
+ Digest::SHA1.hexdigest(str)
20
+ end
21
+ end
22
+
23
+ def add_watcher(watcher)
24
+ add_life_cycle_listener(watcher)
25
+ end
26
+
27
+ def application_path
28
+ config.application_path
29
+ end
30
+
31
+ def last_modified
32
+ mtimes = config.watch.map do |path|
33
+ path = File.expand_path(path, application_path)
34
+ File.exist?(path) ? File.mtime(path).to_i : 0
35
+ end
36
+
37
+ mtimes.max
38
+ end
39
+
40
+ def redeploy
41
+ deploy(build_deploy)
42
+ end
43
+
44
+ def deploy(d)
45
+ d.prepare
46
+ super(d)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ gem "rack", ">= 1.0.0"
2
+ require "rack"
3
+
4
+ module Kirk
5
+ module Applications
6
+ # This class extends a native Java class
7
+ class Rack < Jetty::AbstractHandler
8
+ def self.new(app)
9
+ inst = super()
10
+ inst.app = Handler.new(app)
11
+ inst
12
+ end
13
+
14
+ attr_accessor :app
15
+
16
+ def handle(target, base_request, request, response)
17
+ app.handle(target, base_request, request, response)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,62 @@
1
+ require 'java'
2
+
3
+ module Kirk
4
+ import 'java.util.zip.GZIPInputStream'
5
+ import 'java.util.zip.InflaterInputStream'
6
+
7
+ class Bootstrap
8
+ def warmup(application_path)
9
+ Dir.chdir File.expand_path(application_path)
10
+
11
+ load_rubygems
12
+
13
+ load_bundle.tap do
14
+ add_kirk_to_load_path
15
+
16
+ load_rack
17
+ load_kirk
18
+ end
19
+ end
20
+
21
+ def run(rackup)
22
+ app, options = Rack::Builder.parse_file(rackup)
23
+
24
+ Kirk::Handler.new(app)
25
+ end
26
+
27
+ private
28
+
29
+ def load_rubygems
30
+ require 'rubygems'
31
+ end
32
+
33
+ def load_bundle
34
+ if File.exist?('Gemfile')
35
+ require 'bundler/setup'
36
+
37
+ if File.exist?('Gemfile.lock')
38
+ require 'digest/sha1'
39
+ str = File.read('Gemfile') + File.read('Gemfile.lock')
40
+ Digest::SHA1.hexdigest(str)
41
+ end
42
+ end
43
+ end
44
+
45
+ def add_kirk_to_load_path
46
+ $:.unshift File.expand_path('../..', __FILE__)
47
+ end
48
+
49
+ def load_rack
50
+ gem "rack", ">= 1.0.0"
51
+ require 'rack'
52
+ end
53
+
54
+ def load_kirk
55
+ require 'kirk/version'
56
+ require 'kirk/input_stream'
57
+ require 'kirk/handler'
58
+ end
59
+ end
60
+ end
61
+
62
+ Kirk::Bootstrap.new
@@ -0,0 +1,133 @@
1
+ module Kirk
2
+ class MissingConfigFile < RuntimeError ; end
3
+ class MissingRackupFile < RuntimeError ; end
4
+
5
+ class Builder
6
+
7
+ VALID_LOG_LEVELS = %w(severe warning info config fine finer finest all)
8
+
9
+ attr_reader :options
10
+
11
+ def initialize(root = nil)
12
+ @root = root || Dir.pwd
13
+ @current = nil
14
+ @configs = []
15
+ @options = { :watcher => Applications::DeployWatcher.new }
16
+ end
17
+
18
+ def load(path)
19
+ path = expand_path(path)
20
+
21
+ with_root File.dirname(path) do
22
+ unless File.exist?(path)
23
+ raise MissingConfigFile, "config file `#{path}` does not exist"
24
+ end
25
+
26
+ instance_eval(File.read(path), path)
27
+ end
28
+ end
29
+
30
+ def log(opts = {})
31
+ level = opts[:level]
32
+ raise "Invalid log level" unless VALID_LOG_LEVELS.include?(level.to_s)
33
+ @options[:log_level] = level.to_s
34
+ end
35
+
36
+ def rack(rackup)
37
+ rackup = expand_path(rackup)
38
+
39
+ unless File.exist?(rackup)
40
+ raise MissingRackupFile, "rackup file `#{rackup}` does not exist"
41
+ end
42
+
43
+ @current = new_config
44
+ @current.rackup = rackup
45
+
46
+ yield if block_given?
47
+
48
+ ensure
49
+ @configs << @current
50
+ @current = nil
51
+ end
52
+
53
+ def hosts(*hosts)
54
+ @current.hosts.concat hosts
55
+ end
56
+
57
+ def listen(listen)
58
+ listen = listen.to_s
59
+ listen = ":#{listen}" unless listen.index(':')
60
+ listen = "0.0.0.0#{listen}" if listen.index(':') == 0
61
+
62
+ @current.listen = listen
63
+ end
64
+
65
+ def watch(*watch)
66
+ @current.watch = watch
67
+ end
68
+
69
+ def to_handler
70
+ handlers = @configs.map do |c|
71
+ Jetty::ContextHandler.new.tap do |ctx|
72
+ # Set the virtual hosts
73
+ unless c.hosts.empty?
74
+ ctx.set_virtual_hosts(c.hosts)
75
+ end
76
+
77
+ application = Applications::HotDeployable.new(c)
78
+ application.add_watcher(watcher)
79
+
80
+ ctx.set_connector_names [c.listen]
81
+ ctx.set_handler application
82
+ end
83
+ end
84
+
85
+ Jetty::ContextHandlerCollection.new.tap do |collection|
86
+ collection.set_handlers(handlers)
87
+ end
88
+ end
89
+
90
+ def to_connectors
91
+ @connectors = {}
92
+
93
+ @configs.each do |config|
94
+ next if @connectors.key?(config.listen)
95
+
96
+ host, port = config.listen.split(':')
97
+
98
+ connector = Jetty::SelectChannelConnector.new
99
+ connector.set_host(host)
100
+ connector.set_port(port.to_i)
101
+
102
+ @connectors[config.listen] = connector
103
+ end
104
+
105
+ @connectors.values
106
+ end
107
+
108
+ private
109
+
110
+ def watcher
111
+ @options[:watcher]
112
+ end
113
+
114
+ def with_root(root)
115
+ old, @root = @root, root
116
+ yield
117
+ ensure
118
+ @root = old
119
+ end
120
+
121
+ def expand_path(path)
122
+ File.expand_path(path, @root)
123
+ end
124
+
125
+ def new_config
126
+ Applications::Config.new.tap do |config|
127
+ config.listen = '0.0.0.0:9090'
128
+ config.watch = [ 'REVISION' ]
129
+ config.bootstrap_path = File.expand_path('../bootstrap.rb', __FILE__)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,52 @@
1
+ require 'kirk'
2
+ require 'optparse'
3
+
4
+ module Kirk
5
+ class CLI
6
+ def self.start(argv)
7
+ new(argv).tap { |inst| inst.run }
8
+ end
9
+
10
+ def initialize(argv)
11
+ @argv = argv.dup
12
+ @options = default_options
13
+ end
14
+
15
+ def run
16
+ parse!
17
+
18
+ server = Kirk::Server.build(config)
19
+ server.start
20
+ server.join
21
+ rescue Exception => e
22
+ abort "[ERROR] #{e.message}"
23
+ end
24
+
25
+ private
26
+
27
+ def config
28
+ @options[:config]
29
+ end
30
+
31
+ def default_options
32
+ { :config => "#{Dir.pwd}/Kirkfile" }
33
+ end
34
+
35
+ def parser
36
+ @parser ||= OptionParser.new do |opts|
37
+ opts.banner = "Usage: kirk [options] <command> [<args>]"
38
+
39
+ opts.separator ""
40
+ opts.separator "Server options:"
41
+
42
+ opts.on("-c", "--config FILE", "Load options from a config file") do |file|
43
+ @options[:config] = file
44
+ end
45
+ end
46
+ end
47
+
48
+ def parse!
49
+ parser.parse! @argv
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,126 @@
1
+ module Kirk
2
+ class Handler
3
+ # Required environment variable keys
4
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
5
+ SCRIPT_NAME = 'SCRIPT_NAME'.freeze
6
+ PATH_INFO = 'PATH_INFO'.freeze
7
+ QUERY_STRING = 'QUERY_STRING'.freeze
8
+ SERVER_NAME = 'SERVER_NAME'.freeze
9
+ SERVER_PORT = 'SERVER_PORT'.freeze
10
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
11
+ CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
12
+ REQUEST_URI = 'REQUEST_URI'.freeze
13
+ REMOTE_HOST = 'REMOTE_HOST'.freeze
14
+ REMOTE_ADDR = 'REMOTE_ADDR'.freeze
15
+ REMOTE_USER = 'REMOTE_USER'.freeze
16
+
17
+ # Rack specific variable keys
18
+ RACK_VERSION = 'rack.version'.freeze
19
+ RACK_URL_SCHEME = 'rack.url_scheme'.freeze
20
+ RACK_INPUT = 'rack.input'.freeze
21
+ RACK_ERRORS = 'rack.errors'.freeze
22
+ RACK_MULTITHREAD = 'rack.multithread'.freeze
23
+ RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
24
+ RACK_RUN_ONCE = 'rack.run_once'.freeze
25
+ HTTP_PREFIX = 'HTTP_'.freeze
26
+
27
+ # Rack response header names
28
+ CONTENT_TYPE_RESP = 'Content-Type'
29
+ CONTENT_LENGTH_RESP = 'Content-Length'
30
+
31
+ # Kirk information
32
+ SERVER = "#{NAME} #{VERSION}".freeze
33
+ SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
34
+
35
+ DEFAULT_RACK_ENV = {
36
+ SERVER_SOFTWARE => SERVER,
37
+
38
+ # Rack stuff
39
+ RACK_ERRORS => STDERR,
40
+ RACK_MULTITHREAD => true,
41
+ RACK_MULTIPROCESS => false,
42
+ RACK_RUN_ONCE => false,
43
+ }
44
+
45
+ CONTENT_LENGTH_TYPE_REGEXP = /^Content-(?:Type|Length)$/i
46
+
47
+ def initialize(app)
48
+ @app = app
49
+ end
50
+
51
+ def handle(target, base_request, request, response)
52
+ begin
53
+ env = DEFAULT_RACK_ENV.merge(
54
+ REQUEST_URI => request.getRequestURI,
55
+ PATH_INFO => request.get_path_info,
56
+ REQUEST_METHOD => request.get_method || "GET",
57
+ RACK_URL_SCHEME => request.get_scheme || "http",
58
+ QUERY_STRING => request.get_query_string || "",
59
+ SERVER_NAME => request.get_server_name || "",
60
+ REMOTE_HOST => request.get_remote_host || "",
61
+ REMOTE_ADDR => request.get_remote_addr || "",
62
+ REMOTE_USER => request.get_remote_user || "",
63
+ SERVER_PORT => request.get_server_port.to_s,
64
+ RACK_VERSION => ::Rack::VERSION)
65
+
66
+ # Process the content length
67
+ if (content_length = request.get_content_length) >= 0
68
+ env[CONTENT_LENGTH] = content_length.to_s
69
+ else
70
+ env[CONTENT_LENGTH] = "0"
71
+ end
72
+
73
+ if (content_type = request.get_content_type)
74
+ env[CONTENT_TYPE] = content_type unless content_type == ''
75
+ end
76
+
77
+ request.get_header_names.each do |header|
78
+ next if header =~ CONTENT_LENGTH_TYPE_REGEXP
79
+ value = request.get_header(header)
80
+
81
+ header.tr! '-', '_'
82
+ header.upcase!
83
+
84
+ header = "#{HTTP_PREFIX}#{header}"
85
+ env[header] = value unless env.key?(header) || value == ''
86
+ end
87
+
88
+ input = request.get_input_stream
89
+
90
+ case env['HTTP_CONTENT_ENCODING']
91
+ when 'deflate' then input = InflaterInputStream.new(input)
92
+ when 'gzip' then input = GZIPInputStream.new(input)
93
+ end
94
+
95
+ input = InputStream.new(input)
96
+ env[RACK_INPUT] = input
97
+
98
+ # Dispatch the request
99
+ status, headers, body = @app.call(env)
100
+
101
+ response.set_status(status.to_i)
102
+
103
+ headers.each do |header, value|
104
+ case header
105
+ when CONTENT_TYPE_RESP
106
+ response.set_content_type(value)
107
+ when CONTENT_LENGTH_RESP
108
+ response.set_content_length(value.to_i)
109
+ else
110
+ response.set_header(header, value)
111
+ end
112
+ end
113
+
114
+ buffer = response.get_output_stream
115
+ body.each do |s|
116
+ buffer.write(s.to_java_bytes)
117
+ end
118
+
119
+ body.close if body.respond_to?(:close)
120
+ ensure
121
+ input.recycle if input
122
+ request.set_handled(true)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,261 @@
1
+ require 'java'
2
+
3
+ module Kirk
4
+ class InputStream
5
+ READL_SIZE = 1_024
6
+ CHUNK_SIZE = 8_192
7
+ BUFFER_SIZE = 1_024 * 50
8
+
9
+ import "java.io.File"
10
+ import "java.io.RandomAccessFile"
11
+ import "java.nio.ByteBuffer"
12
+ import "java.nio.channels.Channels"
13
+
14
+ # For our ROFL scale needs
15
+ import "java.util.concurrent.LinkedBlockingQueue"
16
+
17
+ BUFFER_POOL = LinkedBlockingQueue.new
18
+ TMPFILE_PREFIX = "rackinput".to_java_string
19
+ TMPFILE_SUFFIX = "".to_java_string
20
+
21
+ def initialize(io)
22
+ @io = channelize(io)
23
+ @buffer = grab_buffer
24
+ @filebuf = nil
25
+ @read = 0
26
+ @position = 0
27
+ @eof = false
28
+ end
29
+
30
+ def read(size = nil, buffer = nil)
31
+ one_loop = nil
32
+ read_all = size.nil?
33
+
34
+ buffer ? buffer.slice!(0..-1) : buffer = ''
35
+
36
+ raise ArgumentError, "negative length #{size} given" if size && size < 0
37
+
38
+ loop do
39
+ limit = size && size < CHUNK_SIZE ? size : CHUNK_SIZE
40
+
41
+ len = read_chunk(limit, buffer)
42
+
43
+ break unless len
44
+
45
+ one_loop = true
46
+
47
+ break if size && ( size -= len ) <= 0
48
+ end
49
+
50
+ return "" if read_all && !one_loop
51
+
52
+ one_loop && buffer
53
+ end
54
+
55
+ def gets(sep = $/)
56
+ return read unless sep
57
+
58
+ sep = "#{$/}#{$/}" if sep == ""
59
+ buffer = ''
60
+ curpos = pos
61
+
62
+ while chunk = read(READL_SIZE)
63
+ buffer << chunk
64
+
65
+ if i = buffer.index(sep, 0)
66
+ i += sep.bytesize
67
+ buffer.slice!(i..-1)
68
+ seek(curpos + buffer.bytesize)
69
+ break
70
+ end
71
+ end
72
+
73
+ buffer
74
+ end
75
+
76
+ def each
77
+ while chunk = read(CHUNK_SIZE)
78
+ yield chunk
79
+ end
80
+
81
+ self
82
+ end
83
+
84
+ def pos
85
+ @position
86
+ end
87
+
88
+ def seek(pos)
89
+ raise Errno::EINVAL, "Invalid argument - invalid seek value" if pos < 0
90
+ @position = pos
91
+ end
92
+
93
+ def rewind
94
+ seek(0)
95
+ end
96
+
97
+ def close
98
+ nil
99
+ end
100
+
101
+ def recycle
102
+ BUFFER_POOL.put(@buffer)
103
+ @buffer = nil
104
+ nil
105
+ end
106
+
107
+ private
108
+
109
+ def channelize(stream)
110
+ Channels.new_channel(stream)
111
+ end
112
+
113
+ def grab_buffer
114
+ BUFFER_POOL.poll || ByteBuffer.allocate(BUFFER_SIZE)
115
+ end
116
+
117
+ def read_chunk(size, string)
118
+ missing = size - ( @read - @position )
119
+
120
+ if @filebuf || @read + missing > BUFFER_SIZE
121
+
122
+ rotate_to_tmp_file unless @filebuf
123
+ read_chunk_from_tmp_file(size, string)
124
+
125
+ else
126
+
127
+ read_chunk_from_mem(missing, string)
128
+
129
+ end
130
+ end
131
+
132
+ def read_chunk_from_mem(missing, string)
133
+ # We gonna have to read from the input stream
134
+ if missing > 0 && !@eof
135
+
136
+ # Set the new buffer limit to the amount that is going
137
+ # to be read
138
+ @buffer.position(@read).limit(@read + missing)
139
+
140
+ # Read into the buffer
141
+ len = @io.read(@buffer)
142
+
143
+ # Bail if we're at the EOF
144
+ if len == -1
145
+ @eof = true
146
+ return
147
+ end
148
+
149
+ @read += len
150
+
151
+ # We're at the end
152
+ elsif @position == @read
153
+
154
+ return
155
+
156
+ end
157
+
158
+ # Now move the amount read into the string
159
+ @buffer.position(@position).limit(@read)
160
+
161
+ append_buffer_to_string(@buffer, string)
162
+ end
163
+
164
+ def read_chunk_from_tmp_file(size, string)
165
+
166
+ if @read < @position && !@eof
167
+
168
+ return unless buffer_to(@position)
169
+
170
+ end
171
+
172
+ @buffer.clear.limit(size)
173
+
174
+ if @read > @position
175
+
176
+ @filebuf.position(@position)
177
+ @filebuf.read(@buffer)
178
+
179
+ elsif @eof
180
+
181
+ return
182
+
183
+ end
184
+
185
+ unless @eof
186
+
187
+ offset = @buffer.position
188
+ len = @io.read(@buffer)
189
+
190
+ if len == -1
191
+
192
+ @eof = true
193
+ return if offset == 0
194
+
195
+ else
196
+
197
+ @filebuf.position(@read)
198
+ @filebuf.write(@buffer.flip.position(offset))
199
+
200
+ @read += len
201
+
202
+ end
203
+
204
+ @buffer.position(0)
205
+
206
+ end
207
+
208
+ append_buffer_to_string(@buffer, string)
209
+ end
210
+
211
+ def buffer_to(position)
212
+ until @read == position
213
+ limit = position - @read
214
+ limit = BUFFER_SIZE if limit > BUFFER_SIZE
215
+
216
+ @buffer.clear.limit(limit)
217
+
218
+ len = @io.read(@buffer)
219
+
220
+ if len == -1
221
+ @eof = true
222
+ return
223
+ end
224
+
225
+ @buffer.flip
226
+
227
+ @filebuf.position(@read)
228
+ @filebuf.write(@buffer)
229
+
230
+ @read += len
231
+ end
232
+
233
+ true
234
+ end
235
+
236
+ def append_buffer_to_string(buffer, string)
237
+ len = buffer.limit - buffer.position
238
+ bytes = Java::byte[len].new
239
+ buffer.get(bytes)
240
+ string << String.from_java_bytes(bytes)
241
+ @position += len
242
+ len
243
+ end
244
+
245
+ def rotate_to_tmp_file
246
+ @buffer.position(0).limit(@read)
247
+
248
+ @filebuf = create_tmp_file
249
+ @filebuf.write @buffer
250
+
251
+ @buffer.clear
252
+ end
253
+
254
+ def create_tmp_file
255
+ file = File.create_temp_file(TMPFILE_PREFIX, TMPFILE_SUFFIX)
256
+ file.delete_on_exit
257
+
258
+ RandomAccessFile.new(file, "rw").channel
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,18 @@
1
+ require "kirk/jetty/jetty-server-7.2.2.v20101205"
2
+ require "kirk/jetty/servlet-api-2.5"
3
+
4
+ module Kirk
5
+ module Jetty
6
+ # Gimme Jetty
7
+ import "org.eclipse.jetty.server.nio.SelectChannelConnector"
8
+ import "org.eclipse.jetty.server.handler.AbstractHandler"
9
+ import "org.eclipse.jetty.server.handler.ContextHandler"
10
+ import "org.eclipse.jetty.server.handler.ContextHandlerCollection"
11
+ import "org.eclipse.jetty.server.Server"
12
+ import "org.eclipse.jetty.util.component.LifeCycle"
13
+ import "org.eclipse.jetty.util.log.Log"
14
+ import "org.eclipse.jetty.util.log.JavaUtilLog"
15
+
16
+ Log.set_log Jetty::JavaUtilLog.new
17
+ end
18
+ end
Binary file
@@ -0,0 +1,96 @@
1
+ module Kirk
2
+ class Server
3
+ def self.build(file = nil, &blk)
4
+ root = File.dirname(file) if file
5
+ builder = Builder.new(root)
6
+
7
+ if file && !File.exist?(file)
8
+ raise MissingConfigFile, "config file `#{file}` does not exist"
9
+ end
10
+
11
+ file ? builder.instance_eval(File.read(file), file) :
12
+ builder.instance_eval(&blk)
13
+
14
+ options = builder.options
15
+ options[:connectors] = builder.to_connectors
16
+
17
+ new(builder.to_handler, options)
18
+ end
19
+
20
+ def self.start(handler, options = {})
21
+ new(handler, options).tap do |server|
22
+ server.start
23
+ end
24
+ end
25
+
26
+ def initialize(handler, options = {})
27
+ if Jetty::AbstractHandler === handler
28
+ @handler = handler
29
+ elsif handler.respond_to?(:call)
30
+ @handler = Applications::Rack.new(handler)
31
+ else
32
+ raise "#{handler.inspect} is not a valid Rack application"
33
+ end
34
+
35
+ @options = options
36
+ end
37
+
38
+ def start
39
+ @server = Jetty::Server.new.tap do |server|
40
+ connectors.each do |conn|
41
+ server.add_connector(conn)
42
+ end
43
+
44
+ server.set_handler(@handler)
45
+ end
46
+
47
+ configure!
48
+
49
+ @server.start
50
+ end
51
+
52
+ def join
53
+ @server.join
54
+ end
55
+
56
+ def stop
57
+ watcher.stop if watcher
58
+ @server.stop
59
+ end
60
+
61
+ private
62
+
63
+ def configure!
64
+ Kirk.logger.set_level log_level
65
+ end
66
+
67
+ def connectors
68
+ @options[:connectors] ||=
69
+ [ Jetty::SelectChannelConnector.new.tap do |conn|
70
+ host = @options[:host] || '0.0.0.0'
71
+ port = @options[:port] || 9090
72
+
73
+ conn.set_host(host)
74
+ conn.set_port(port.to_i)
75
+ end ]
76
+ end
77
+
78
+ def watcher
79
+ @options[:watcher]
80
+ end
81
+
82
+ def log_level
83
+ case (@options[:log_level] || "info").to_s
84
+ when "severe" then Level::SEVERE
85
+ when "warning" then Level::WARNING
86
+ when "info" then Level::INFO
87
+ when "config" then Level::CONFIG
88
+ when "fine" then Level::FINE
89
+ when "finer" then Level::FINER
90
+ when "finest" then Level::FINEST
91
+ when "all" then Level::ALL
92
+ else Level::INFO
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,4 @@
1
+ module Kirk
2
+ NAME = "kirk".freeze
3
+ VERSION = "0.1.0".freeze
4
+ end
@@ -0,0 +1,25 @@
1
+ require 'kirk'
2
+ require 'rack'
3
+
4
+ module Rack
5
+ module Handler
6
+ class Kirk
7
+ def self.run(app, options = {})
8
+ options[:host] = options[:Host]
9
+ options[:port] = options[:Port]
10
+
11
+ options[:log_level] ||= "warning"
12
+
13
+ server = ::Kirk::Server.new(app, options)
14
+
15
+ yield server if block_given?
16
+
17
+ # Tears :'(
18
+ trap(:INT) { server.stop }
19
+
20
+ server.start
21
+ server.join
22
+ end
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kirk
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: java
7
+ authors:
8
+ - Carl Lerche
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-02-02 00:00:00 -08:00
14
+ default_executable: kirk
15
+ dependencies: []
16
+
17
+ description: |
18
+ Kirk is a wrapper around Jetty that hides all of the insanity and
19
+ wraps your Rack application in a loving embrace. Also, Kirk is
20
+ probably one of the least HTTP retarded ruby rack server out there.
21
+
22
+ email:
23
+ - me@carllerche.com
24
+ executables:
25
+ - kirk
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - lib/kirk.rb
32
+ - lib/kirk/bootstrap.rb
33
+ - lib/kirk/builder.rb
34
+ - lib/kirk/cli.rb
35
+ - lib/kirk/handler.rb
36
+ - lib/kirk/input_stream.rb
37
+ - lib/kirk/jetty.rb
38
+ - lib/kirk/server.rb
39
+ - lib/kirk/version.rb
40
+ - lib/kirk/applications/config.rb
41
+ - lib/kirk/applications/deploy_watcher.rb
42
+ - lib/kirk/applications/hot_deployable.rb
43
+ - lib/kirk/applications/rack.rb
44
+ - lib/rack/handler/kirk.rb
45
+ - lib/kirk/native.jar
46
+ - lib/kirk/jetty/jetty-server-7.2.2.v20101205.jar
47
+ - lib/kirk/jetty/servlet-api-2.5.jar
48
+ - bin/kirk
49
+ - README.md
50
+ - LICENSE
51
+ has_rdoc: true
52
+ homepage: http://github.com/carllerche/kirk
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 1.3.6
72
+ requirements: []
73
+
74
+ rubyforge_project: kirk
75
+ rubygems_version: 1.4.2
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: A JRuby Rack Server Based on Jetty
79
+ test_files: []
80
+