rack-process 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-process.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Samuel Cochran
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Rack::Process
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rack-process'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install rack-process
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,7 @@
1
+ #!/usr/env/bin ruby
2
+
3
+ $:.unshift File.expand_path '../../lib', __FILE__
4
+
5
+ require 'rack-process'
6
+
7
+ Rack::Process::Worker.run *ARGV
@@ -0,0 +1,4 @@
1
+ class Rack::Process
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,33 @@
1
+ begin
2
+ # Avoid activating json gem
3
+ if defined? gem_original_require
4
+ gem_original_require 'json'
5
+ else
6
+ require 'json'
7
+ end
8
+ rescue LoadError
9
+ end
10
+
11
+ class Rack::Process
12
+ module JSON
13
+ if defined? ::JSON
14
+ def self.encode obj
15
+ obj.to_json
16
+ end
17
+
18
+ def self.decode json
19
+ ::JSON.parse json
20
+ end
21
+ else
22
+ require 'okjson'
23
+
24
+ def self.encode obj
25
+ ::OkJson.encode obj
26
+ end
27
+
28
+ def self.decode json
29
+ ::OkJson.decode json
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,68 @@
1
+ require 'stringio'
2
+ require 'strscan'
3
+
4
+ class Rack::Process
5
+ # http://cr.yp.to/proto/netstrings.txt
6
+ module NetString
7
+ module_function
8
+
9
+ def read io
10
+ length = ns_length io.readline ":"
11
+ buffer = io.read length
12
+
13
+ if io.eof?
14
+ return
15
+ elsif io.getc != ?,
16
+ raise Error, "Invalid netstring length, expected to be #{length}"
17
+ end
18
+
19
+ buffer
20
+ end
21
+
22
+ def write io, str
23
+ io << "#{str.bytesize}:" << str << ","
24
+ io.flush
25
+ end
26
+
27
+ def encode str
28
+ io = StringIO.new
29
+ write io, str
30
+ io.string
31
+ end
32
+
33
+ def decode str
34
+ io = StringIO.new str
35
+ io.rewind
36
+ read io
37
+ end
38
+
39
+ protected
40
+ module_function
41
+
42
+ def ns_length str
43
+ s = StringScanner.new str
44
+
45
+ if slen = s.scan(/\d+/)
46
+ if slen =~ /^0\d+$/
47
+ raise Error, "Invalid netstring with leading 0"
48
+ elsif slen.length > 9
49
+ raise Error, "netstring is too large"
50
+ end
51
+
52
+ len = Integer(slen)
53
+
54
+ if s.scan(/:/)
55
+ len
56
+ elsif s.eos?
57
+ raise Error, "Invalid netstring terminated after length"
58
+ else
59
+ raise Error, "Unexpected character '#{s.peek(1)}' found at offset #{s.pos}"
60
+ end
61
+ elsif s.peek(1) == ':'
62
+ raise Error, "Invalid netstring with leading ':'"
63
+ else
64
+ raise Error, "Unexpected character '#{s.peek(1)}' found at offset #{s.pos}"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ class Process
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,114 @@
1
+ class Rack::Process
2
+ class Worker::RackInput
3
+ include Enumerable
4
+
5
+ def initialize worker
6
+ @worker = worker
7
+ @buffer = ""
8
+ @position = 0
9
+ end
10
+
11
+ # A bunch of IO::Readable pilfered from http://git.tpope.net/ruby-io-mixins.git
12
+
13
+ def read length=nil, buffer=""
14
+ raise ArgumentError, "negative length #{length} given", caller if (length||0) < 0
15
+ return "" if length == 0 && @buffer.length > 0
16
+ return (length ? nil : "") if eof
17
+ return "" if length == 0
18
+ if length
19
+ @buffer << sysread if @buffer.length<length
20
+ else
21
+ begin
22
+ while str = sysread
23
+ @buffer << str
24
+ end
25
+ rescue EOFError
26
+ nil # For coverage
27
+ end
28
+ end
29
+ buffer[0..-1] = @buffer.slice!(0..(length || 0)-1)
30
+ @position ||= 0
31
+ @position += buffer.length
32
+ return buffer
33
+ end
34
+
35
+ def getc
36
+ read(1).to_s[0]
37
+ end
38
+
39
+ def gets sep_string=$/
40
+ return read(nil) unless sep_string
41
+ line = ""
42
+ paragraph = false
43
+ if sep_string == ""
44
+ sep_string = "\n\n"
45
+ paragraph = true
46
+ end
47
+ sep = sep_string.dup
48
+ position = @position
49
+ while (char = getc)
50
+ if paragraph && line.empty?
51
+ if char == ?\n
52
+ next
53
+ end
54
+ end
55
+ if char == sep[0]
56
+ sep[0] = ""
57
+ else
58
+ sep = sep_string.dup
59
+ end
60
+ if sep == ""
61
+ if paragraph
62
+ ungetc char
63
+ else
64
+ line << char
65
+ end
66
+ break
67
+ end
68
+ line << char
69
+ if position && @position == position
70
+ raise IOError, "loop encountered", caller
71
+ end
72
+ end
73
+ line = nil if line == ""
74
+ $_ = line
75
+ end
76
+
77
+ def each sep_string = $/
78
+ while line = gets(sep_string)
79
+ yield line
80
+ end
81
+ self
82
+ end
83
+
84
+ def eof
85
+ return false unless @buffer.empty?
86
+ str = sysread
87
+ if str
88
+ @buffer << str
89
+ @buffer.empty?
90
+ else
91
+ true
92
+ end
93
+ rescue EOFError
94
+ return true
95
+ end
96
+
97
+ alias eof? eof
98
+
99
+ def close
100
+ end
101
+
102
+ def sysread
103
+ @worker.write "input"
104
+ case command = @worker.read
105
+ when "input"
106
+ return @worker.read
107
+ when "close"
108
+ exit
109
+ else
110
+ raise "Expected input, got #{command}"
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,90 @@
1
+ require 'rack/builder'
2
+ require 'rack/rewindable_input'
3
+
4
+ class Rack::Process
5
+ class Worker
6
+ def self.run *args
7
+ new(*args).start
8
+ end
9
+
10
+ attr_accessor :config_path, :input, :output, :error
11
+
12
+ def initialize config_path, input=$stdin, output=$stdout, error=$stderr
13
+ self.config_path = config_path
14
+ self.input = if input.is_a? String then File.open(input, 'r') else input end
15
+ self.output = if output.is_a? String then File.open(output, 'w') else output end
16
+ self.error = if error.is_a? String then File.open(error, 'w') else error end
17
+ end
18
+
19
+ def config
20
+ ::File.read config_path
21
+ end
22
+
23
+ def app
24
+ @app ||= eval "Rack::Builder.new {( #{config}\n )}.to_app", TOPLEVEL_BINDING, config_path
25
+ end
26
+
27
+ def read
28
+ NetString.read input
29
+ end
30
+
31
+ def write str
32
+ NetString.write output, str
33
+ end
34
+
35
+ def start
36
+ trap('TERM') { exit }
37
+ trap('INT') { exit }
38
+ trap('QUIT') { exit }
39
+
40
+ input.set_encoding 'ASCII-8BIT' if input.respond_to? :set_encoding
41
+
42
+ while not input.eof?
43
+ case command = read
44
+ when "request"
45
+ handle_request JSON.decode read
46
+ when "close"
47
+ exit
48
+ end
49
+ end
50
+ rescue SystemExit, Errno::EINTR
51
+ # Ignore
52
+ rescue Exception => e
53
+ write "error"
54
+ write JSON.encode 'name' => e.class.name,
55
+ 'message' => e.message,
56
+ 'stack' => e.backtrace.join("\n")
57
+ end
58
+
59
+ def handle_request env
60
+ env = {
61
+ "rack.version" => Rack::VERSION,
62
+ "rack.input" => Rack::RewindableInput.new(RackInput.new(self)),
63
+ "rack.errors" => error,
64
+ "rack.multithread" => false,
65
+ "rack.multiprocess" => true,
66
+ "rack.run_once" => false,
67
+ "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
68
+ }.merge(env)
69
+
70
+ status, headers, body = app.call env
71
+
72
+ write "status"
73
+ write status.to_s
74
+ write "headers"
75
+ write JSON.encode headers
76
+ body.each do |piece|
77
+ write "output"
78
+ write piece
79
+ end
80
+ write "done"
81
+ rescue SystemExit, Errno::EINTR
82
+ # Ignore
83
+ rescue Exception => e
84
+ write "error"
85
+ write JSON.encode 'name' => e.class.name,
86
+ 'message' => e.message,
87
+ 'stack' => e.backtrace.join("\n")
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,105 @@
1
+ require 'rack'
2
+
3
+ class Rack::Process
4
+ attr_reader :config_path
5
+
6
+ def initialize config_path
7
+ @config_path = config_path
8
+ @config_path = ::File.join @config_path, "config.ru" if ::File.directory? @config_path
9
+ @config_path = ::File.absolute_path @config_path
10
+
11
+ raise ArgumentError, "Rackup file #{config_path} does not exist." unless ::File.exists? config_path
12
+
13
+ at_exit { close }
14
+ end
15
+
16
+ def call env
17
+ write "request"
18
+ write JSON.encode env.reject { |key| key[/\A(?:rack|async)/] }
19
+
20
+ status = headers = body = nil
21
+
22
+ while status.nil?
23
+ case command = read
24
+ when "status"
25
+ status = read.to_i
26
+ when "input"
27
+ write "input"
28
+ write env["rack.input"].read
29
+ when "error"
30
+ raise Error, "Rack process error: #{JSON.decode(read).inspect}"
31
+ else
32
+ raise Error, "Expecting status, got #{command}"
33
+ end
34
+ end
35
+
36
+ while headers.nil?
37
+ case command = read
38
+ when "headers"
39
+ headers = JSON.decode read
40
+ when "input"
41
+ write "input"
42
+ write env["rack.input"].read
43
+ when "error"
44
+ raise Error, "Rack process error: #{JSON.decode(read).inspect}"
45
+ else
46
+ raise Error, "Expecting headers, got #{command}"
47
+ end
48
+ end
49
+
50
+ body = Enumerator.new do |body|
51
+ while command = read
52
+ case command
53
+ when "output"
54
+ body << read
55
+ when "input"
56
+ write "input"
57
+ write env['rack.input'].read
58
+ when "done"
59
+ break
60
+ when "error"
61
+ raise Error, "Rack process error: #{JSON.decode(read).inspect}"
62
+ else
63
+ raise Error, "Expecting output, got #{command}"
64
+ end
65
+ end
66
+ end
67
+
68
+ [status, headers, body]
69
+ end
70
+
71
+ protected
72
+
73
+ def config_dir
74
+ ::File.dirname(config_path)
75
+ end
76
+
77
+ def worker_path
78
+ ::File.expand_path "../../../bin/rack-process-worker", __FILE__
79
+ end
80
+
81
+ def worker
82
+ # TODO: Better process management
83
+ @worker ||= IO.popen ["ruby", worker_path, config_path], "r+", chdir: config_dir, err: [:child, :err]
84
+ end
85
+
86
+ def read
87
+ NetString.read worker
88
+ end
89
+
90
+ def write str
91
+ NetString.write worker, str
92
+ end
93
+
94
+ def close
95
+ write "close"
96
+ ::Process.wait @worker.pid if @worker
97
+ end
98
+ end
99
+
100
+ require 'rack/process/version'
101
+ require 'rack/process/error'
102
+ require 'rack/process/json'
103
+ require 'rack/process/net_string'
104
+ require 'rack/process/worker'
105
+ require 'rack/process/worker/rack_input'
@@ -0,0 +1 @@
1
+ require "rack/process"
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/rack/process/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Samuel Cochran"]
6
+ gem.email = ["sj26@sj26.com"]
7
+ gem.description = %q{Proxy to a rack app in a seperate process.}
8
+ gem.summary = %q{Proxy to a rack app in a seperate process.}
9
+ gem.homepage = "http://github.com/sj26/rack-process"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {spec}/*`.split("\n")
14
+ gem.name = "rack-process"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Rack::Process::VERSION
17
+
18
+ gem.add_development_dependency "rspec", "~> 2.9"
19
+ end
@@ -0,0 +1,3 @@
1
+ require 'rack-process'
2
+
3
+ run Rack::Process.new File.expand_path "../example-file.ru", __FILE__
@@ -0,0 +1,3 @@
1
+ run proc do |env|
2
+ [200, {}, "Hello, #{env['HTTP_X_NAME'] || "world"}, from example-dir/config.ru"]
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'rack'
2
+
3
+ run(proc do |env|
4
+ request = Rack::Request.new env
5
+ [200, {"Content-Type" => "text/html"}, [%{<!DOCTYPE html>\n<html><head><title>Example File</title></head><body><p>Hello, #{request.POST['name'] || "world"}, from example-file.ru</p><form method="POST"><input name="name" placeholder="Name"> <button>Know Me</button></form></body></html>}]]
6
+ end)
@@ -0,0 +1,5 @@
1
+ RSpec.configure do |config|
2
+ config.treat_symbols_as_metadata_keys_with_true_values = true
3
+ config.run_all_when_everything_filtered = true
4
+ config.filter_run :focus
5
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-process
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Samuel Cochran
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70333352912860 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.9'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70333352912860
25
+ description: Proxy to a rack app in a seperate process.
26
+ email:
27
+ - sj26@sj26.com
28
+ executables:
29
+ - rack-process-worker
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - .rspec
35
+ - Gemfile
36
+ - LICENSE
37
+ - README.md
38
+ - Rakefile
39
+ - bin/rack-process-worker
40
+ - lib/rack-process.rb
41
+ - lib/rack/process.rb
42
+ - lib/rack/process/error.rb
43
+ - lib/rack/process/json.rb
44
+ - lib/rack/process/net_string.rb
45
+ - lib/rack/process/version.rb
46
+ - lib/rack/process/worker.rb
47
+ - lib/rack/process/worker/rack_input.rb
48
+ - rack-process.gemspec
49
+ - spec/fixtures/config.ru
50
+ - spec/fixtures/example-dir/config.ru
51
+ - spec/fixtures/example-file.ru
52
+ - spec/spec_helper.rb
53
+ homepage: http://github.com/sj26/rack-process
54
+ licenses: []
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.11
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Proxy to a rack app in a seperate process.
77
+ test_files: []
78
+ has_rdoc: