kirk 0.1.0-java

Sign up to get free protection for your applications and to get access to all the features.
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
+