ringleader 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/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@ringleader --create
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem "foreman", :git => "https://github.com/ddollar/foreman.git"
7
+ end
8
+
9
+ group :development, :test do
10
+ if RUBY_PLATFORM =~ /darwin/
11
+ gem "growl"
12
+ gem "rb-fsevent"
13
+ end
14
+ end
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard 'rspec', :version => 2 do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
6
+
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Nathan Witmer
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,77 @@
1
+ ```plain
2
+ _____
3
+ (, / ) , /) /)
4
+ /__ / __ _ // _ _ _(/ _ __
5
+ ) / \__(_/ (_(_/_(/__(/_(_(_(_(__(/_/ (_
6
+ (_/ .-/
7
+ (_/
8
+ ```
9
+
10
+ Ringleader is an application proxy for socket applications.
11
+
12
+ ## Is it any good?
13
+
14
+ [Yes](http://news.ycombinator.com/item?id=3067434).
15
+
16
+ ## What's it for?
17
+
18
+ I designed this for a large suite of apps running behind nginx with a somewhat
19
+ complex routing configuration in nginx. Additionally, many of the apps required
20
+ active [resque](https://github.com/defunkt/resque/) pollers to run properly.
21
+ Ultimately this meant having many terminal windows open just to make a few
22
+ requests to the apps. Instead, I wanted something to manage all that for me:
23
+
24
+ +-------+
25
+ +->| app 1 |
26
+ | +-------+
27
+ +-----------+ +--------------+ |
28
+ http | | | | | +-------+
29
+ requests ---> | nginx +--->| ringleader +--+->| app 2 |
30
+ | | | | | +-------+
31
+ +-----------+ +--------------+ |
32
+ | +-------+
33
+ +->| app n |
34
+ +-------+
35
+
36
+ Ringleader is essentially a generalized replacement for [pow](http://pow.cx/),
37
+ and allows on-demand startup and proxying for any TCP server programs. It can
38
+ start a foreman or ruby or any other process which opens a socket. For example,
39
+ I started resque pollers alongside applications using foreman.
40
+
41
+ ## Installation
42
+
43
+ $ gem install ringleader
44
+ $ ringleader --help
45
+
46
+ ## Configuration
47
+
48
+ Ringleader requires a yml configuration file to start. It should look something
49
+ like this:
50
+
51
+ ```yml
52
+ ---
53
+ # name of app (used in logging)
54
+ main_app:
55
+ # working directory, where to start the app from
56
+ dir: "~/apps/main"
57
+ # the command to run to start up the app server. Executed under "bash -c".
58
+ command: "foreman start"
59
+ # the host to listen on, defaults to 127.0.0.1
60
+ hostname: 0.0.0.0
61
+ # the port ringleader listens on
62
+ server_port: 3000
63
+ # the port the application listens on
64
+ port: 4000
65
+ # idle timeout in seconds, defaults to 0. 0 means "never".
66
+ idle_timeout: 6000
67
+ other_app:
68
+ [...]
69
+ ```
70
+
71
+ ## Contributing
72
+
73
+ 1. Fork it
74
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
75
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
76
+ 4. Push to the branch (`git push origin my-new-feature`)
77
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/ringleader ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ringleader'
4
+ Ringleader::CLI.new.run(ARGV)
@@ -0,0 +1,6 @@
1
+ # loop: ruby sleep_loop.rb 5
2
+ # listen: sleep 3 && ncat -k -l 10001
3
+ # slow_echo: sleep 10 && ruby echo_server.rb
4
+ # echo: ruby echo_server.rb
5
+ sleep: sleep 30
6
+ # stubborn: ruby stubborn.rb
@@ -0,0 +1,35 @@
1
+ require 'celluloid/io'
2
+
3
+ class EchoServer
4
+ include Celluloid::IO
5
+ include Celluloid::Logger
6
+
7
+ def initialize(host, port)
8
+ info "*** Starting echo server on #{host}:#{port}"
9
+
10
+ # Since we included Celluloid::IO, we're actually making a
11
+ # Celluloid::IO::TCPServer here
12
+ @server = TCPServer.new(host, port)
13
+ run!
14
+ end
15
+
16
+ def finalize
17
+ @server.close if @server
18
+ end
19
+
20
+ def run
21
+ loop { handle_connection! @server.accept }
22
+ end
23
+
24
+ def handle_connection(socket)
25
+ _, port, host = socket.peeraddr
26
+ debug "*** Received connection from #{host}:#{port}"
27
+ loop { socket.write socket.readpartial(4096) }
28
+ rescue EOFError
29
+ debug "*** #{host}:#{port} disconnected"
30
+ end
31
+ end
32
+
33
+ trap("INT") { exit }
34
+ EchoServer.new "localhost", 10001
35
+ sleep
@@ -0,0 +1,56 @@
1
+ require "celluloid"
2
+
3
+ class S
4
+ include Celluloid
5
+ include Celluloid::Logger
6
+
7
+ def go
8
+ debug "sleeping..."
9
+ sleep 1
10
+ debug "awake. signaling awake"
11
+ signal :awake, true
12
+ end
13
+ end
14
+
15
+ class C
16
+ include Celluloid
17
+ include Celluloid::Logger
18
+
19
+ def initialize(n, s)
20
+ @n, @s = n, s
21
+ await!
22
+ end
23
+
24
+ def await
25
+ debug "#{@n} waiting"
26
+ @s.wait :awake
27
+ debug "#{@n} signaled"
28
+ end
29
+ end
30
+
31
+
32
+ class Foo
33
+ include Celluloid
34
+ include Celluloid::Logger
35
+
36
+ def go
37
+ after(1) { ping! }
38
+ debug "waiting"
39
+ ping = wait :ping
40
+ debug "got a ping! #{ping}"
41
+ end
42
+
43
+ def ping
44
+ signal :ping, "lol"
45
+ end
46
+ end
47
+
48
+ # s = S.new
49
+ # s.go!
50
+ # 5.times { |n| C.new(n, s) }
51
+ # sleep 2
52
+ # puts "my turn"
53
+ # s.wait :awake
54
+
55
+ Foo.new.go!
56
+ sleep 5
@@ -0,0 +1,47 @@
1
+ require "rubygems"
2
+ require "celluloid/io"
3
+
4
+ class App
5
+ include Celluloid
6
+ include Celluloid::Logger
7
+
8
+ def initialize(cmd)
9
+ @cmd = cmd
10
+ run!
11
+ end
12
+
13
+ def run
14
+ reader, writer = ::IO.pipe
15
+ @pid = Process.spawn @cmd, :out => writer, :err => writer
16
+ info "started process: #{@pid}"
17
+ proxy reader, $stdout
18
+ end
19
+
20
+ def proxy(input, output)
21
+ Thread.new do
22
+ until input.eof?
23
+ info "#{@pid} | " + input.gets.strip
24
+ end
25
+ end
26
+ end
27
+
28
+ def stop
29
+ info "stopping #{@cmd}"
30
+ Process.kill("SIGHUP", @pid)
31
+ info "waiting for #{@cmd}"
32
+ status = Process.wait @pid
33
+ rescue Errno::ESRCH, Errno::EPERM
34
+ ensure
35
+ terminate
36
+ end
37
+
38
+ end
39
+
40
+ app = App.new "bundle exec foreman start"
41
+ # app = App.new "ruby sleep_loop.rb 5"
42
+
43
+ trap("INT") do
44
+ app.stop
45
+ exit
46
+ end
47
+ sleep
@@ -0,0 +1,26 @@
1
+ def log(msg)
2
+ STDOUT.puts "#{$$} stdout #{msg}"
3
+ STDOUT.flush
4
+ STDERR.puts "#{$$} stderr #{msg}"
5
+ end
6
+
7
+ %w(HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP
8
+ TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1
9
+ USR1).each.with_index do |signal, i|
10
+ trap(signal) {
11
+ log signal
12
+ log "waiting a second"
13
+ sleep 1
14
+ log "exiting"
15
+ exit i
16
+ }
17
+ end
18
+
19
+ times = Integer(ARGV[0] || "120")
20
+ times.times do |n|
21
+ sleep 1
22
+ log n + 1
23
+ end
24
+ log "loop complete"
25
+
26
+ at_exit { log "exiting" }
@@ -0,0 +1,9 @@
1
+ trap "HUP" do
2
+ STDERR.puts "LOL, NOT QUITTING"
3
+ end
4
+
5
+ at_exit do
6
+ STDERR.puts "FUCK YOUUUUUUU"
7
+ end
8
+
9
+ sleep
@@ -0,0 +1,8 @@
1
+ ---
2
+ test:
3
+ dir: "./dev_scripts"
4
+ # command: "ncat -k -l 10001"
5
+ command: "bundle exec foreman start"
6
+ server_port: 10000
7
+ port: 10001
8
+ idle_timeout: 10
@@ -0,0 +1,154 @@
1
+ module Ringleader
2
+
3
+ # Represents a running instance of an application.
4
+ class App
5
+ include Celluloid
6
+ include Celluloid::Logger
7
+ include NameLogger
8
+
9
+ attr_reader :config
10
+
11
+ # Create a new App instance.
12
+ #
13
+ # config - a configuration object for this app
14
+ def initialize(config)
15
+ @config = config
16
+ @starting = @running = false
17
+ @restart_file = File.expand_path(config.dir + "/tmp/restart.txt")
18
+ @mtime = File.exist?(@restart_file) ? File.mtime(@restart_file).to_i : 0
19
+ end
20
+
21
+ # Public: query if the app is running
22
+ def running?
23
+ @running
24
+ end
25
+
26
+ # Public: start the application.
27
+ #
28
+ # This method is intended to be used synchronously. If the app is already
29
+ # running, it'll return immediately. If the app hasn't been started, or is
30
+ # in the process of starting, this method blocks until it starts or fails to
31
+ # start correctly.
32
+ #
33
+ # Returns true if the app started, false if not.
34
+ def start
35
+ if restart?
36
+ info "tmp/restart.txt modified, restarting..."
37
+ stop
38
+ end
39
+
40
+ if @running
41
+ true
42
+ elsif @starting
43
+ wait :running
44
+ else
45
+ start_app
46
+ end
47
+ end
48
+
49
+ # Public: stop the application.
50
+ #
51
+ # Sends a SIGHUP to the app's process, and expects it to exit like a sane
52
+ # and well-behaved application within 30 seconds before sending a SIGTERM.
53
+ def stop
54
+ return unless @pid
55
+
56
+ info "stopping `#{config.command}`"
57
+ Process.kill "SIGHUP", -@pid
58
+
59
+ timer = after 30 do
60
+ if @running
61
+ warn "process #{@pid} did not shut down cleanly, killing it"
62
+ Process.kill "SIGTERM", -@pid
63
+ end
64
+ end
65
+
66
+ wait :running # wait for the exit callback
67
+ timer.cancel
68
+ rescue Errno::ESRCH, Errno::EPERM
69
+ exited
70
+ end
71
+
72
+ # Internal: callback for when the application port has opened
73
+ def port_opened
74
+ info "listening on #{config.hostname}:#{config.port}"
75
+ signal :running, true
76
+ end
77
+
78
+ # Internal: callback for when the process has exited.
79
+ def exited
80
+ debug "pid #{@pid} has exited"
81
+ info "exited."
82
+ @running = false
83
+ @pid = nil
84
+ @wait_for_port.terminate if @wait_for_port.alive?
85
+ @wait_for_exit.terminate if @wait_for_exit.alive?
86
+ signal :running, false
87
+ end
88
+
89
+ # Private: start the application process and associated infrastructure
90
+ #
91
+ # Intended to be synchronous, as it blocks until the app has started (or
92
+ # failed to start).
93
+ #
94
+ # Returns true if the app started, false if not.
95
+ def start_app
96
+ @starting = true
97
+ info "starting process `#{config.command}`"
98
+
99
+ # need a pipe for input, even if we don't use it
100
+ stdin, stdout = ::IO.pipe
101
+ # give the child process a terminal
102
+ master, slave = PTY.open
103
+ @pid = Process.spawn %Q(bash -c "#{config.command}"),
104
+ :in => stdin,
105
+ :out => slave,
106
+ :err => slave,
107
+ :pgroup => true,
108
+ :chdir => config.dir
109
+ stdin.close
110
+ stdout.close
111
+ slave.close
112
+ proxy_output master
113
+ debug "started with pid #{@pid}"
114
+
115
+ @wait_for_exit = WaitForExit.new @pid, Actor.current
116
+ @wait_for_port = WaitForPort.new config.hostname, config.port, Actor.current
117
+
118
+ timer = after(30) { warn "application startup took too long"; stop! }
119
+
120
+ @running = wait :running
121
+ @starting = false
122
+ timer.cancel
123
+
124
+ @running
125
+ rescue Errno::ENOENT
126
+ debug "could not start process `#{config.command}`"
127
+ false
128
+ end
129
+
130
+ # Private: proxy output streams to the logger.
131
+ #
132
+ # Fire and forget, runs in its own thread.
133
+ def proxy_output(input)
134
+ Thread.new do
135
+ until input.eof?
136
+ info input.gets.strip
137
+ end
138
+ end
139
+ end
140
+
141
+ # Check the mtime of the tmp/restart.txt file. If modified, restart the app.
142
+ def restart?
143
+ if File.exist?(@restart_file)
144
+ new_mtime = File.mtime(@restart_file).to_i
145
+ if new_mtime > @mtime
146
+ @mtime = new_mtime
147
+ true
148
+ end
149
+ end
150
+ end
151
+
152
+ end
153
+
154
+ end
@@ -0,0 +1,61 @@
1
+ module Ringleader
2
+
3
+ # Proxy for an application running on a given port.
4
+ class AppProxy
5
+ include Celluloid::IO
6
+ include Celluloid::Logger
7
+
8
+ attr_reader :config
9
+
10
+ # Create a new AppProxy instance
11
+ #
12
+ # app - the application to proxy to
13
+ # config - a configuration object for this app
14
+ def initialize(app, config)
15
+ @config = config
16
+ @app = app
17
+ @server = TCPServer.new config.hostname, config.server_port
18
+ run!
19
+ rescue Errno::EADDRINUSE
20
+ error "could not bind to #{config.hostname}:#{config.server_port} for #{config.name}!"
21
+ end
22
+
23
+ def finalize
24
+ @server.close if @server
25
+ end
26
+
27
+ def run
28
+ start_activity_timer if config.idle_timeout > 0
29
+ info "server listening for connections for #{config.name} on #{config.hostname}:#{config.server_port}"
30
+ loop { handle_connection! @server.accept }
31
+ end
32
+
33
+ def handle_connection(socket)
34
+ _, port, host = socket.peeraddr
35
+ debug "received connection from #{host}:#{port}"
36
+
37
+ started = @app.start
38
+ if started
39
+ proxy_to_app! socket
40
+ @activity_timer.reset if @activity_timer
41
+ else
42
+ error "could not start app"
43
+ socket.close
44
+ end
45
+ end
46
+
47
+ def proxy_to_app(socket)
48
+ SocketProxy.new socket, config.hostname, config.port
49
+ end
50
+
51
+ def start_activity_timer
52
+ @activity_timer = every config.idle_timeout do
53
+ if @app.running?
54
+ info "#{config.name} has been idle for #{config.idle_timeout} seconds, shutting it down"
55
+ @app.stop
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,137 @@
1
+ module Ringleader
2
+ class CLI
3
+ include Celluloid::Logger
4
+
5
+ TERMINAL_COLORS = [:red, :green, :yellow, :blue, :magenta, :cyan]
6
+
7
+ def run(argv)
8
+ # hide "shutdown" info message until after opts are validated
9
+ Celluloid.logger.level = ::Logger::ERROR
10
+
11
+ opts = nil
12
+ Trollop.with_standard_exception_handling parser do
13
+ opts = parser.parse argv
14
+ end
15
+
16
+ die "must provide a filename" if argv.empty?
17
+ die "could not find config file #{argv.first}" unless File.exist?(argv.first)
18
+
19
+ configure_logging(opts.verbose ? "debug" : "info")
20
+
21
+ configs = Config.new(argv.first).apps.values
22
+ colorized = assign_colors configs, opts.boring
23
+ start_app_server colorized
24
+ end
25
+
26
+ def configure_logging(level)
27
+ Celluloid.logger.level = ::Logger.const_get(level.upcase)
28
+ format = "%5s %s.%06d | %s\n"
29
+ date_format = "%H:%M:%S"
30
+ Celluloid.logger.formatter = lambda do |severity, time, progname, msg|
31
+ format % [severity, time.strftime(date_format), time.usec, msg]
32
+ end
33
+ end
34
+
35
+ def assign_colors(configs, boring=false)
36
+ if boring
37
+ configs.map.with_index do |config, i|
38
+ config.color = TERMINAL_COLORS[ i % TERMINAL_COLORS.length ]
39
+ config
40
+ end
41
+ else
42
+ offset = 360/configs.size
43
+ configs.map.with_index do |config, i|
44
+ config.color = Color::HSL.new(offset * i, 100, 50).html
45
+ config
46
+ end
47
+ end
48
+ end
49
+
50
+ def start_app_server(app_configs)
51
+ apps = app_configs.map do |app_config|
52
+ app = App.new app_config
53
+ AppProxy.new app, app_config
54
+ app
55
+ end
56
+
57
+ trap("INT") do
58
+ info "shutting down..."
59
+ apps.each { |app| app.stop! }
60
+ exit
61
+ end
62
+
63
+ sleep
64
+ end
65
+
66
+ def die(msg)
67
+ error msg
68
+ exit -1
69
+ end
70
+
71
+ def parser
72
+ @parser ||= Trollop::Parser.new do
73
+
74
+ version Ringleader::VERSION
75
+
76
+ banner <<-banner
77
+ ringleader - your socket app server host
78
+
79
+ SYNOPSIS
80
+
81
+ Ringleader runs, monitors, and proxies socket applications. Upon receiving a new
82
+ connection to a given port, ringleader will start the correct application and
83
+ proxy the connection to the now-running app. It also supports automatic timeout
84
+ for shutting down applications that haven't been used recently.
85
+
86
+ USAGE
87
+
88
+ ringleader <config.yml> [options+]
89
+
90
+ APPLICATIONS
91
+
92
+ Ringleader supports any application that runs in the foreground (not
93
+ daemonized), and listens on a port. It expects applications to be well-behaved,
94
+ that is, respond appropriately to SIGHUP for graceful shutdown.
95
+
96
+ When first starting an app, ringleader will wait for the application's port to
97
+ open, at which point it will proxy the incoming connection through.
98
+
99
+ SIGNALS
100
+
101
+ While ringleader is running, Ctrl+C (SIGINT) will gracefully shut down
102
+ ringleader as well as the applications it's hosting.
103
+
104
+ CONFIGURATION
105
+
106
+ Ringleader requires a configuration .yml file to operate. The file should look
107
+ something like this:
108
+
109
+ ---
110
+ # name of app (used in logging)
111
+ main_app:
112
+ # working directory, where to start the app from
113
+ dir: "~/apps/main"
114
+ # the command to run to start up the app server. Executed under "bash -c".
115
+ command: "foreman start"
116
+ # the host to listen on, defaults to 127.0.0.1
117
+ hostname: 0.0.0.0
118
+ # the port ringleader listens on
119
+ server_port: 3000
120
+ # the port the application listens on
121
+ port: 4000
122
+ # idle timeout in seconds, defaults to 0. 0 means "never".
123
+ idle_timeout: 6000
124
+ other_app:
125
+ [...]
126
+
127
+ OPTIONS
128
+ banner
129
+
130
+ opt "verbose", "log at debug level", :long => "--verbose", :short => "-v", :type => :boolean
131
+ opt "boring", "use boring colors instead of a fabulous rainbow", :long => "--boring", :short => "-b", :type => :boolean, :default => false
132
+
133
+ end
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,36 @@
1
+ module Ringleader
2
+ class Config
3
+
4
+ DEFAULT_IDLE_TIMEOUT = 0
5
+ DEFAULT_HOSTNAME = "127.0.0.1"
6
+ REQUIRED_KEYS = %w(dir command server_port port idle_timeout)
7
+
8
+ def initialize(file)
9
+ @config = YAML.load(File.read(file))
10
+ end
11
+
12
+ def apps
13
+ unless @app
14
+ configs = @config.map do |name, options|
15
+ options["name"] = name
16
+ options["hostname"] ||= DEFAULT_HOSTNAME
17
+ options["idle_timeout"] ||= DEFAULT_IDLE_TIMEOUT
18
+ validate name, options
19
+ [name, OpenStruct.new(options)]
20
+ end
21
+
22
+ @apps = Hash[*configs.flatten]
23
+ end
24
+ @apps
25
+ end
26
+
27
+ # Private: validate that the options have all of the required keys
28
+ def validate(name, options)
29
+ REQUIRED_KEYS.each do |key|
30
+ unless options.has_key?(key)
31
+ raise "#{key} missing in #{name} config"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ module Ringleader
2
+ module NameLogger
3
+ # Send a debug message
4
+ def debug(string)
5
+ super with_name(string)
6
+ end
7
+
8
+ # Send a info message
9
+ def info(string)
10
+ super with_name(string)
11
+ end
12
+
13
+ # Send a warning message
14
+ def warn(string)
15
+ super with_name(string)
16
+ end
17
+
18
+ # Send an error message
19
+ def error(string)
20
+ super with_name(string)
21
+ end
22
+
23
+ def with_name(string)
24
+ colorized = config.name.color(config.color)
25
+ "#{colorized} | #{string}"
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ module Ringleader
2
+
3
+ # Proxies data to and from a server socket to a new downstream connection.
4
+ #
5
+ # This is fire-and-forget: create the SocketProxy and off it goes.
6
+ #
7
+ # Closes the server connection when proxying is complete and terminates the
8
+ # actor.
9
+ class SocketProxy
10
+ include Celluloid::IO
11
+ include Celluloid::Logger
12
+
13
+ def initialize(upstream, host, port)
14
+ @upstream = upstream
15
+
16
+ debug "proxying to #{host}:#{port}"
17
+ @socket = TCPSocket.new(host, port)
18
+
19
+ proxy! @socket, @upstream
20
+ proxy! @upstream, @socket
21
+
22
+ rescue Errno::ECONNREFUSED
23
+ error "could not proxy to #{host}:#{port}"
24
+ @upstream.close
25
+ terminate
26
+ end
27
+
28
+ def proxy(from, to)
29
+ ::IO.copy_stream from, to
30
+ rescue IOError
31
+ # from or to were closed
32
+ ensure
33
+ from.close unless from.closed?
34
+ to.close unless to.closed?
35
+ terminate
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,3 @@
1
+ module Ringleader
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,16 @@
1
+ module Ringleader
2
+ class WaitForExit
3
+ include Celluloid
4
+
5
+ def initialize(pid, app)
6
+ @pid, @app = pid, app
7
+ wait!
8
+ end
9
+
10
+ def wait
11
+ Process.waitpid @pid
12
+ @app.exited!
13
+ terminate
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ module Ringleader
2
+ class WaitForPort
3
+ include Celluloid
4
+ include Celluloid::Logger
5
+
6
+ def initialize(host, port, app)
7
+ @host, @port, @app = host, port, app
8
+ wait!
9
+ end
10
+
11
+ def wait
12
+ begin
13
+ socket = TCPSocket.new @host, @port
14
+ rescue Errno::ECONNREFUSED
15
+ sleep 0.5
16
+ debug "#{@host}:#{@port} not open yet"
17
+ retry
18
+ end
19
+ debug "#{@host}:#{@port} open"
20
+ @app.port_opened!
21
+ terminate
22
+ end
23
+ end
24
+ end
data/lib/ringleader.rb ADDED
@@ -0,0 +1,22 @@
1
+ require "ringleader/version"
2
+
3
+ require "yaml"
4
+ require "ostruct"
5
+ require "celluloid"
6
+ require "celluloid/io"
7
+ require "pty"
8
+ require "trollop"
9
+ require "rainbow"
10
+ require "color"
11
+
12
+ module Ringleader
13
+ end
14
+
15
+ require "ringleader/config"
16
+ require "ringleader/name_logger"
17
+ require "ringleader/wait_for_exit"
18
+ require "ringleader/wait_for_port"
19
+ require "ringleader/socket_proxy"
20
+ require "ringleader/app"
21
+ require "ringleader/app_proxy"
22
+ require "ringleader/cli"
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ringleader/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Nathan Witmer"]
6
+ gem.email = ["nwitmer@gmail.com"]
7
+ gem.description = %q{TCP application host and proxy server}
8
+ gem.summary = %q{Proxy TCP connections to an on-demand pool of configured applications}
9
+ gem.homepage = "https://github.com/aniero/ringleader"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "ringleader"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Ringleader::VERSION
17
+ gem.required_ruby_version = "~> 1.9.3"
18
+
19
+ gem.add_dependency "celluloid", "~> 0.11.0"
20
+ gem.add_dependency "celluloid-io", "~> 0.11.0"
21
+ gem.add_dependency "trollop", "~> 1.16.2"
22
+ gem.add_dependency "rainbow", "~> 1.1.4"
23
+ gem.add_dependency "color", "~> 1.4.1"
24
+
25
+ gem.add_development_dependency "rspec", "~> 2.11.0"
26
+ gem.add_development_dependency "guard-rspec"
27
+ end
@@ -0,0 +1,18 @@
1
+ ---
2
+ main_site:
3
+ dir: "~/apps/main"
4
+ command: "bundle exec foreman start"
5
+ hostname: "0.0.0.0"
6
+ server_port: 3000
7
+ port: 4000
8
+ idle_timeout: 1800
9
+ admin:
10
+ dir: "~/apps/admin"
11
+ command: "bundle exec foreman start"
12
+ server_port: 3001
13
+ port: 4001
14
+ authentication:
15
+ dir: "~/apps/auth"
16
+ command: "bundle exec foreman start"
17
+ server_port: 3002
18
+ port: 4002
@@ -0,0 +1,5 @@
1
+ ---
2
+ main_site:
3
+ dir: "~/apps/main"
4
+ server_port: 3000
5
+ port: 4000
@@ -0,0 +1,40 @@
1
+ require "spec_helper"
2
+
3
+ describe Ringleader::Config do
4
+
5
+ context "when initialized with a config file" do
6
+ subject { Ringleader::Config.new "spec/fixtures/config.yml" }
7
+
8
+ describe "#apps" do
9
+ it "returns a list of app configs" do
10
+ expect(subject.apps).to have(3).entries
11
+ end
12
+
13
+ it "returns a hash of configurations" do
14
+ config = subject.apps["main_site"]
15
+ expect(config.dir).to eq("~/apps/main")
16
+ end
17
+
18
+ it "includes a default hostname" do
19
+ expect(subject.apps["admin"].hostname).to eq("127.0.0.1")
20
+ end
21
+
22
+ it "includes a default idle timeout" do
23
+ expect(subject.apps["admin"].idle_timeout).to eq(0)
24
+ end
25
+
26
+ it "sets the config name to match the key in the config file" do
27
+ expect(subject.apps["admin"].name).to eq("admin")
28
+ end
29
+ end
30
+ end
31
+
32
+ context "when initialized with an invalid config" do
33
+ subject { Ringleader::Config.new "spec/fixtures/invalid.yml" }
34
+
35
+ it "raises an exception" do
36
+ expect { subject.apps }.to raise_error(/command.*missing/i)
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,22 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+ config.expect_with :rspec do |c|
12
+ c.syntax = :expect
13
+ end
14
+
15
+ # Run specs in random order to surface order dependencies. If you find an
16
+ # order dependency and want to debug it, you can fix the order by providing
17
+ # the seed, which is printed after each run.
18
+ # --seed 1234
19
+ config.order = 'random'
20
+ end
21
+
22
+ require "ringleader"
metadata ADDED
@@ -0,0 +1,193 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ringleader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nathan Witmer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: celluloid
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.11.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.11.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: celluloid-io
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.11.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.11.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: trollop
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 1.16.2
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.16.2
62
+ - !ruby/object:Gem::Dependency
63
+ name: rainbow
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 1.1.4
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 1.1.4
78
+ - !ruby/object:Gem::Dependency
79
+ name: color
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 1.4.1
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 1.4.1
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 2.11.0
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 2.11.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: guard-rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: TCP application host and proxy server
127
+ email:
128
+ - nwitmer@gmail.com
129
+ executables:
130
+ - ringleader
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - .gitignore
135
+ - .rspec
136
+ - .rvmrc
137
+ - Gemfile
138
+ - Guardfile
139
+ - LICENSE
140
+ - README.md
141
+ - Rakefile
142
+ - bin/ringleader
143
+ - dev_scripts/Procfile
144
+ - dev_scripts/echo_server.rb
145
+ - dev_scripts/signaling.rb
146
+ - dev_scripts/signals.rb
147
+ - dev_scripts/sleep_loop.rb
148
+ - dev_scripts/stubborn.rb
149
+ - dev_scripts/test.yml
150
+ - lib/ringleader.rb
151
+ - lib/ringleader/app.rb
152
+ - lib/ringleader/app_proxy.rb
153
+ - lib/ringleader/cli.rb
154
+ - lib/ringleader/config.rb
155
+ - lib/ringleader/name_logger.rb
156
+ - lib/ringleader/socket_proxy.rb
157
+ - lib/ringleader/version.rb
158
+ - lib/ringleader/wait_for_exit.rb
159
+ - lib/ringleader/wait_for_port.rb
160
+ - ringleader.gemspec
161
+ - spec/fixtures/config.yml
162
+ - spec/fixtures/invalid.yml
163
+ - spec/ringleader/config_spec.rb
164
+ - spec/spec_helper.rb
165
+ homepage: https://github.com/aniero/ringleader
166
+ licenses: []
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ none: false
173
+ requirements:
174
+ - - ~>
175
+ - !ruby/object:Gem::Version
176
+ version: 1.9.3
177
+ required_rubygems_version: !ruby/object:Gem::Requirement
178
+ none: false
179
+ requirements:
180
+ - - ! '>='
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubyforge_project:
185
+ rubygems_version: 1.8.24
186
+ signing_key:
187
+ specification_version: 3
188
+ summary: Proxy TCP connections to an on-demand pool of configured applications
189
+ test_files:
190
+ - spec/fixtures/config.yml
191
+ - spec/fixtures/invalid.yml
192
+ - spec/ringleader/config_spec.rb
193
+ - spec/spec_helper.rb