├── .ruby-version
├── .ruby-gemset
├── .rspec
├── lib
├── ringleader
│ ├── version.rb
│ ├── wait_for_exit.rb
│ ├── app_serializer.rb
│ ├── name_logger.rb
│ ├── controller.rb
│ ├── wait_for_port.rb
│ ├── server.rb
│ ├── config.rb
│ ├── celluloid_fix.rb
│ ├── app.rb
│ ├── cli.rb
│ └── process.rb
└── ringleader.rb
├── screenshot.png
├── assets
├── favicon.ico
├── top_hat.png
├── bootstrap
│ ├── img
│ │ ├── glyphicons-halflings.png
│ │ └── glyphicons-halflings-white.png
│ ├── css
│ │ ├── bootstrap-responsive.min.css
│ │ └── bootstrap-responsive.css
│ └── js
│ │ ├── bootstrap.min.js
│ │ └── bootstrap.js
├── app.js
├── index.html
├── underscore-min.js
├── backbone-min.js
└── jquery.mustache.js
├── bin
└── ringleader
├── spec
├── fixtures
│ ├── invalid.yml
│ ├── rvm.yml
│ ├── no_app_port.yml
│ ├── no_server_port.yml
│ ├── chruby.yml
│ ├── rbenv.yml
│ ├── invalid_app_dir.yml
│ └── config.yml
├── spec_helper.rb
└── ringleader
│ └── config_spec.rb
├── dev_scripts
├── webserver.rb
├── stubborn.rb
├── Procfile
├── test.yml
├── wait_fork_tree.rb
├── bad_parent.rb
├── wait_test.rb
├── sleep_loop.rb
├── echo_server.rb
├── signaling.rb
├── signals.rb
└── many.yml
├── Guardfile
├── .gitignore
├── CHANGES.md
├── Gemfile
├── Rakefile
├── LICENSE
├── ringleader.gemspec
└── README.md
/.ruby-version:
--------------------------------------------------------------------------------
1 | 1.9.3
2 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | ringleader
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format progress
3 |
--------------------------------------------------------------------------------
/lib/ringleader/version.rb:
--------------------------------------------------------------------------------
1 | module Ringleader
2 | VERSION = "1.1.8"
3 | end
4 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerowidth/ringleader/HEAD/screenshot.png
--------------------------------------------------------------------------------
/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerowidth/ringleader/HEAD/assets/favicon.ico
--------------------------------------------------------------------------------
/assets/top_hat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerowidth/ringleader/HEAD/assets/top_hat.png
--------------------------------------------------------------------------------
/bin/ringleader:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'ringleader'
4 | Ringleader::CLI.new.run(ARGV)
5 |
--------------------------------------------------------------------------------
/spec/fixtures/invalid.yml:
--------------------------------------------------------------------------------
1 | ---
2 | main_site:
3 | dir: "~/apps/main"
4 | server_port: 3000
5 | app_port: 4000
6 |
--------------------------------------------------------------------------------
/spec/fixtures/rvm.yml:
--------------------------------------------------------------------------------
1 | ---
2 | rvm_app:
3 | dir: "~/apps/main"
4 | rvm: "foreman start"
5 | server_port: 3000
6 | app_port: 4000
7 |
--------------------------------------------------------------------------------
/assets/bootstrap/img/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerowidth/ringleader/HEAD/assets/bootstrap/img/glyphicons-halflings.png
--------------------------------------------------------------------------------
/spec/fixtures/no_app_port.yml:
--------------------------------------------------------------------------------
1 | ---
2 | main_site:
3 | dir: "~/apps/main"
4 | command: "bundle exec foreman start"
5 | server_port: 3000
6 |
--------------------------------------------------------------------------------
/spec/fixtures/no_server_port.yml:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | main_site:
4 | dir: "~/apps/main"
5 | command: "bundle exec foreman start"
6 | app_port: 4000
7 |
--------------------------------------------------------------------------------
/assets/bootstrap/img/glyphicons-halflings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerowidth/ringleader/HEAD/assets/bootstrap/img/glyphicons-halflings-white.png
--------------------------------------------------------------------------------
/dev_scripts/webserver.rb:
--------------------------------------------------------------------------------
1 | require "ringleader"
2 | server = Ringleader::Server.new "0.0.0.0", 42000
3 | trap(:INT) { server.terminate; exit }
4 | sleep
5 |
--------------------------------------------------------------------------------
/spec/fixtures/chruby.yml:
--------------------------------------------------------------------------------
1 | ---
2 | chruby_app:
3 | dir: "/Users/demo/apps/main"
4 | chruby: "foreman start"
5 | server_port: 3000
6 | app_port: 4000
7 |
--------------------------------------------------------------------------------
/spec/fixtures/rbenv.yml:
--------------------------------------------------------------------------------
1 | ---
2 | rbenv_app:
3 | dir: "~/apps/main"
4 | rbenv: "bundle exec foreman start"
5 | server_port: 3000
6 | app_port: 4000
7 |
8 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
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 |
7 |
--------------------------------------------------------------------------------
/spec/fixtures/invalid_app_dir.yml:
--------------------------------------------------------------------------------
1 | ---
2 | main_site:
3 | dir: "~/does_not_exist"
4 | command: "bundle exec foreman start"
5 | host: "0.0.0.0"
6 | server_port: 3000
7 | app_port: 4000
8 | idle_timeout: 1800
9 |
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
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
18 |
--------------------------------------------------------------------------------
/dev_scripts/stubborn.rb:
--------------------------------------------------------------------------------
1 | trap "INT" do
2 | STDERR.puts "BRO DON'T INTERRUPT ME"
3 | end
4 |
5 | trap "HUP" do
6 | STDERR.puts "LOL, NOT QUITTING"
7 | end
8 |
9 | at_exit do
10 | STDERR.puts "FUCK YOUUUUUUU"
11 | end
12 |
13 | sleep
14 |
--------------------------------------------------------------------------------
/dev_scripts/Procfile:
--------------------------------------------------------------------------------
1 | # loop: ruby sleep_loop.rb 5
2 | # listen: sleep 3 && ncat -k -l 10001
3 | listen: ncat -k -l 10001
4 | # slow_echo: sleep 10 && ruby echo_server.rb
5 | # echo: ruby echo_server.rb
6 | # sleep: sleep 30
7 | # stubborn: ruby stubborn.rb
8 | orphans: ruby bad_parent.rb
9 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | 1.0.1
2 | ---
3 | * Warn but proxy anyway to processes running outside ringleader's control
4 | * Add support for environment variable overrides in app config file
5 | * Support `rbenv` key in config for rbenv-managed projects
6 |
7 | 1.0.0
8 | ---
9 | * Initial release
10 |
--------------------------------------------------------------------------------
/dev_scripts/test.yml:
--------------------------------------------------------------------------------
1 | ---
2 | test:
3 | dir: "./dev_scripts"
4 | # command: "ncat -k -l 10001"
5 | command: "bundle exec foreman start"
6 | # command: sleep 10
7 | server_port: 10000
8 | app_port: 10001
9 | idle_timeout: 5
10 | startup_timeout: 5
11 | kill_with: "INT"
12 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
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
15 |
--------------------------------------------------------------------------------
/dev_scripts/wait_fork_tree.rb:
--------------------------------------------------------------------------------
1 | if fork
2 | if fork
3 | puts "parent #{$$}"
4 | sleep 1
5 | puts "exiting #{$$}"
6 | else
7 | puts "child 2 #{$$}"
8 | sleep 0.25
9 | puts "child 2 exiting #{$$}"
10 | end
11 | else
12 | puts "child #{$$}"
13 | sleep 0.5
14 | puts "exiting #{$$}"
15 | end
16 |
--------------------------------------------------------------------------------
/lib/ringleader/wait_for_exit.rb:
--------------------------------------------------------------------------------
1 | module Ringleader
2 | class WaitForExit
3 | include Celluloid
4 |
5 | def initialize(pid, app)
6 | @pid, @app = pid, app
7 | async.wait
8 | end
9 |
10 | def wait
11 | ::Process.waitpid @pid
12 | @app.async.exited
13 | terminate
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'bundler'
3 | Bundler::GemHelper.install_tasks
4 |
5 | require 'rspec/core/rake_task'
6 |
7 | desc 'Default: run unit tests.'
8 | task :default => :spec
9 |
10 | desc "Run all specs"
11 | RSpec::Core::RakeTask.new do |t|
12 | t.pattern = 'spec/**/*_spec.rb'
13 | t.rspec_opts = ["-c", "-f progress"]
14 | end
--------------------------------------------------------------------------------
/lib/ringleader/app_serializer.rb:
--------------------------------------------------------------------------------
1 | module Ringleader
2 | class AppSerializer
3 | def initialize(app)
4 | @app = app
5 | end
6 |
7 | def to_json(*args)
8 | {
9 | "name" => @app.name,
10 | "enabled" => @app.enabled?,
11 | "running" => @app.running?
12 | }.to_json(*args)
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/dev_scripts/bad_parent.rb:
--------------------------------------------------------------------------------
1 | trap("INT") { STDERR.puts "#{$$} ignoring INT" }
2 | trap("HUP") { STDERR.puts "#{$$} ignoring HUP" }
3 | trap("TERM") { STDERR.puts "#{$$} ignoring TERM" }
4 |
5 | @extra = 0
6 | 3.times do |n|
7 | STDERR.puts "#{$$} forking (#{n})"
8 | if pid = fork
9 | STDERR.puts "#{$$} forked child #{pid}"
10 | break
11 | else
12 | @extra += 1
13 | end
14 | end
15 | sleep 10 + @extra * 2 # do clean up, eventually, with parent dying first
16 |
--------------------------------------------------------------------------------
/spec/fixtures/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | main_site:
3 | dir: "~/apps/main"
4 | command: "bundle exec foreman start"
5 | host: "0.0.0.0"
6 | server_port: 3000
7 | app_port: 4000
8 | idle_timeout: 1800
9 | admin:
10 | dir: "~/apps/admin"
11 | command: "bundle exec foreman start"
12 | server_port: 3001
13 | app_port: 4001
14 | env:
15 | OVERRIDE: true
16 | authentication:
17 | dir: "~/apps/auth"
18 | command: "bundle exec foreman start"
19 | server_port: 3002
20 | app_port: 4002
21 |
--------------------------------------------------------------------------------
/dev_scripts/wait_test.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby -wKU
2 |
3 |
4 | pid = Process.spawn "ruby wait_fork_tree.rb", :pgroup => true
5 | puts "forked: #{pid}"
6 |
7 | # unicorn-style
8 | def reap(pid)
9 | begin
10 | wpid = Process.waitpid(-1)#, Process::WNOHANG)
11 | if wpid
12 | puts "got child pid #{wpid}"
13 | else
14 | puts "no child pid"
15 | # return
16 | end
17 | rescue Errno::ECHILD
18 | puts "NO CHILD"
19 | break
20 | end while true
21 | end
22 |
23 | reap pid
24 |
25 | # welp, can't wait for grandchildren. oh well.
26 |
--------------------------------------------------------------------------------
/dev_scripts/sleep_loop.rb:
--------------------------------------------------------------------------------
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" }
27 |
--------------------------------------------------------------------------------
/lib/ringleader/name_logger.rb:
--------------------------------------------------------------------------------
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
30 |
--------------------------------------------------------------------------------
/lib/ringleader/controller.rb:
--------------------------------------------------------------------------------
1 | module Ringleader
2 | class Controller
3 | include Celluloid
4 | include Celluloid::Logger
5 |
6 | def initialize(configs)
7 | @apps = {}
8 | configs.each do |name, config|
9 | @apps[name] = App.new(config)
10 | end
11 | end
12 |
13 | def apps
14 | @apps.values.sort_by { |a| a.name }
15 | end
16 |
17 | def app(name)
18 | @apps[name]
19 | end
20 |
21 | def stop
22 | exit if @stopping # if ctrl-c called twice...
23 | @stopping = true
24 | info "shutting down..."
25 | @apps.values.map do |app|
26 | Thread.new { app.stop(:forever) if app.alive? }
27 | end.map(&:join)
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/ringleader.rb:
--------------------------------------------------------------------------------
1 | require "ringleader/version"
2 |
3 | require "yaml"
4 | require "ostruct"
5 | require "json"
6 | require "celluloid"
7 | require "celluloid/io"
8 | require "reel"
9 | require "pty"
10 | require "trollop"
11 | require "rainbow"
12 | require "color"
13 | require "pathname"
14 | require 'sys/proctable'
15 |
16 | module Ringleader
17 | end
18 |
19 | require 'ringleader/celluloid_fix'
20 | require "ringleader/config"
21 | require "ringleader/name_logger"
22 | require "ringleader/wait_for_exit"
23 | require "ringleader/wait_for_port"
24 | require "ringleader/process"
25 | require "ringleader/app"
26 | require "ringleader/app_serializer"
27 | require "ringleader/controller"
28 | require "ringleader/server"
29 | require "ringleader/cli"
30 |
--------------------------------------------------------------------------------
/lib/ringleader/wait_for_port.rb:
--------------------------------------------------------------------------------
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 | async.wait
9 | end
10 |
11 | def wait
12 | begin
13 | TCPSocket.new @host, @port
14 | rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
15 | debug "#{@host}:#{@port} not open yet"
16 | sleep 0.5
17 | retry
18 | rescue IOError, SystemCallError => e
19 | error "unexpected error while waiting for port: #{e}"
20 | sleep 0.5
21 | retry
22 | end
23 | debug "#{@host}:#{@port} open"
24 | @app.async.port_opened
25 | terminate
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
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"
23 |
--------------------------------------------------------------------------------
/dev_scripts/echo_server.rb:
--------------------------------------------------------------------------------
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
36 |
--------------------------------------------------------------------------------
/dev_scripts/signaling.rb:
--------------------------------------------------------------------------------
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
57 |
--------------------------------------------------------------------------------
/dev_scripts/signals.rb:
--------------------------------------------------------------------------------
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
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
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.
--------------------------------------------------------------------------------
/dev_scripts/many.yml:
--------------------------------------------------------------------------------
1 | ---
2 | one:
3 | dir: "./dev_scripts"
4 | command: "ncat -k -l 10001"
5 | server_port: 10001
6 | app_port: 20001
7 | two:
8 | dir: "./dev_scripts"
9 | command: "ncat -k -l 10002"
10 | server_port: 10002
11 | app_port: 20002
12 | three:
13 | dir: "./dev_scripts"
14 | command: "ncat -k -l 10003"
15 | server_port: 10003
16 | app_port: 20003
17 | four:
18 | dir: "./dev_scripts"
19 | command: "ncat -k -l 10004"
20 | server_port: 10004
21 | app_port: 20004
22 | five:
23 | dir: "./dev_scripts"
24 | command: "ncat -k -l 10005"
25 | server_port: 10005
26 | app_port: 20005
27 | six:
28 | dir: "./dev_scripts"
29 | command: "ncat -k -l 10006"
30 | server_port: 10006
31 | app_port: 20006
32 | seven:
33 | dir: "./dev_scripts"
34 | command: "ncat -k -l 10007"
35 | server_port: 10007
36 | app_port: 20007
37 | eight:
38 | dir: "./dev_scripts"
39 | command: "ncat -k -l 10008"
40 | server_port: 10008
41 | app_port: 20008
42 | nine:
43 | dir: "./dev_scripts"
44 | command: "ncat -k -l 10009"
45 | server_port: 10009
46 | app_port: 20009
47 | ten:
48 | dir: "./dev_scripts"
49 | command: "ncat -k -l 10010"
50 | server_port: 10010
51 | app_port: 20010
52 |
--------------------------------------------------------------------------------
/ringleader.gemspec:
--------------------------------------------------------------------------------
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/zerowidth/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 | gem.license = 'MIT'
19 |
20 | gem.add_dependency "celluloid", "~> 0.15.0"
21 | gem.add_dependency "celluloid-io", "~> 0.15.0"
22 | gem.add_dependency "reel", "~> 0.3.0"
23 | gem.add_dependency "trollop", "~> 1.16.2"
24 | gem.add_dependency "rainbow", "~> 1.1.4"
25 | gem.add_dependency "color", "~> 1.4.1"
26 | gem.add_dependency "sys-proctable", "= 0.9.1"
27 | gem.add_dependency 'http', '= 0.4.0'
28 |
29 | gem.add_development_dependency "rake"
30 | gem.add_development_dependency "rspec", "~> 2.11.0"
31 | gem.add_development_dependency "guard-rspec"
32 | end
33 |
--------------------------------------------------------------------------------
/assets/app.js:
--------------------------------------------------------------------------------
1 | var App = Backbone.Model.extend({
2 | idAttribute: 'name',
3 |
4 | initialize: function() {
5 | var model = this;
6 | // unfortunately, there's not a nice way of updating apps collectively
7 | // without replacing the entire collection each time.
8 | this.refresh = setInterval(function() { model.fetch(); }, 5000);
9 | },
10 |
11 | url: function(extra) {
12 | if(extra) {
13 | return '/apps/' + this.get('name') + '/' + extra;
14 | }
15 | else {
16 | return '/apps/' + this.get('name');
17 | }
18 | },
19 |
20 | // for mustache, these must be exclusive
21 | name: function() { return this.get('name'); },
22 | 'disabled?': function() { return !this.get('enabled'); },
23 | 'stopped?': function() { return this.get('enabled') && !this.get('running'); },
24 | 'running?': function() { return this.get('enabled') && this.get('running'); },
25 |
26 | // actions
27 | enable: function() { this.request('enable'); },
28 | disable: function() { this.request('disable'); },
29 | stop: function() { this.request('stop'); },
30 | start: function() { this.request('start'); },
31 | restart: function() { this.request('restart'); },
32 |
33 | request: function(action) {
34 | var model = this;
35 | this.set({waiting: true});
36 | console.log(action, this.get('name'), this.url(action));
37 | $.post(
38 | this.url(action),
39 | function(data) { model.set(_.extend({}, data, {waiting: false})); },
40 | 'json');
41 | }
42 | });
43 |
44 | var Apps = Backbone.Collection.extend({
45 | url: '/apps',
46 | model: App
47 | });
48 |
49 | var AppControl = Backbone.View.extend({
50 | className: 'app',
51 | initialize: function() {
52 | this.template = $('#app-template');
53 | this.model.bind('change', this.render, this);
54 | },
55 | events: {
56 | 'click .start' : 'start',
57 | 'click .stop' : 'stop',
58 | 'click .restart' : 'restart',
59 | 'click .enable' : 'enable',
60 | 'click .disable' : 'disable'
61 | },
62 | render: function() {
63 | $(this.el).html(this.template.mustache(this.model));
64 | if(this.model.get('waiting')) {
65 | this.$('.buttons').hide();
66 | this.$('.loading').show();
67 | }
68 | else {
69 | this.$('.buttons').show();
70 | this.$('.loading').hide();
71 | }
72 | return this;
73 | },
74 | start: function() { this.model.start(); return false; },
75 | stop: function() { this.model.stop(); return false; },
76 | restart: function() { this.model.restart(); return false; },
77 | enable: function() { this.model.enable(); return false; },
78 | disable: function() { this.model.disable(); return false; }
79 | });
80 |
81 | var ControlPanel = Backbone.View.extend({
82 | el: $('#apps'),
83 | initialize: function(options) {
84 | this.collection = new Apps();
85 | this.collection.bind('add', this.addApp, this);
86 | this.collection.bind('reset', this.addAll, this);
87 | this.collection.fetch();
88 |
89 | var self = this;
90 | },
91 |
92 | addApp: function(app) {
93 | var view = new AppControl({model: app});
94 | this.$el.append(view.render().el);
95 | },
96 |
97 | addAll: function(apps) {
98 | this.$el.html('');
99 | apps.each(this.addApp, this);
100 | }
101 |
102 | });
103 |
104 | $(function() {
105 | window.control_panel = new ControlPanel();
106 | });
107 |
--------------------------------------------------------------------------------
/lib/ringleader/server.rb:
--------------------------------------------------------------------------------
1 | module Ringleader
2 | class Server < Reel::Server
3 | include Celluloid::Logger
4 |
5 | ASSET_PATH = Pathname.new(File.expand_path("../../../assets", __FILE__))
6 | ACTIONS = %w(enable disable stop start restart).freeze
7 |
8 | def initialize(controller, host, port)
9 | debug "starting webserver on #{host}:#{port}"
10 | super host, port, &method(:on_connection)
11 | @controller = controller
12 | info "web control panel started on http://#{host}:#{port}"
13 | end
14 |
15 | def on_connection(connection)
16 | request = connection.request
17 | route request if request
18 | end
19 |
20 | # thanks to dcell explorer for this code
21 | def route(request)
22 | if request.url == "/"
23 | path = "index.html"
24 | else
25 | path = request.url[%r{^/([a-z0-9\.\-_]+(/[a-z0-9\.\-_]+)*)$}, 1]
26 | end
27 |
28 | if !path or path[".."]
29 | request.respond :not_found, "Not found"
30 | debug "404 #{path}"
31 | return
32 | end
33 |
34 | case request.method
35 | when "GET"
36 | if path == "apps"
37 | app_index request
38 | elsif path =~ %r(^apps/\w+)
39 | show_app path, request
40 | else
41 | static_file path, request
42 | end
43 | when "POST"
44 | update_app path, request
45 | else
46 | error "unknown #{request.method} request to #{request.url}"
47 | end
48 | end
49 |
50 | def app_index(request)
51 | json = @controller.apps.map { |app| app_as_json(app) }.to_json
52 | request.respond :ok, json
53 | debug "GET /apps: 200"
54 | end
55 |
56 | def static_file(path, request)
57 | filename = ASSET_PATH + path
58 | if filename.exist?
59 | mime_type = content_type_for filename.extname
60 | filename.open("r") do |file|
61 | request.respond :ok, {"Content-type" => mime_type}, file
62 | end
63 | debug "GET #{path}: 200"
64 | else
65 | request.respond :not_found, "Not found"
66 | debug "GET #{path}: 404"
67 | end
68 | end
69 |
70 | def show_app(uri, request)
71 | _, name, _ = uri.split("/")
72 | app = @controller.app name
73 | if app
74 | request.respond :ok, app_as_json(app).to_json
75 | debug "GET #{uri}: 200"
76 | else
77 | request.respond :not_found, "Not found"
78 | debug "GET #{uri}: 404"
79 | end
80 | end
81 |
82 | def update_app(uri, request)
83 | _, name, action = uri.split("/")
84 | app = @controller.app name
85 | if app && ACTIONS.include?(action)
86 | app.send action
87 | request.respond :ok, app_as_json(app).to_json
88 | debug "POST #{uri}: 200"
89 | else
90 | request.respond :not_found, "Not found"
91 | debug "POST #{uri}: 404"
92 | end
93 | end
94 |
95 | def app_as_json(app)
96 | AppSerializer.new(app)
97 | end
98 |
99 | def content_type_for(extname)
100 | case extname
101 | when ".html"
102 | "text/html"
103 | when ".js"
104 | "application/json"
105 | when ".css"
106 | "text/css"
107 | when ".ico"
108 | "image/x-icon"
109 | when ".png"
110 | "image/png"
111 | else
112 | "text/plain"
113 | end
114 | end
115 |
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/lib/ringleader/config.rb:
--------------------------------------------------------------------------------
1 | module Ringleader
2 | class Config
3 | include Celluloid::Logger
4 |
5 | DEFAULT_IDLE_TIMEOUT = 1800
6 | DEFAULT_STARTUP_TIMEOUT = 30
7 | DEFAULT_HOST= "127.0.0.1"
8 | REQUIRED_KEYS = %w(dir command app_port server_port)
9 |
10 | TERMINAL_COLORS = [:red, :green, :yellow, :blue, :magenta, :cyan]
11 |
12 | attr_reader :apps
13 |
14 | # Public: Load the configs from a file
15 | #
16 | # file - the yml file to load the config from
17 | # boring - use terminal colors instead of a rainbow for app colors
18 | def initialize(file, boring=false)
19 | config_data = YAML.load(File.read(file))
20 | configs = convert_and_validate config_data, boring
21 | @apps = Hash[*configs.flatten]
22 | end
23 |
24 | # Internal: convert a YML hash to an array of name/OpenStruct pairs
25 | #
26 | # Does validation for each app config and raises an error if anything is
27 | # wrong. Sets default values for missing options, and assigns colors to each
28 | # app config.
29 | #
30 | # configs - a hash of config data
31 | # boring - whether or not to use a rainbow of colors for the apps
32 | #
33 | # Returns [ [app_name, OpenStruct], ... ]
34 | def convert_and_validate(configs, boring)
35 | assign_colors configs, boring
36 | configs.map do |name, options|
37 | options["name"] = name
38 | options["host"] ||= DEFAULT_HOST
39 | options["idle_timeout"] ||= DEFAULT_IDLE_TIMEOUT
40 | options["startup_timeout"] ||= DEFAULT_STARTUP_TIMEOUT
41 | options["kill_with"] ||= "INT"
42 | options["env"] ||= {}
43 |
44 | options["dir"] = File.expand_path options["dir"]
45 | unless File.directory?(options["dir"]) || options["disabled"]
46 | warn "#{options["dir"]} does not exist!"
47 | end
48 |
49 | if command = options.delete("rvm")
50 | options["command"] = "source ~/.rvm/scripts/rvm && rvm in #{options["dir"]} do #{command}"
51 | elsif command = options.delete("chruby")
52 | options["command"] = "source /usr/local/share/chruby/chruby.sh ; source /usr/local/share/chruby/auto.sh ; cd #{options["dir"]} ; #{command}"
53 | elsif command = options.delete("rbenv")
54 | options["command"] = "rbenv exec #{command}"
55 | options["env"]["RBENV_VERSION"] = nil
56 | options["env"]["RBENV_DIR"] = nil
57 | options["env"]["GEM_HOME"] = nil
58 | end
59 |
60 | validate name, options
61 |
62 | [name, OpenStruct.new(options)]
63 | end
64 | end
65 |
66 | # Internal: validate that the options have all of the required keys
67 | def validate(name, options)
68 | REQUIRED_KEYS.each do |key|
69 | unless options.has_key?(key)
70 | raise ArgumentError, "#{key} missing in #{name} config"
71 | end
72 | end
73 | end
74 |
75 | # Internal: assign a color to each application configuration.
76 | #
77 | # configs - the config data to modify
78 | # boring - use boring standard terminal colors instead of a rainbow.
79 | def assign_colors(configs, boring)
80 | sorted = configs.sort_by(&:first).map(&:last)
81 | if boring
82 | sorted.each.with_index do |config, i|
83 | config["color"] = TERMINAL_COLORS[ i % TERMINAL_COLORS.length ]
84 | end
85 | else
86 | offset = 360/configs.size
87 | sorted.each.with_index do |config, i|
88 | config["color"] = Color::HSL.new(offset * i, 100, 50).html
89 | end
90 | end
91 | end
92 |
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/spec/ringleader/config_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Ringleader::Config do
4 |
5 | before :each do
6 | File.stub(:directory?) do |arg|
7 | if arg =~ %r(/apps/(main|admin|auth))
8 | true
9 | else
10 | false
11 | end
12 | end
13 | end
14 |
15 | context "when initialized with a config file" do
16 | subject { Ringleader::Config.new "spec/fixtures/config.yml" }
17 |
18 | describe "#apps" do
19 | it "returns a list of app configs" do
20 | expect(subject.apps).to have(3).entries
21 | end
22 |
23 | it "returns a hash of configurations" do
24 | config = subject.apps["main_site"]
25 | expect(config.dir).to eq(File.expand_path("~/apps/main"))
26 | end
27 |
28 | it "includes a default host" do
29 | expect(subject.apps["admin"].host).to eq("127.0.0.1")
30 | end
31 |
32 | it "includes a default idle timeout" do
33 | expect(subject.apps["admin"].idle_timeout).to eq(Ringleader::Config::DEFAULT_IDLE_TIMEOUT)
34 | end
35 |
36 | it "sets a default start timeout" do
37 | expect(subject.apps["admin"].startup_timeout).to eq(Ringleader::Config::DEFAULT_STARTUP_TIMEOUT)
38 | end
39 |
40 | it "sets the config name to match the key in the config file" do
41 | expect(subject.apps["admin"].name).to eq("admin")
42 | end
43 |
44 | it "sets the env hash to an empty hash if not specified" do
45 | expect(subject.apps["main_site"].env).to eq({})
46 | expect(subject.apps["admin"].env).to have_key("OVERRIDE")
47 | end
48 |
49 | it "sets 'INT' as the default kill signal" do
50 | expect(subject.apps["main_site"].kill_with).to eq("INT")
51 | end
52 | end
53 | end
54 |
55 | context "when initialized with an invalid config" do
56 | subject { Ringleader::Config.new "spec/fixtures/invalid.yml" }
57 |
58 | it "raises an exception" do
59 | expect { subject.apps }.to raise_error(/command.*missing/i)
60 | end
61 | end
62 |
63 | context "with a config without an app port" do
64 | it "raises an exception" do
65 | expect {
66 | Ringleader::Config.new("spec/fixtures/no_app_port.yml").apps
67 | }.to raise_error(/app_port/)
68 | end
69 | end
70 |
71 | context "with a config without a server port" do
72 | it "raises an exception" do
73 | expect {
74 | Ringleader::Config.new("spec/fixtures/no_server_port.yml").apps
75 | }.to raise_error(/server_port/)
76 | end
77 | end
78 |
79 | context "with a config with an 'rvm' key instead of a 'command'" do
80 | it "replaces the rvm command with a command to use rvm" do
81 | config = Ringleader::Config.new "spec/fixtures/rvm.yml"
82 | expect(config.apps["rvm_app"].command).to match(%r(rvm in \S+/apps/main do foreman start))
83 | end
84 | end
85 |
86 | context "with a config with an 'chruby' key instead of a 'command'" do
87 | it "replaces the command with a command to use chruby" do
88 | config = Ringleader::Config.new "spec/fixtures/chruby.yml"
89 | expect(config.apps["chruby_app"].command).to eq("source /usr/local/share/chruby/chruby.sh ; source /usr/local/share/chruby/auto.sh ; cd /Users/demo/apps/main ; foreman start")
90 | end
91 | end
92 |
93 | context "with a config with a 'rbenv' key instead of 'command'" do
94 | it "replaces the 'rbenv' command with an rbenv command and environment" do
95 | config = Ringleader::Config.new "spec/fixtures/rbenv.yml"
96 | expect(config.apps["rbenv_app"].command).to eq("rbenv exec bundle exec foreman start")
97 |
98 | %w(RBENV_VERSION RBENV_DIR GEM_HOME).each do |key|
99 | expect(config.apps["rbenv_app"].env).to have_key(key)
100 | expect(config.apps["rbenv_app"].env[key]).to eq(nil)
101 | end
102 | end
103 | end
104 |
105 | end
106 |
--------------------------------------------------------------------------------
/lib/ringleader/celluloid_fix.rb:
--------------------------------------------------------------------------------
1 | # FIXME: see zerowidth/ringleader#36 and celluloid/celluloid-io#23. If the
2 | # celluloid issue is ever merged, this monkeypatch / wholesale rewrite needs to
3 | # go away.
4 |
5 | require 'nio'
6 |
7 | module Celluloid
8 | module IO
9 | # React to external I/O events. This is kinda sorta supposed to resemble the
10 | # Reactor design pattern.
11 | class Reactor
12 | extend Forwardable
13 |
14 | # Unblock the reactor (i.e. to signal it from another thread)
15 | def_delegator :@selector, :wakeup
16 | # Terminate the reactor
17 | def_delegator :@selector, :close, :shutdown
18 |
19 | def initialize
20 | @selector = NIO::Selector.new
21 | @monitors = {}
22 | end
23 |
24 | # Wait for the given IO object to become readable
25 | def wait_readable(io)
26 | wait io do |monitor|
27 | monitor.wait_readable
28 | end
29 | end
30 |
31 | # Wait for the given IO object to become writable
32 | def wait_writable(io)
33 | wait io do |monitor|
34 | monitor.wait_writable
35 | end
36 | end
37 |
38 | # Wait for the given IO operation to complete
39 | def wait(io)
40 | # zomg ugly type conversion :(
41 | unless io.is_a?(::IO) or io.is_a?(OpenSSL::SSL::SSLSocket)
42 | if io.respond_to? :to_io
43 | io = io.to_io
44 | elsif ::IO.respond_to? :try_convert
45 | io = ::IO.try_convert(io)
46 | end
47 |
48 | raise TypeError, "can't convert #{io.class} into IO" unless io.is_a?(::IO)
49 | end
50 |
51 | unless monitor = @monitors[io]
52 | monitor = Monitor.new(@selector, io)
53 | @monitors[io] = monitor
54 | end
55 |
56 | yield monitor
57 | end
58 |
59 | # Run the reactor, waiting for events or wakeup signal
60 | def run_once(timeout = nil)
61 | @selector.select(timeout) do |monitor|
62 | monitor.value.resume
63 | end
64 | end
65 |
66 | class Monitor
67 | def initialize(selector, io)
68 | @selector = selector
69 | @io = io
70 | @interests = {}
71 | end
72 |
73 | def wait_readable
74 | wait :r
75 | end
76 |
77 | def wait_writable
78 | wait :w
79 | end
80 |
81 | def wait(interest)
82 | raise "Already waiting for #{interest.inspect}" if @interests.include?(interest)
83 | @interests[interest] = Task.current
84 | reregister
85 | Task.suspend :iowait
86 | end
87 |
88 | def reregister
89 | if @monitor
90 | @monitor.close
91 | @monitor = nil
92 | end
93 |
94 | if interests_symbol
95 | @monitor = @selector.register(@io, interests_symbol)
96 | @monitor.value = self
97 | end
98 | end
99 |
100 | def interests_symbol
101 | case @interests.keys
102 | when [:r]
103 | :r
104 | when [:w]
105 | :w
106 | when [:r, :w]
107 | :rw
108 | end
109 | end
110 |
111 | def resume
112 | raise "No monitor" unless @monitor
113 |
114 | if @monitor.readable?
115 | resume_for :r
116 | end
117 |
118 | if @monitor.writable?
119 | resume_for :w
120 | end
121 |
122 | reregister
123 | end
124 |
125 | def resume_for(interest)
126 | task = @interests.delete(interest)
127 |
128 | if task
129 | if task.running?
130 | task.resume
131 | else
132 | raise "reactor attempted to resume a dead task"
133 | end
134 | end
135 | end
136 | end
137 | end
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/lib/ringleader/app.rb:
--------------------------------------------------------------------------------
1 | module Ringleader
2 |
3 | # A configured application.
4 | #
5 | # Listens on a port, starts and runs the app process on demand, and proxies
6 | # network data to the process.
7 | class App
8 | include Celluloid::IO
9 | include Celluloid::Logger
10 |
11 | def initialize(config)
12 | @config = config
13 | @process = Process.new(config)
14 | async.enable unless config.disabled
15 | start if config.run_on_load
16 | end
17 |
18 | def name
19 | @config.name
20 | end
21 |
22 | def enabled?
23 | @enabled
24 | end
25 |
26 | def running?
27 | @process.running?
28 | end
29 |
30 | def start
31 | return if @process.running?
32 | info "starting #{@config.name}..."
33 | if @process.start
34 | start_activity_timer
35 | end
36 | end
37 |
38 | def stop(forever=false)
39 | return unless @process.running?
40 | info "stopping #{@config.name}..."
41 |
42 | if forever
43 | # stop processing requests
44 | @server.close
45 | @server = nil
46 | end
47 |
48 | stop_activity_timer
49 | @process.stop
50 | end
51 |
52 | def restart
53 | stop
54 | start
55 | end
56 |
57 | def enable
58 | return if @server
59 | @server = TCPServer.new @config.host, @config.server_port
60 | @enabled = true
61 | async.run
62 | rescue Errno::EADDRINUSE
63 | error "could not bind to #{@config.host}:#{@config.server_port} for #{@config.name}!"
64 | @server = nil
65 | end
66 |
67 | def disable
68 | info "disabling #{@config.name}..."
69 | return unless @server
70 | stop_activity_timer
71 | @server.close
72 | @server = nil
73 | @process.stop
74 | @enabled = false
75 | end
76 |
77 | def close_server_socket
78 | @server.close if @server && !@server.closed?
79 | @server = nil
80 | end
81 | finalizer :close_server_socket
82 |
83 | def run
84 | info "listening for connections for #{@config.name} on #{@config.host}:#{@config.server_port}"
85 | loop { async.handle_connection @server.accept }
86 | rescue IOError
87 | @server.close if @server
88 | end
89 |
90 | def handle_connection(socket)
91 | _, port, host = socket.peeraddr
92 | debug "received connection from #{host}:#{port}"
93 |
94 | started = @process.start
95 | if started
96 | async.proxy_to_app socket
97 | reset_activity_timer
98 | else
99 | error "could not start app"
100 | socket.close
101 | end
102 | end
103 |
104 | def proxy_to_app(upstream)
105 | debug "proxying to #{@config.host}:#{@config.app_port}"
106 |
107 | downstream = TCPSocket.new(@config.host, @config.app_port)
108 | async.proxy downstream, upstream
109 | async.proxy upstream, downstream
110 |
111 | rescue IOError, SystemCallError => e
112 | error "could not proxy to #{@config.host}:#{@config.app_port}: #{e}"
113 | upstream.close
114 | end
115 |
116 | def start_activity_timer
117 | return if @activity_timer || @config.idle_timeout == 0
118 | @activity_timer = every @config.idle_timeout do
119 | if @process.running?
120 | info "#{@config.name} has been idle for #{@config.idle_timeout} seconds, shutting it down"
121 | @process.stop
122 | end
123 | end
124 | end
125 |
126 | def reset_activity_timer
127 | start_activity_timer
128 | @activity_timer.reset if @activity_timer
129 | end
130 |
131 | def stop_activity_timer
132 | if @activity_timer
133 | @activity_timer.cancel
134 | @activity_timer = nil
135 | end
136 | end
137 |
138 | def proxy(from, to)
139 | ::IO.copy_stream from, to
140 | rescue IOError, SystemCallError
141 | # from or to were closed or connection was reset
142 | ensure
143 | from.close unless from.closed?
144 | to.close unless to.closed?
145 | end
146 |
147 | end
148 | end
149 |
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ringleader Control Panel
6 |
7 |
8 |
9 |
10 |
11 |
51 |
52 |
53 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
110 |
111 |
118 |
119 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/lib/ringleader/cli.rb:
--------------------------------------------------------------------------------
1 | module Ringleader
2 | class CLI
3 | include Celluloid::Logger
4 |
5 | RC_FILE = File.expand_path('~/.ringleaderrc')
6 |
7 | def run(argv)
8 | configure_logging
9 |
10 | opts = nil
11 | Trollop.with_standard_exception_handling parser do
12 | opts = merge_rc_opts(parser.parse(argv))
13 | end
14 |
15 | die "must provide a filename" if argv.empty?
16 | die "could not find config file #{argv.first}" unless File.exist?(argv.first)
17 |
18 | if opts.verbose
19 | Celluloid.logger.level = ::Logger::DEBUG
20 | end
21 |
22 | apps = Config.new(argv.first, opts.boring).apps
23 |
24 | controller = Controller.new(apps)
25 | Server.new(controller, opts.host, opts.port)
26 |
27 | # gracefully die instead of showing an interrupted sleep below
28 | trap("INT") do
29 | controller.stop
30 | exit
31 | end
32 |
33 | sleep
34 | end
35 |
36 | def configure_logging
37 | # set to INFO at first to hide celluloid's shutdown message until after
38 | # opts are validated.
39 | Celluloid.logger.level = ::Logger::INFO
40 | format = "%5s %s.%06d | %s\n"
41 | date_format = "%H:%M:%S"
42 | Celluloid.logger.formatter = lambda do |severity, time, progname, msg|
43 | format % [severity, time.strftime(date_format), time.usec, msg]
44 | end
45 | end
46 |
47 | def die(msg)
48 | error msg
49 | exit(-1)
50 | end
51 |
52 | def parser
53 | @parser ||= Trollop::Parser.new do
54 |
55 | version Ringleader::VERSION
56 |
57 | banner <<-banner
58 | ringleader - your socket app server host
59 |
60 | SYNOPSIS
61 |
62 | Ringleader runs, monitors, and proxies socket applications. Upon receiving a new
63 | connection to a given port, ringleader will start the correct application and
64 | proxy the connection to the now-running app. It also supports automatic timeout
65 | for shutting down applications that haven't been used recently.
66 |
67 | USAGE
68 |
69 | ringleader [options+]
70 |
71 | APPLICATIONS
72 |
73 | Ringleader supports any application that runs in the foreground (not
74 | daemonized), and listens on a port. It expects applications to be well-behaved,
75 | that is, respond appropriately to SIGINT for graceful shutdown.
76 |
77 | When first starting an app, ringleader will wait for the application's port to
78 | open, at which point it will proxy the incoming connection through.
79 |
80 | SIGNALS
81 |
82 | While ringleader is running, Ctrl+C (SIGINT) will gracefully shut down
83 | ringleader as well as the applications it's hosting.
84 |
85 | CONFIGURATION
86 |
87 | Ringleader requires a configuration .yml file to operate. The file should look
88 | something like this:
89 |
90 | ---
91 | # name of app (used in logging)
92 | main_app:
93 |
94 | # Required settings
95 | dir: "~/apps/main" # Working directory
96 | command: "foreman start" # The command to run to start up the app server.
97 | # Executed under "bash -c".
98 | server_port: 3000 # The port ringleader listens on
99 | app_port: 4000 # The port the application listens on
100 |
101 | # Optional settings
102 | host: 127.0.0.1 # The host ringleader should listen on
103 | idle_timeout: 6000 # Idle timeout in seconds, 0 for infinite
104 | startup_timeout: 180 # Application startup timeout
105 | disabled: true # Set the app to be disabled when ringleader starts
106 | env: # Override or set environment variables inherited
107 | FOO: hello # from the current environment. Use nil to unset a
108 | BAR: nil # var.
109 | kill_with: INT # Signal to use to kill the process tree with. Use
110 | # TERM or KILL if the default is leaving zombies.
111 | run_on_load: false # Set this to true to start an app when ringleader
112 | # loads.
113 |
114 | # If you have an application managed by rvm, this setting automatically
115 | # adds the rvm-specific shell setup before executing the given command.
116 | # This supersedes the `command` setting.
117 | rvm: "foreman start"
118 |
119 | # Likewise for rbenv:
120 | rbenv: "foreman start"
121 |
122 | OPTIONS
123 | banner
124 |
125 | opt :verbose, "log at debug level",
126 | :short => "-v", :default => false
127 | opt :host, "host for web control panel",
128 | :short => "-H", :default => "localhost"
129 | opt :port, "port for the web control panel",
130 | :short => "-p", :default => 42000
131 | opt :boring, "use boring colors instead of a fabulous rainbow",
132 | :short => "-b", :default => false
133 |
134 | end
135 | end
136 |
137 | def merge_rc_opts(opts)
138 | [:verbose, :host, :port, :boring].each do |option_name|
139 | if rc_opts.has_key?(option_name) && !opts["#{option_name}_given".to_sym]
140 | opts[option_name] = rc_opts[option_name]
141 | end
142 | end
143 | opts
144 | end
145 |
146 | def rc_opts
147 | unless @rc_opts
148 | if File.readable?(RC_FILE)
149 | info "reading options from ~/.ringleaderrc"
150 | @rc_opts = parser.parse File.read(RC_FILE).strip.split(/\s+/)
151 | else
152 | @rc_opts = {}
153 | end
154 | end
155 | @rc_opts
156 | end
157 |
158 | end
159 | end
160 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
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. Additionally, many of the apps required active
20 | [resque](https://github.com/defunkt/resque/) pollers to run properly. Ultimately
21 | this meant having many terminal windows open just to make a few requests to the
22 | apps. Instead, I wanted something to manage all that for me.
23 |
24 | Before, each app in a terminal, started manually:
25 |
26 | +-------+
27 | +->| app 1 |
28 | | +-------+
29 | +-----------+ |
30 | http | | | +-------+
31 | requests ---> | nginx +--+->| app 2 |
32 | | | | +-------+
33 | +-----------+ |
34 | | +-------+
35 | +->| app n |
36 | +-------+
37 |
38 | After, apps managed by ringleader, started on demand:
39 |
40 | +-------+
41 | +->| app 1 |
42 | | +-------+
43 | +-----------+ +--------------+ |
44 | http | | | | | +-------+
45 | requests ---> | nginx +--->| ringleader +--+->| app 2 |
46 | | | | | | +-------+
47 | +-----------+ +--------------+ |
48 | | +-------+
49 | +->| app n |
50 | +-------+
51 |
52 | Ringleader gives on-demand startup and proxying for any TCP server program. It
53 | can be a rails app managed with foreman, a node app, or simply a shell command
54 | to start netcat.
55 |
56 | ## Isn't this just like inetd?
57 |
58 | Pretty much. But with pretty colors in console and a nice web interface.
59 |
60 | ## Web interface?
61 |
62 | Yep. Hook it up with [fluid](http://fluidapp.com) and put it in the menu bar. By
63 | default it runs at [http://localhost:42000](http://localhost:42000).
64 |
65 |
66 | 
67 |
68 | ## Installation
69 |
70 | $ gem install ringleader
71 | $ ringleader --help
72 |
73 | ## Configuration
74 |
75 | Ringleader requires a yml configuration file to start. It should look something
76 | like this:
77 |
78 | ```yml
79 | ---
80 | # name of app (used in logging)
81 | main_app:
82 |
83 | # Required settings
84 | dir: "~/apps/main" # Working directory
85 | command: "foreman start" # The command to run to start up the app server.
86 | # Executed under "bash -c".
87 | server_port: 3000 # The port ringleader listens on
88 | app_port: 4000 # The port the application listens on
89 |
90 | # Optional settings
91 | host: 127.0.0.1 # The host ringleader should listen on
92 | idle_timeout: 6000 # Idle timeout in seconds, 0 for infinite
93 | startup_timeout: 180 # Application startup timeout
94 | disabled: true # Set the app to be disabled when ringleader starts
95 | env: # Override or set environment variables inherited
96 | FOO: hello # from the current environment. Use nil to unset a
97 | BAR: nil # var.
98 | kill_with: INT # Signal to use to kill the process tree with. Use
99 | # TERM or KILL if the default is leaving zombies.
100 | run_on_load: false # Set this to true to start an app when ringleader
101 | # loads.
102 |
103 | # If you have an application managed by rvm, this setting automatically adds
104 | # the rvm-specific shell setup before executing the given command. This
105 | # supersedes the `command` setting.
106 | rvm: "foreman start"
107 |
108 | # Likewise for rbenv:
109 | rbenv: "foreman start"
110 |
111 | # And chruby:
112 | chruby: "foreman start"
113 |
114 | other_app:
115 | [...]
116 | ```
117 |
118 | ## Known issues
119 |
120 | ### Too many open files - pipe (Errno::EMFILE)
121 |
122 | You may get this error if you have a high number of projects in your ringleader file. It happens because Celluloid::IO is trying to opens more file descriptors that your OS allows. This number is different for each version of SO and you can check it running ```ulimit -a```.
123 |
124 | You can increase the maximum number of open file descriptors using the ```ulimit -n NUMBER```. Currently I'm using ```ulimit -n 1024``` with a huge ringleader file.
125 |
126 | If you are using OS X [check it](http://superuser.com/questions/827984/open-files-limit-does-not-work-as-before-in-osx-yosemite).
127 |
128 | ## License
129 |
130 | MIT, see `LICENSE`.
131 |
132 | Top hat icon by [Luka Taylor](http://lukataylo.deviantart.com/gallery/#/d2g95fp)
133 | under a Creative Commons Attribution/Non-commercial license.
134 |
135 | ## Contributing
136 |
137 | 1. Fork it
138 | 2. Create your feature branch (`git checkout -b my-new-feature`)
139 | 3. Commit your changes (`git commit -am 'Add some feature'`)
140 | 4. Push to the branch (`git push origin my-new-feature`)
141 | 5. Create new Pull Request
142 |
--------------------------------------------------------------------------------
/lib/ringleader/process.rb:
--------------------------------------------------------------------------------
1 | module Ringleader
2 |
3 | # Represents an instance of a configured application.
4 | class Process
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 | end
18 |
19 | # Public: query if the app is running
20 | def running?
21 | @running
22 | end
23 |
24 | # Public: start the application.
25 | #
26 | # This method is intended to be used synchronously. If the app is already
27 | # running, it'll return immediately. If the app hasn't been started, or is
28 | # in the process of starting, this method blocks until it starts or fails to
29 | # start correctly.
30 | #
31 | # Returns true if the app started, false if not.
32 | def start
33 | if @running
34 | true
35 | elsif @starting
36 | wait :running
37 | else
38 | if already_running?
39 | warn "#{config.name} already running on port #{config.app_port}"
40 | return true
41 | else
42 | start_app
43 | end
44 | end
45 | end
46 |
47 | # Public: stop the application.
48 | #
49 | # Sends a SIGTERM to the app's process, and expects it to exit like a sane
50 | # and well-behaved application within 7 seconds before sending a SIGKILL.
51 | #
52 | # Uses config.kill_with for the initial signal, which defaults to "TERM".
53 | # If a configured process doesn't respond well to TERM (i.e. leaving
54 | # zombies), use KILL instead.
55 | def stop
56 | return unless @pid
57 |
58 | children = child_pids @pid
59 |
60 | info "stopping #{@pid}"
61 | debug "child pids: #{children.inspect}"
62 |
63 | @master.close unless @master.closed?
64 |
65 | debug "kill -#{config.kill_with} #{@pid}"
66 | ::Process.kill config.kill_with, -@pid
67 |
68 | failsafe = after 7 do
69 | warn "process #{@pid} did not shut down cleanly, killing it"
70 | debug "kill -KILL #{@pid}"
71 | ::Process.kill "KILL", -@pid
72 | reap_orphans children
73 | end
74 |
75 | wait :running # wait for the exit callback
76 | failsafe.cancel
77 | sleep 2 # give the children a chance to shut down
78 | reap_orphans children
79 |
80 | rescue Errno::ESRCH, Errno::EPERM
81 | exited
82 | end
83 |
84 | # Internal: callback for when the application port has opened
85 | def port_opened
86 | info "listening on #{config.host}:#{config.app_port}"
87 | signal :running, true
88 | end
89 |
90 | # Internal: callback for when the process has exited.
91 | def exited
92 | info "pid #{@pid} exited"
93 | @running = false
94 | @pid = nil
95 | @wait_for_port.terminate if @wait_for_port.alive?
96 | @wait_for_exit.terminate if @wait_for_exit.alive?
97 | signal :running, false
98 | end
99 |
100 | # Internal: start the application process and associated infrastructure
101 | #
102 | # Intended to be synchronous, as it blocks until the app has started (or
103 | # failed to start).
104 | #
105 | # Returns true if the app started, false if not.
106 | def start_app
107 | @starting = true
108 | info "starting process `#{config.command}`"
109 |
110 | unless File.directory?(config.dir)
111 | error "#{config.dir} does not exist!"
112 | @starting = false
113 | @running = false
114 | return false
115 | end
116 |
117 | # give the child process a terminal so output isn't buffered
118 | @master, slave = PTY.open
119 | in_clean_environment do
120 | @pid = ::Process.spawn(
121 | config.env,
122 | %Q(bash -c "#{config.command}"),
123 | :in => slave,
124 | :out => slave,
125 | :err => slave,
126 | :chdir => config.dir,
127 | :pgroup => true
128 | )
129 | end
130 | slave.close
131 | proxy_output @master
132 | debug "started with pid #{@pid}"
133 |
134 | @wait_for_exit = WaitForExit.new @pid, Actor.current
135 | @wait_for_port = WaitForPort.new config.host, config.app_port, Actor.current
136 |
137 | timer = after config.startup_timeout do
138 | warn "application startup took more than #{config.startup_timeout}"
139 | async.stop
140 | end
141 |
142 | @running = wait :running
143 |
144 | @starting = false
145 | timer.cancel
146 |
147 | @running
148 | rescue Errno::ENOENT
149 | @starting = false
150 | @running = false
151 | false
152 | ensure
153 | unless @running
154 | warn "could not start `#{config.command}`"
155 | end
156 | end
157 |
158 | # Internal: check if the app is already running outside ringleader
159 | def already_running?
160 | socket = TCPSocket.new config.host, config.app_port
161 | socket.close
162 | true
163 | rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
164 | false
165 | rescue IOError, SystemCallError => e
166 | error "unexpected error when checking status: #{e}"
167 | end
168 |
169 | # Internal: proxy output streams to the logger.
170 | #
171 | # Fire and forget, runs in its own thread.
172 | def proxy_output(input)
173 | Thread.new do
174 | until input.eof?
175 | info input.gets.strip
176 | end
177 | end
178 | end
179 |
180 | # Internal: execute a command in a clean environment (bundler)
181 | def in_clean_environment(&block)
182 | if Object.const_defined?(:Bundler)
183 | ::Bundler.with_clean_env(&block)
184 | else
185 | yield
186 | end
187 | end
188 |
189 | # Internal: kill orphaned processes
190 | def reap_orphans(pids)
191 | pids.each do |pid|
192 | debug "checking for child #{pid}"
193 | next unless Sys::ProcTable.ps(pid)
194 | error "child process #{pid} was orphaned, killing it"
195 | begin
196 | ::Process.kill "KILL", pid
197 | rescue Errno::ESRCH, Errno::EPERM
198 | debug "could not kill #{pid}"
199 | end
200 | end
201 | end
202 |
203 | # Internal: returns all child pids of the given parent
204 | def child_pids(parent_pid)
205 | debug "retrieving child pids of #{parent_pid}"
206 | proc_table = Sys::ProcTable.ps
207 | children_of parent_pid, proc_table
208 | end
209 |
210 | # Internal: find child pids given a parent pid and a proc table
211 | def children_of(parent_pid, proc_table)
212 | [].tap do |pids|
213 | proc_table.each do |proc_record|
214 | if proc_record.ppid == parent_pid
215 | pids << proc_record.pid
216 | pids.concat children_of proc_record.pid, proc_table
217 | end
218 | end
219 | end
220 | end
221 |
222 | end
223 |
224 | end
225 |
--------------------------------------------------------------------------------
/assets/bootstrap/css/bootstrap-responsive.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Responsive v2.0.4
3 | *
4 | * Copyright 2012 Twitter, Inc
5 | * Licensed under the Apache License v2.0
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Designed and built with all the love in the world @twitter by @mdo and @fat.
9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}@media(max-width:767px){.visible-phone{display:inherit!important}.hidden-phone{display:none!important}.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}}@media(min-width:768px) and (max-width:979px){.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:18px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.modal{position:absolute;top:10px;right:10px;left:10px;width:auto;margin:0}.modal.fade.in{top:auto}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:auto;margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:20px}.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:28px;margin-left:2.762430939%;*margin-left:2.709239449638298%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:99.999999993%;*width:99.9468085036383%}.row-fluid .span11{width:91.436464082%;*width:91.38327259263829%}.row-fluid .span10{width:82.87292817100001%;*width:82.8197366816383%}.row-fluid .span9{width:74.30939226%;*width:74.25620077063829%}.row-fluid .span8{width:65.74585634900001%;*width:65.6926648596383%}.row-fluid .span7{width:57.182320438000005%;*width:57.129128948638304%}.row-fluid .span6{width:48.618784527%;*width:48.5655930376383%}.row-fluid .span5{width:40.055248616%;*width:40.0020571266383%}.row-fluid .span4{width:31.491712705%;*width:31.4385212156383%}.row-fluid .span3{width:22.928176794%;*width:22.874985304638297%}.row-fluid .span2{width:14.364640883%;*width:14.311449393638298%}.row-fluid .span1{width:5.801104972%;*width:5.747913482638298%}input,textarea,.uneditable-input{margin-left:0}input.span12,textarea.span12,.uneditable-input.span12{width:714px}input.span11,textarea.span11,.uneditable-input.span11{width:652px}input.span10,textarea.span10,.uneditable-input.span10{width:590px}input.span9,textarea.span9,.uneditable-input.span9{width:528px}input.span8,textarea.span8,.uneditable-input.span8{width:466px}input.span7,textarea.span7,.uneditable-input.span7{width:404px}input.span6,textarea.span6,.uneditable-input.span6{width:342px}input.span5,textarea.span5,.uneditable-input.span5{width:280px}input.span4,textarea.span4,.uneditable-input.span4{width:218px}input.span3,textarea.span3,.uneditable-input.span3{width:156px}input.span2,textarea.span2,.uneditable-input.span2{width:94px}input.span1,textarea.span1,.uneditable-input.span1{width:32px}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:30px}.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:28px;margin-left:2.564102564%;*margin-left:2.510911074638298%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145300001%;*width:91.3997999636383%}.row-fluid .span10{width:82.905982906%;*width:82.8527914166383%}.row-fluid .span9{width:74.358974359%;*width:74.30578286963829%}.row-fluid .span8{width:65.81196581200001%;*width:65.7587743226383%}.row-fluid .span7{width:57.264957265%;*width:57.2117657756383%}.row-fluid .span6{width:48.717948718%;*width:48.6647572286383%}.row-fluid .span5{width:40.170940171000005%;*width:40.117748681638304%}.row-fluid .span4{width:31.623931624%;*width:31.5707401346383%}.row-fluid .span3{width:23.076923077%;*width:23.0237315876383%}.row-fluid .span2{width:14.529914530000001%;*width:14.4767230406383%}.row-fluid .span1{width:5.982905983%;*width:5.929714493638298%}input,textarea,.uneditable-input{margin-left:0}input.span12,textarea.span12,.uneditable-input.span12{width:1160px}input.span11,textarea.span11,.uneditable-input.span11{width:1060px}input.span10,textarea.span10,.uneditable-input.span10{width:960px}input.span9,textarea.span9,.uneditable-input.span9{width:860px}input.span8,textarea.span8,.uneditable-input.span8{width:760px}input.span7,textarea.span7,.uneditable-input.span7{width:660px}input.span6,textarea.span6,.uneditable-input.span6{width:560px}input.span5,textarea.span5,.uneditable-input.span5{width:460px}input.span4,textarea.span4,.uneditable-input.span4{width:360px}input.span3,textarea.span3,.uneditable-input.span3{width:260px}input.span2,textarea.span2,.uneditable-input.span2{width:160px}input.span1,textarea.span1,.uneditable-input.span1{width:60px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:18px}.navbar-fixed-bottom{margin-top:18px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 9px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#999;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#222}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:block;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222;border-bottom:1px solid #222;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}}
10 |
--------------------------------------------------------------------------------
/assets/underscore-min.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.3.3
2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
3 | // Underscore is freely distributable under the MIT license.
4 | // Portions of Underscore are inspired or borrowed from Prototype,
5 | // Oliver Steele's Functional, and John Resig's Micro-Templating.
6 | // For all details and documentation:
7 | // http://documentcloud.github.com/underscore
8 | (function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
9 | c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
10 | g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
11 | c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e2;a==null&&(a=[]);if(A&&
12 | a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
13 | c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
14 | a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
17 | j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
20 | i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
25 | c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
26 | function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
27 | b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
28 | b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
29 | function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
30 | u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
31 | b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
32 | this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
33 |
--------------------------------------------------------------------------------
/assets/backbone-min.js:
--------------------------------------------------------------------------------
1 | // Backbone.js 0.9.2
2 |
3 | // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
4 | // Backbone may be freely distributed under the MIT license.
5 | // For all details and documentation:
6 | // http://backbonejs.org
7 | (function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
8 | {});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
9 | z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
10 | {};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
11 | b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
12 | b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
13 | a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
14 | h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
15 | return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
16 | {};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
17 | !this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
18 | this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
30 | this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
31 | stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
32 | function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
33 | this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
34 | f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
35 | for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b label {
103 | float: none;
104 | width: auto;
105 | padding-top: 0;
106 | text-align: left;
107 | }
108 | .form-horizontal .controls {
109 | margin-left: 0;
110 | }
111 | .form-horizontal .control-list {
112 | padding-top: 0;
113 | }
114 | .form-horizontal .form-actions {
115 | padding-right: 10px;
116 | padding-left: 10px;
117 | }
118 | .modal {
119 | position: absolute;
120 | top: 10px;
121 | right: 10px;
122 | left: 10px;
123 | width: auto;
124 | margin: 0;
125 | }
126 | .modal.fade.in {
127 | top: auto;
128 | }
129 | .modal-header .close {
130 | padding: 10px;
131 | margin: -10px;
132 | }
133 | .carousel-caption {
134 | position: static;
135 | }
136 | }
137 |
138 | @media (max-width: 767px) {
139 | body {
140 | padding-right: 20px;
141 | padding-left: 20px;
142 | }
143 | .navbar-fixed-top,
144 | .navbar-fixed-bottom {
145 | margin-right: -20px;
146 | margin-left: -20px;
147 | }
148 | .container-fluid {
149 | padding: 0;
150 | }
151 | .dl-horizontal dt {
152 | float: none;
153 | width: auto;
154 | clear: none;
155 | text-align: left;
156 | }
157 | .dl-horizontal dd {
158 | margin-left: 0;
159 | }
160 | .container {
161 | width: auto;
162 | }
163 | .row-fluid {
164 | width: 100%;
165 | }
166 | .row,
167 | .thumbnails {
168 | margin-left: 0;
169 | }
170 | [class*="span"],
171 | .row-fluid [class*="span"] {
172 | display: block;
173 | float: none;
174 | width: auto;
175 | margin-left: 0;
176 | }
177 | .input-large,
178 | .input-xlarge,
179 | .input-xxlarge,
180 | input[class*="span"],
181 | select[class*="span"],
182 | textarea[class*="span"],
183 | .uneditable-input {
184 | display: block;
185 | width: 100%;
186 | min-height: 28px;
187 | -webkit-box-sizing: border-box;
188 | -moz-box-sizing: border-box;
189 | -ms-box-sizing: border-box;
190 | box-sizing: border-box;
191 | }
192 | .input-prepend input,
193 | .input-append input,
194 | .input-prepend input[class*="span"],
195 | .input-append input[class*="span"] {
196 | display: inline-block;
197 | width: auto;
198 | }
199 | }
200 |
201 | @media (min-width: 768px) and (max-width: 979px) {
202 | .row {
203 | margin-left: -20px;
204 | *zoom: 1;
205 | }
206 | .row:before,
207 | .row:after {
208 | display: table;
209 | content: "";
210 | }
211 | .row:after {
212 | clear: both;
213 | }
214 | [class*="span"] {
215 | float: left;
216 | margin-left: 20px;
217 | }
218 | .container,
219 | .navbar-fixed-top .container,
220 | .navbar-fixed-bottom .container {
221 | width: 724px;
222 | }
223 | .span12 {
224 | width: 724px;
225 | }
226 | .span11 {
227 | width: 662px;
228 | }
229 | .span10 {
230 | width: 600px;
231 | }
232 | .span9 {
233 | width: 538px;
234 | }
235 | .span8 {
236 | width: 476px;
237 | }
238 | .span7 {
239 | width: 414px;
240 | }
241 | .span6 {
242 | width: 352px;
243 | }
244 | .span5 {
245 | width: 290px;
246 | }
247 | .span4 {
248 | width: 228px;
249 | }
250 | .span3 {
251 | width: 166px;
252 | }
253 | .span2 {
254 | width: 104px;
255 | }
256 | .span1 {
257 | width: 42px;
258 | }
259 | .offset12 {
260 | margin-left: 764px;
261 | }
262 | .offset11 {
263 | margin-left: 702px;
264 | }
265 | .offset10 {
266 | margin-left: 640px;
267 | }
268 | .offset9 {
269 | margin-left: 578px;
270 | }
271 | .offset8 {
272 | margin-left: 516px;
273 | }
274 | .offset7 {
275 | margin-left: 454px;
276 | }
277 | .offset6 {
278 | margin-left: 392px;
279 | }
280 | .offset5 {
281 | margin-left: 330px;
282 | }
283 | .offset4 {
284 | margin-left: 268px;
285 | }
286 | .offset3 {
287 | margin-left: 206px;
288 | }
289 | .offset2 {
290 | margin-left: 144px;
291 | }
292 | .offset1 {
293 | margin-left: 82px;
294 | }
295 | .row-fluid {
296 | width: 100%;
297 | *zoom: 1;
298 | }
299 | .row-fluid:before,
300 | .row-fluid:after {
301 | display: table;
302 | content: "";
303 | }
304 | .row-fluid:after {
305 | clear: both;
306 | }
307 | .row-fluid [class*="span"] {
308 | display: block;
309 | float: left;
310 | width: 100%;
311 | min-height: 28px;
312 | margin-left: 2.762430939%;
313 | *margin-left: 2.709239449638298%;
314 | -webkit-box-sizing: border-box;
315 | -moz-box-sizing: border-box;
316 | -ms-box-sizing: border-box;
317 | box-sizing: border-box;
318 | }
319 | .row-fluid [class*="span"]:first-child {
320 | margin-left: 0;
321 | }
322 | .row-fluid .span12 {
323 | width: 99.999999993%;
324 | *width: 99.9468085036383%;
325 | }
326 | .row-fluid .span11 {
327 | width: 91.436464082%;
328 | *width: 91.38327259263829%;
329 | }
330 | .row-fluid .span10 {
331 | width: 82.87292817100001%;
332 | *width: 82.8197366816383%;
333 | }
334 | .row-fluid .span9 {
335 | width: 74.30939226%;
336 | *width: 74.25620077063829%;
337 | }
338 | .row-fluid .span8 {
339 | width: 65.74585634900001%;
340 | *width: 65.6926648596383%;
341 | }
342 | .row-fluid .span7 {
343 | width: 57.182320438000005%;
344 | *width: 57.129128948638304%;
345 | }
346 | .row-fluid .span6 {
347 | width: 48.618784527%;
348 | *width: 48.5655930376383%;
349 | }
350 | .row-fluid .span5 {
351 | width: 40.055248616%;
352 | *width: 40.0020571266383%;
353 | }
354 | .row-fluid .span4 {
355 | width: 31.491712705%;
356 | *width: 31.4385212156383%;
357 | }
358 | .row-fluid .span3 {
359 | width: 22.928176794%;
360 | *width: 22.874985304638297%;
361 | }
362 | .row-fluid .span2 {
363 | width: 14.364640883%;
364 | *width: 14.311449393638298%;
365 | }
366 | .row-fluid .span1 {
367 | width: 5.801104972%;
368 | *width: 5.747913482638298%;
369 | }
370 | input,
371 | textarea,
372 | .uneditable-input {
373 | margin-left: 0;
374 | }
375 | input.span12,
376 | textarea.span12,
377 | .uneditable-input.span12 {
378 | width: 714px;
379 | }
380 | input.span11,
381 | textarea.span11,
382 | .uneditable-input.span11 {
383 | width: 652px;
384 | }
385 | input.span10,
386 | textarea.span10,
387 | .uneditable-input.span10 {
388 | width: 590px;
389 | }
390 | input.span9,
391 | textarea.span9,
392 | .uneditable-input.span9 {
393 | width: 528px;
394 | }
395 | input.span8,
396 | textarea.span8,
397 | .uneditable-input.span8 {
398 | width: 466px;
399 | }
400 | input.span7,
401 | textarea.span7,
402 | .uneditable-input.span7 {
403 | width: 404px;
404 | }
405 | input.span6,
406 | textarea.span6,
407 | .uneditable-input.span6 {
408 | width: 342px;
409 | }
410 | input.span5,
411 | textarea.span5,
412 | .uneditable-input.span5 {
413 | width: 280px;
414 | }
415 | input.span4,
416 | textarea.span4,
417 | .uneditable-input.span4 {
418 | width: 218px;
419 | }
420 | input.span3,
421 | textarea.span3,
422 | .uneditable-input.span3 {
423 | width: 156px;
424 | }
425 | input.span2,
426 | textarea.span2,
427 | .uneditable-input.span2 {
428 | width: 94px;
429 | }
430 | input.span1,
431 | textarea.span1,
432 | .uneditable-input.span1 {
433 | width: 32px;
434 | }
435 | }
436 |
437 | @media (min-width: 1200px) {
438 | .row {
439 | margin-left: -30px;
440 | *zoom: 1;
441 | }
442 | .row:before,
443 | .row:after {
444 | display: table;
445 | content: "";
446 | }
447 | .row:after {
448 | clear: both;
449 | }
450 | [class*="span"] {
451 | float: left;
452 | margin-left: 30px;
453 | }
454 | .container,
455 | .navbar-fixed-top .container,
456 | .navbar-fixed-bottom .container {
457 | width: 1170px;
458 | }
459 | .span12 {
460 | width: 1170px;
461 | }
462 | .span11 {
463 | width: 1070px;
464 | }
465 | .span10 {
466 | width: 970px;
467 | }
468 | .span9 {
469 | width: 870px;
470 | }
471 | .span8 {
472 | width: 770px;
473 | }
474 | .span7 {
475 | width: 670px;
476 | }
477 | .span6 {
478 | width: 570px;
479 | }
480 | .span5 {
481 | width: 470px;
482 | }
483 | .span4 {
484 | width: 370px;
485 | }
486 | .span3 {
487 | width: 270px;
488 | }
489 | .span2 {
490 | width: 170px;
491 | }
492 | .span1 {
493 | width: 70px;
494 | }
495 | .offset12 {
496 | margin-left: 1230px;
497 | }
498 | .offset11 {
499 | margin-left: 1130px;
500 | }
501 | .offset10 {
502 | margin-left: 1030px;
503 | }
504 | .offset9 {
505 | margin-left: 930px;
506 | }
507 | .offset8 {
508 | margin-left: 830px;
509 | }
510 | .offset7 {
511 | margin-left: 730px;
512 | }
513 | .offset6 {
514 | margin-left: 630px;
515 | }
516 | .offset5 {
517 | margin-left: 530px;
518 | }
519 | .offset4 {
520 | margin-left: 430px;
521 | }
522 | .offset3 {
523 | margin-left: 330px;
524 | }
525 | .offset2 {
526 | margin-left: 230px;
527 | }
528 | .offset1 {
529 | margin-left: 130px;
530 | }
531 | .row-fluid {
532 | width: 100%;
533 | *zoom: 1;
534 | }
535 | .row-fluid:before,
536 | .row-fluid:after {
537 | display: table;
538 | content: "";
539 | }
540 | .row-fluid:after {
541 | clear: both;
542 | }
543 | .row-fluid [class*="span"] {
544 | display: block;
545 | float: left;
546 | width: 100%;
547 | min-height: 28px;
548 | margin-left: 2.564102564%;
549 | *margin-left: 2.510911074638298%;
550 | -webkit-box-sizing: border-box;
551 | -moz-box-sizing: border-box;
552 | -ms-box-sizing: border-box;
553 | box-sizing: border-box;
554 | }
555 | .row-fluid [class*="span"]:first-child {
556 | margin-left: 0;
557 | }
558 | .row-fluid .span12 {
559 | width: 100%;
560 | *width: 99.94680851063829%;
561 | }
562 | .row-fluid .span11 {
563 | width: 91.45299145300001%;
564 | *width: 91.3997999636383%;
565 | }
566 | .row-fluid .span10 {
567 | width: 82.905982906%;
568 | *width: 82.8527914166383%;
569 | }
570 | .row-fluid .span9 {
571 | width: 74.358974359%;
572 | *width: 74.30578286963829%;
573 | }
574 | .row-fluid .span8 {
575 | width: 65.81196581200001%;
576 | *width: 65.7587743226383%;
577 | }
578 | .row-fluid .span7 {
579 | width: 57.264957265%;
580 | *width: 57.2117657756383%;
581 | }
582 | .row-fluid .span6 {
583 | width: 48.717948718%;
584 | *width: 48.6647572286383%;
585 | }
586 | .row-fluid .span5 {
587 | width: 40.170940171000005%;
588 | *width: 40.117748681638304%;
589 | }
590 | .row-fluid .span4 {
591 | width: 31.623931624%;
592 | *width: 31.5707401346383%;
593 | }
594 | .row-fluid .span3 {
595 | width: 23.076923077%;
596 | *width: 23.0237315876383%;
597 | }
598 | .row-fluid .span2 {
599 | width: 14.529914530000001%;
600 | *width: 14.4767230406383%;
601 | }
602 | .row-fluid .span1 {
603 | width: 5.982905983%;
604 | *width: 5.929714493638298%;
605 | }
606 | input,
607 | textarea,
608 | .uneditable-input {
609 | margin-left: 0;
610 | }
611 | input.span12,
612 | textarea.span12,
613 | .uneditable-input.span12 {
614 | width: 1160px;
615 | }
616 | input.span11,
617 | textarea.span11,
618 | .uneditable-input.span11 {
619 | width: 1060px;
620 | }
621 | input.span10,
622 | textarea.span10,
623 | .uneditable-input.span10 {
624 | width: 960px;
625 | }
626 | input.span9,
627 | textarea.span9,
628 | .uneditable-input.span9 {
629 | width: 860px;
630 | }
631 | input.span8,
632 | textarea.span8,
633 | .uneditable-input.span8 {
634 | width: 760px;
635 | }
636 | input.span7,
637 | textarea.span7,
638 | .uneditable-input.span7 {
639 | width: 660px;
640 | }
641 | input.span6,
642 | textarea.span6,
643 | .uneditable-input.span6 {
644 | width: 560px;
645 | }
646 | input.span5,
647 | textarea.span5,
648 | .uneditable-input.span5 {
649 | width: 460px;
650 | }
651 | input.span4,
652 | textarea.span4,
653 | .uneditable-input.span4 {
654 | width: 360px;
655 | }
656 | input.span3,
657 | textarea.span3,
658 | .uneditable-input.span3 {
659 | width: 260px;
660 | }
661 | input.span2,
662 | textarea.span2,
663 | .uneditable-input.span2 {
664 | width: 160px;
665 | }
666 | input.span1,
667 | textarea.span1,
668 | .uneditable-input.span1 {
669 | width: 60px;
670 | }
671 | .thumbnails {
672 | margin-left: -30px;
673 | }
674 | .thumbnails > li {
675 | margin-left: 30px;
676 | }
677 | .row-fluid .thumbnails {
678 | margin-left: 0;
679 | }
680 | }
681 |
682 | @media (max-width: 979px) {
683 | body {
684 | padding-top: 0;
685 | }
686 | .navbar-fixed-top,
687 | .navbar-fixed-bottom {
688 | position: static;
689 | }
690 | .navbar-fixed-top {
691 | margin-bottom: 18px;
692 | }
693 | .navbar-fixed-bottom {
694 | margin-top: 18px;
695 | }
696 | .navbar-fixed-top .navbar-inner,
697 | .navbar-fixed-bottom .navbar-inner {
698 | padding: 5px;
699 | }
700 | .navbar .container {
701 | width: auto;
702 | padding: 0;
703 | }
704 | .navbar .brand {
705 | padding-right: 10px;
706 | padding-left: 10px;
707 | margin: 0 0 0 -5px;
708 | }
709 | .nav-collapse {
710 | clear: both;
711 | }
712 | .nav-collapse .nav {
713 | float: none;
714 | margin: 0 0 9px;
715 | }
716 | .nav-collapse .nav > li {
717 | float: none;
718 | }
719 | .nav-collapse .nav > li > a {
720 | margin-bottom: 2px;
721 | }
722 | .nav-collapse .nav > .divider-vertical {
723 | display: none;
724 | }
725 | .nav-collapse .nav .nav-header {
726 | color: #999999;
727 | text-shadow: none;
728 | }
729 | .nav-collapse .nav > li > a,
730 | .nav-collapse .dropdown-menu a {
731 | padding: 6px 15px;
732 | font-weight: bold;
733 | color: #999999;
734 | -webkit-border-radius: 3px;
735 | -moz-border-radius: 3px;
736 | border-radius: 3px;
737 | }
738 | .nav-collapse .btn {
739 | padding: 4px 10px 4px;
740 | font-weight: normal;
741 | -webkit-border-radius: 4px;
742 | -moz-border-radius: 4px;
743 | border-radius: 4px;
744 | }
745 | .nav-collapse .dropdown-menu li + li a {
746 | margin-bottom: 2px;
747 | }
748 | .nav-collapse .nav > li > a:hover,
749 | .nav-collapse .dropdown-menu a:hover {
750 | background-color: #222222;
751 | }
752 | .nav-collapse.in .btn-group {
753 | padding: 0;
754 | margin-top: 5px;
755 | }
756 | .nav-collapse .dropdown-menu {
757 | position: static;
758 | top: auto;
759 | left: auto;
760 | display: block;
761 | float: none;
762 | max-width: none;
763 | padding: 0;
764 | margin: 0 15px;
765 | background-color: transparent;
766 | border: none;
767 | -webkit-border-radius: 0;
768 | -moz-border-radius: 0;
769 | border-radius: 0;
770 | -webkit-box-shadow: none;
771 | -moz-box-shadow: none;
772 | box-shadow: none;
773 | }
774 | .nav-collapse .dropdown-menu:before,
775 | .nav-collapse .dropdown-menu:after {
776 | display: none;
777 | }
778 | .nav-collapse .dropdown-menu .divider {
779 | display: none;
780 | }
781 | .nav-collapse .navbar-form,
782 | .nav-collapse .navbar-search {
783 | float: none;
784 | padding: 9px 15px;
785 | margin: 9px 0;
786 | border-top: 1px solid #222222;
787 | border-bottom: 1px solid #222222;
788 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
789 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
790 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
791 | }
792 | .navbar .nav-collapse .nav.pull-right {
793 | float: none;
794 | margin-left: 0;
795 | }
796 | .nav-collapse,
797 | .nav-collapse.collapse {
798 | height: 0;
799 | overflow: hidden;
800 | }
801 | .navbar .btn-navbar {
802 | display: block;
803 | }
804 | .navbar-static .navbar-inner {
805 | padding-right: 10px;
806 | padding-left: 10px;
807 | }
808 | }
809 |
810 | @media (min-width: 980px) {
811 | .nav-collapse.collapse {
812 | height: auto !important;
813 | overflow: visible !important;
814 | }
815 | }
816 |
--------------------------------------------------------------------------------
/assets/jquery.mustache.js:
--------------------------------------------------------------------------------
1 | /*
2 | Shameless port of a shameless port
3 | @defunkt => @janl => @aq
4 |
5 | See http://github.com/defunkt/mustache for more info.
6 | */
7 |
8 | ;(function($) {
9 |
10 | /*!
11 | * mustache.js - Logic-less {{mustache}} templates with JavaScript
12 | * http://github.com/janl/mustache.js
13 | */
14 |
15 | var Mustache;
16 |
17 | (function (exports) {
18 | if (typeof module !== "undefined") {
19 | module.exports = exports; // CommonJS
20 | } else if (typeof define === "function") {
21 | define(exports); // AMD
22 | } else {
23 | Mustache = exports; //