rack-process 0.0.1

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/.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: