diff --git a/.gitignore b/.gitignore index 65cbda8..fe7f5d5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ *.gem coverage/ log/ -tmp/ .ruby-version history.yml diff --git a/.travis.yml b/.travis.yml index f68ed76..efb2a95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: ruby rvm: - - 2.3.0 - - 2.2.4 - - 2.1.8 - -script: "rake test" + - 2.3.4 + - 2.2.7 + - 2.1.10 + - jruby-19mode + - jruby-9.1.10.0 +script: rake test diff --git a/README.md b/README.md index 6ff4065..1aa914b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ $ cd my-project # Install the bundle of project specific gems $ bundle # Start the example dashboard! -$ smashing start +$ puma ``` [Check out our wiki](https://github.com/Smashing/smashing/wiki). diff --git a/Rakefile b/Rakefile index fc91969..da4603e 100644 --- a/Rakefile +++ b/Rakefile @@ -6,6 +6,7 @@ require 'rake/testtask' Rake::TestTask.new(:test) do |test| test.libs << 'lib' << 'test' test.pattern = 'test/**/*_test.rb' + test.warning = false end -task :default => [:test] +task default: [:test] diff --git a/lib/dashing/app.rb b/lib/dashing/app.rb index 5ee085f..d40cc0a 100644 --- a/lib/dashing/app.rb +++ b/lib/dashing/app.rb @@ -1,12 +1,12 @@ -require 'sinatra' -require 'sprockets' -require 'sinatra/content_for' -require 'rufus/scheduler' require 'coffee-script' -require 'sass' require 'json' +require 'rufus/scheduler' +require 'sass' +require 'sinatra' +require 'sinatra/content_for' +require 'sinatra/streaming' +require 'sprockets' require 'yaml' -require 'thin' SCHEDULER = Rufus::Scheduler.new @@ -34,13 +34,14 @@ def authenticated?(token) set :sprockets, Sprockets::Environment.new(settings.root) set :assets_prefix, '/assets' set :digest_assets, false -set :server, 'thin' +set :server, 'puma' set :connections, [] set :history_file, 'history.yml' set :public_folder, File.join(settings.root, 'public') set :views, File.join(settings.root, 'dashboards') set :default_dashboard, nil set :auth_token, nil +set :template_languages, %i[html erb] if File.exists?(settings.history_file) set :history, YAML.load_file(settings.history_file) @@ -57,7 +58,7 @@ def authenticated?(token) end not_found do - send_file File.join(settings.public_folder, '404.html'), :status => 404 + send_file File.join(settings.public_folder, '404.html'), status: 404 end at_exit do @@ -72,21 +73,31 @@ def authenticated?(token) redirect "/" + dashboard end -get '/events', :provides => 'text/event-stream' do + +get '/events', provides: 'text/event-stream' do protected! response.headers['X-Accel-Buffering'] = 'no' # Disable buffering for nginx - stream :keep_open do |out| - settings.connections << out + stream do |out| out << latest_events - out.callback { settings.connections.delete(out) } + settings.connections << connection = {out: out, mutex: Mutex.new, terminated: false} + terminated = false + + loop do + connection[:mutex].synchronize do + terminated = true if connection[:terminated] + end + break if terminated + end + + settings.connections.delete(connection) end end get '/:dashboard' do protected! - tilt_html_engines.each do |suffix, _| - file = File.join(settings.views, "#{params[:dashboard]}.#{suffix}") - return render(suffix.to_sym, params[:dashboard].to_sym) if File.exist? file + settings.template_languages.each do |language| + file = File.join(settings.views, "#{params[:dashboard]}.#{language}") + return render(language, params[:dashboard].to_sym) if File.exist?(file) end halt 404 @@ -119,21 +130,12 @@ def authenticated?(token) get '/views/:widget?.html' do protected! - tilt_html_engines.each do |suffix, engines| - file = File.join(settings.root, "widgets", params[:widget], "#{params[:widget]}.#{suffix}") - return engines.first.new(file).render if File.exist? file - end - "Drats! Unable to find a widget file named: #{params[:widget]} to render." -end - -Thin::Server.class_eval do - def stop_with_connection_closing - Sinatra::Application.settings.connections.dup.each(&:close) - stop_without_connection_closing + settings.template_languages.each do |language| + file = File.join(settings.root, "widgets", params[:widget], "#{params[:widget]}.#{language}") + return Tilt[language].new(file).render if File.exist?(file) end - alias_method :stop_without_connection_closing, :stop - alias_method :stop, :stop_with_connection_closing + "Drats! Unable to find a widget file named: #{params[:widget]} to render." end def send_event(id, body, target=nil) @@ -141,7 +143,18 @@ def send_event(id, body, target=nil) body[:updatedAt] ||= Time.now.to_i event = format_event(body.to_json, target) Sinatra::Application.settings.history[id] = event unless target == 'dashboards' - Sinatra::Application.settings.connections.each { |out| out << event } + Sinatra::Application.settings.connections.each do |connection| + connection[:mutex].synchronize do + begin + connection[:out] << event unless connection[:out].closed? + rescue Puma::ConnectionError + connection[:terminated] = true + rescue Exception => e + connection[:terminated] = true + puts e + end + end + end end def format_event(body, name=nil) @@ -162,13 +175,6 @@ def first_dashboard files.sort.first end -def tilt_html_engines - Tilt.mappings.select do |_, engines| - default_mime_type = engines.first.default_mime_type - default_mime_type.nil? || default_mime_type == 'text/html' - end -end - def require_glob(relative_glob) Dir[File.join(settings.root, relative_glob)].each do |file| require file diff --git a/lib/dashing/cli.rb b/lib/dashing/cli.rb index 4b93f89..b141187 100644 --- a/lib/dashing/cli.rb +++ b/lib/dashing/cli.rb @@ -54,22 +54,6 @@ def install(gist_id, *args) say set_color("Could not find gist at #{public_url}"), :red end - desc "start", "Starts the server in style!" - method_option :job_path, :desc => "Specify the directory where jobs are stored" - def start(*args) - port_option = args.include?('-p') ? '' : ' -p 3030' - args = args.join(' ') - command = "bundle exec thin -R config.ru start#{port_option} #{args}" - command.prepend "export JOB_PATH=#{options[:job_path]}; " if options[:job_path] - run_command(command) - end - - desc "stop", "Stops the thin server" - def stop - command = "bundle exec thin stop" - run_command(command) - end - desc "job JOB_NAME AUTH_TOKEN(optional)", "Runs the specified job. Make sure to supply your auth token if you have one set." def job(name, auth_token = "") Dir[File.join(Dir.pwd, 'lib/**/*.rb')].each {|file| require_file(file) } @@ -94,10 +78,10 @@ def install_widget_from_gist(gist, skip_overwrite) if file =~ /\.(html|coffee|scss)\z/ widget_name = File.basename(file, '.*') new_path = File.join(Dir.pwd, 'widgets', widget_name, file) - create_file(new_path, details['content'], :skip => skip_overwrite) + create_file(new_path, details['content'], skip: skip_overwrite) elsif file.end_with?('.rb') new_path = File.join(Dir.pwd, 'jobs', file) - create_file(new_path, details['content'], :skip => skip_overwrite) + create_file(new_path, details['content'], skip: skip_overwrite) end end end diff --git a/smashing.gemspec b/smashing.gemspec index c8f1cb2..5043886 100644 --- a/smashing.gemspec +++ b/smashing.gemspec @@ -15,21 +15,21 @@ Gem::Specification.new do |s| s.files = Dir['README.md', 'javascripts/**/*', 'templates/**/*','templates/**/.[a-z]*', 'lib/**/*'] - s.add_dependency('sass', '~> 3.2.12') - s.add_dependency('coffee-script', '~> 2.2.0') - s.add_dependency('execjs', '~> 2.0.2') - s.add_dependency('sinatra', '~> 1.4.4') - s.add_dependency('sinatra-contrib', '~> 1.4.2') - s.add_dependency('thin', '~> 1.6.1') - s.add_dependency('rufus-scheduler', '~> 2.0.24') - s.add_dependency('thor', '~> 0.19') - s.add_dependency('sprockets', '~> 2.10.1') - s.add_dependency('rack', '~> 1.5.4') + s.add_dependency('sass', '~> 3.4.24') + s.add_dependency('coffee-script', '~> 2.4.1') + s.add_dependency('execjs', '~> 2.7.0') + s.add_dependency('sinatra', '~> 2.0.0') + s.add_dependency('sinatra-contrib', '~> 2.0.0') + s.add_dependency('puma', '~> 3.8.2') + s.add_dependency('rufus-scheduler', '~> 3.4.2') + s.add_dependency('thor', '~> 0.19.4') + s.add_dependency('sprockets', '~> 3.7.1') - s.add_development_dependency('rake', '~> 10.1.0') - s.add_development_dependency('haml', '~> 4.0.4') - s.add_development_dependency('minitest', '~> 5.2.0') - s.add_development_dependency('mocha', '~> 0.14.0') + s.add_development_dependency('rake', '~> 12.0.0') + s.add_development_dependency('haml', '~> 5.0.1') + s.add_development_dependency('rack-test', '~> 0.6.3') + s.add_development_dependency('minitest', '~> 5.10.2') + s.add_development_dependency('mocha', '~> 1.2.1') s.add_development_dependency('fakeweb', '~> 1.3.0') - s.add_development_dependency('simplecov', '~> 0.8.2') + s.add_development_dependency('simplecov', '~> 0.14.1') end diff --git a/templates/job/%name%.rb b/templates/job/%name%.rb index 76f221a..c5c00c7 100644 --- a/templates/job/%name%.rb +++ b/templates/job/%name%.rb @@ -1,4 +1,4 @@ # :first_in sets how long it takes before the job is first run. In this case, it is run immediately -SCHEDULER.every '1m', :first_in => 0 do |job| +SCHEDULER.every '1m', first_in: 0 do |job| send_event('widget_id', { }) -end \ No newline at end of file +end diff --git a/templates/project/config.ru b/templates/project/config.ru index 624244e..eecd5e0 100644 --- a/templates/project/config.ru +++ b/templates/project/config.ru @@ -3,6 +3,10 @@ require 'dashing' configure do set :auth_token, 'YOUR_AUTH_TOKEN' + # See http://www.sinatrarb.com/intro.html > Available Template Languages on + # how to add additional template languages. + set :template_languages, %i[html erb] + helpers do def protected! # Put any authentication code you want in here. diff --git a/templates/project/config/puma.rb b/templates/project/config/puma.rb new file mode 100644 index 0000000..26140fa --- /dev/null +++ b/templates/project/config/puma.rb @@ -0,0 +1,42 @@ +# For a complete list of puma configuration parameters, please see +# https://github.com/puma/puma + +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum. +# +threads_count = ENV.fetch("PUMA_MAX_THREADS") { 5 }.to_i +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests, default is 2020. +# +port ENV.fetch("DASHING_PORT") { 3030 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RACK_ENV") { "production" } + +# Daemonize the server into the background. Highly suggest that +# this be combined with "pidfile" and "stdout_redirect". +# +# The default is "false". +# +daemonize ENV.fetch("DAEMONIZE") { false } + +# Store the pid of the server in the file at "path". +# +pidfile './tmp/pids/puma.pid' + +# Use "path" as the file to store the server info state. This is +# used by "pumactl" to query and control the server. +# +state_path './tmp/pids/puma.state' + +# Redirect STDOUT and STDERR to files specified. The 3rd parameter +# ("append") specifies whether the output is appended, the default is +# "false". +# +# stdout_redirect '/u/apps/lolcat/log/stdout', '/u/apps/lolcat/log/stderr' +# stdout_redirect '/u/apps/lolcat/log/stdout', '/u/apps/lolcat/log/stderr', true \ No newline at end of file diff --git a/templates/project/jobs/twitter.rb b/templates/project/jobs/twitter.rb index 7b2bd6f..0cac98e 100644 --- a/templates/project/jobs/twitter.rb +++ b/templates/project/jobs/twitter.rb @@ -12,7 +12,7 @@ search_term = URI::encode('#todayilearned') -SCHEDULER.every '10m', :first_in => 0 do |job| +SCHEDULER.every '10m', first_in: 0 do |job| begin tweets = twitter.search("#{search_term}") @@ -25,4 +25,4 @@ rescue Twitter::Error puts "\e[33mFor the twitter widget to work, you need to put in your twitter API keys in the jobs/twitter.rb file.\e[0m" end -end \ No newline at end of file +end diff --git a/templates/project/tmp/pids/.empty_directory b/templates/project/tmp/pids/.empty_directory new file mode 100644 index 0000000..3da7f49 --- /dev/null +++ b/templates/project/tmp/pids/.empty_directory @@ -0,0 +1 @@ +.empty_directory \ No newline at end of file diff --git a/test/app_test.rb b/test/app_test.rb index 0eb6f3a..b23b98b 100644 --- a/test/app_test.rb +++ b/test/app_test.rb @@ -1,10 +1,16 @@ require 'test_helper' require 'haml' +class StreamStub < Array + def closed? + return false + end +end + class AppTest < Dashing::Test def setup - @connection = [] - app.settings.connections = [@connection] + @connection = {out: StreamStub.new, mutex: Mutex.new, terminated: false} + app.settings.connections = [ @connection ] app.settings.auth_token = nil app.settings.default_dashboard = nil app.settings.history_file = File.join(Dir.tmpdir, 'history.yml') @@ -48,9 +54,8 @@ def test_errors_out_when_no_dashboards_available def test_post_widgets_without_auth_token post '/widgets/some_widget', JSON.generate({value: 6}) assert_equal 204, last_response.status - - assert_equal 1, @connection.length - data = parse_data @connection[0] + assert_equal 1, @connection[:out].length + data = parse_data @connection[:out][0] assert_equal 6, data['value'] assert_equal 'some_widget', data['id'] assert data['updatedAt'] @@ -72,19 +77,15 @@ def test_get_events post '/widgets/some_widget', JSON.generate({value: 8}) assert_equal 204, last_response.status - get '/events' - assert_equal 200, last_response.status - assert_equal 8, parse_data(@connection[0])['value'] + assert_equal 8, parse_data(@connection[:out][0])['value'] end def test_dashboard_events post '/dashboards/my_super_sweet_dashboard', JSON.generate({event: 'reload'}) assert_equal 204, last_response.status - get '/events' - assert_equal 200, last_response.status - assert_equal 'dashboards', parse_event(@connection[0]) - assert_equal 'reload', parse_data(@connection[0])['event'] + assert_equal 'dashboards', parse_event(@connection[:out][0]) + assert_equal 'reload', parse_data(@connection[:out][0])['event'] end def test_get_dashboard @@ -104,6 +105,8 @@ def test_page_title_set_correctly end def test_get_haml_dashboard + app.template_languages << :haml + with_generated_project do |dir| File.write(File.join(dir, 'dashboards/hamltest.haml'), '.gridster') get '/hamltest' @@ -113,6 +116,8 @@ def test_get_haml_dashboard end def test_get_haml_widget + app.template_languages << :haml + with_generated_project do |dir| File.write(File.join(dir, 'widgets/clock/clock.haml'), '%h1 haml') File.unlink(File.join(dir, 'widgets/clock/clock.html')) @@ -156,6 +161,11 @@ def with_generated_project app.settings.public_folder = File.join(dir, 'new_project/public') app.settings.views = File.join(dir, 'new_project/dashboards') app.settings.root = File.join(dir, 'new_project') + + app.settings.raise_errors = true + app.settings.dump_errors = false + app.settings.show_exceptions = false + yield app.settings.root end end diff --git a/test/cli_test.rb b/test/cli_test.rb index 4d82296..85191ce 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -101,34 +101,6 @@ def test_install_task_warns_when_gist_not_found assert_includes output, 'Could not find gist at ' end - def test_start_task_starts_thin_with_default_port - command = 'bundle exec thin -R config.ru start -p 3030 ' - @cli.stubs(:run_command).with(command).once - @cli.start - end - - def test_start_task_starts_thin_with_specified_port - command = 'bundle exec thin -R config.ru start -p 2020' - @cli.stubs(:run_command).with(command).once - @cli.start('-p', '2020') - end - - def test_start_task_supports_job_path_option - commands = [ - 'export JOB_PATH=other_spot; ', - 'bundle exec thin -R config.ru start -p 3030 ' - ] - - @cli.stubs(:options).returns(job_path: 'other_spot') - @cli.stubs(:run_command).with(commands.join('')).once - @cli.start - end - - def test_stop_task_stops_thin_server - @cli.stubs(:run_command).with('bundle exec thin stop') - @cli.stop - end - def test_job_task_requires_job_file Dir.stubs(:pwd).returns('') @cli.stubs(:require_file).with('/jobs/special_job.rb').once