From 568929e25a8556f6ac8d500159d5c1e0889a0617 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 16 Aug 2012 22:22:31 -0500 Subject: [PATCH 01/98] * initial commit of websocket gateway. login working, still working on login to jam session --- .gitignore | 22 ++ .rvmrc | 2 + Gemfile | 29 +++ Guardfile | 2 + README.md | 10 + Rakefile | 2 + bin/jam_websockets | 23 ++ bin/jam_websockets.sh | 4 + config/application.yml | 11 + config/database.yml | 8 + features/login.feature | 8 + jam_websockets.gemspec | 17 ++ lib/jam_websockets.rb | 9 + lib/jam_websockets/client_context.rb | 13 + lib/jam_websockets/router.rb | 350 +++++++++++++++++++++++++++ lib/jam_websockets/server.rb | 27 +++ lib/jam_websockets/version.rb | 3 + spec/factories.rb | 20 ++ spec/jam_websockets/router_spec.rb | 186 ++++++++++++++ spec/spec_db.rb | 11 + spec/spec_helper.rb | 96 ++++++++ 21 files changed, 853 insertions(+) create mode 100644 .gitignore create mode 100644 .rvmrc create mode 100644 Gemfile create mode 100644 Guardfile create mode 100644 README.md create mode 100644 Rakefile create mode 100755 bin/jam_websockets create mode 100755 bin/jam_websockets.sh create mode 100644 config/application.yml create mode 100644 config/database.yml create mode 100644 features/login.feature create mode 100644 jam_websockets.gemspec create mode 100644 lib/jam_websockets.rb create mode 100644 lib/jam_websockets/client_context.rb create mode 100644 lib/jam_websockets/router.rb create mode 100644 lib/jam_websockets/server.rb create mode 100644 lib/jam_websockets/version.rb create mode 100644 spec/factories.rb create mode 100644 spec/jam_websockets/router_spec.rb create mode 100644 spec/spec_db.rb create mode 100644 spec/spec_helper.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a35fe92d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp + +.idea +*~ +*.swp +*.iml diff --git a/.rvmrc b/.rvmrc new file mode 100644 index 000000000..ad94dd625 --- /dev/null +++ b/.rvmrc @@ -0,0 +1,2 @@ +rvm use jruby-1.7.0.preview1@websockets --create +#rvm use ruby-1.9.3@websockets --create diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..598a7113f --- /dev/null +++ b/Gemfile @@ -0,0 +1,29 @@ +source 'https://rubygems.org' + +# Look for $WORKSPACE, otherwise use "workspace" as dev path. +workspace = ENV["WORKSPACE"] || "~/workspace" + +gem 'uuidtools', '2.1.2' +gem 'bcrypt-ruby', '3.0.1' +gem 'jruby-openssl' +gem 'ruby-protocol-buffers', '1.2.2' +gem 'pg_migrate', '0.1.4' +gem 'jam_db', :path => '~/workspace/jam-db/target/ruby_package' +gem 'jam_ruby', :path => '~/workspace/jam-ruby' +gem 'jampb', :path => '~/workspace/jam-pb/target/ruby/jampb' +gem 'em-websocket', :path=> '~/workspace/em-websocket' +gem 'hot_bunnies', '1.3.8' +gem 'activerecord', '3.2.7' +gem 'logging' +#gem 'em-http-request' + +group :test do + gem 'cucumber' + gem 'rspec' + gem 'factory_girl' + gem 'spork', '0.9.0' + gem 'database_cleaner', '0.7.0' + gem 'guard', '>= 0.10.0' + gem 'guard-rspec', '>= 0.7.3' +gem 'guard-jruby-rspec' +end diff --git a/Guardfile b/Guardfile new file mode 100644 index 000000000..e84dab66b --- /dev/null +++ b/Guardfile @@ -0,0 +1,2 @@ +interactor :simple +guard 'jruby-rspec', :spec_paths => ["spec"] diff --git a/README.md b/README.md new file mode 100644 index 000000000..dd14c7de9 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +TODO & DESIGN LIMITATIONS +========================= + +* !!!! lock up multi-threaded unsafe data structures + +* The rabbitmq connection isn't pooled. Throughput limitation (but could be resolved by just starting more instances of JamWebsocket behind Haproxy) +* The database connection isn't pooled. Throughput limitation (but could be resolved by just starting more instances of JamWebsocket behind Haproxy) +* The user model is stored in memory, meaning periodically it should be reloaded from the database (in case a user was marked inactive and you want them knocked out of the system) +* The user could easily join to multiple sessions. Currently, though, the ClientContext object only tracks one jam session topic subscription. This is minimial to change. +* peek logic not implemented on server for protoc messages; this could be done to save cost of deserialization and serialization for session/user directed messages \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..f57ae68a8 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +#!/usr/bin/env rake +require "bundler/gem_tasks" diff --git a/bin/jam_websockets b/bin/jam_websockets new file mode 100755 index 000000000..41db8e466 --- /dev/null +++ b/bin/jam_websockets @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby + +require 'jam_websockets' + +include JamWebsockets + +# run some method + +jamenv = ENV['JAMENV'] +jamenv ||= 'development' + +config = YAML::load(File.open('config/application.yml'))[jamenv] + + +if config["verbose"] + Logging.logger.root.level = :debug +else + Logging.logger.root.level = :info +end + +Logging.logger.root.appenders = Logging.appenders.stdout + +Server.new.run :port => config["port"], :debug => false#=> config["verbose"] diff --git a/bin/jam_websockets.sh b/bin/jam_websockets.sh new file mode 100755 index 000000000..f23e59142 --- /dev/null +++ b/bin/jam_websockets.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# this script is used only in development + +bundle exec ruby -Ilib bin/jam_websockets $* diff --git a/config/application.yml b/config/application.yml new file mode 100644 index 000000000..cc37caeb7 --- /dev/null +++ b/config/application.yml @@ -0,0 +1,11 @@ +development: + port: 6767 + verbose: true + +test: + port: 6769 + verbose: true + +production: + port: 80 + verbose: false diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 000000000..303d4b907 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,8 @@ +test: + adapter: postgresql + database: jam_websockets_test + pool: 3 + username: postgres + password: postgres + timeout: 2000 + encoding: unicode \ No newline at end of file diff --git a/features/login.feature b/features/login.feature new file mode 100644 index 000000000..31f025966 --- /dev/null +++ b/features/login.feature @@ -0,0 +1,8 @@ +Feature: Login + In order to protect the internal cloud from malicious actors, login is required to remain connected and send commands as a particular user. + + Scenario: Login with bad credentials and be bounced + Given I supply bad credentials in Login command + When I send the command + Then the result should be an error seen and a closed connection + diff --git a/jam_websockets.gemspec b/jam_websockets.gemspec new file mode 100644 index 000000000..79146ce0b --- /dev/null +++ b/jam_websockets.gemspec @@ -0,0 +1,17 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../lib/jam_websockets/version', __FILE__) + +Gem::Specification.new do |gem| + gem.authors = ["Seth Call"] + gem.email = ["sethcall@gmail.com"] + gem.description = %q{websocket server for clients} + gem.summary = %q{websocket server for clients} + gem.homepage = "" + + gem.files = `git ls-files`.split($\) + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.name = "jam_websockets" + gem.require_paths = ["lib"] + gem.version = JamWebsockets::VERSION +end diff --git a/lib/jam_websockets.rb b/lib/jam_websockets.rb new file mode 100644 index 000000000..c9342516d --- /dev/null +++ b/lib/jam_websockets.rb @@ -0,0 +1,9 @@ +require "logging" +require "jam_websockets/version" +require "jam_websockets/client_context" +require "jam_websockets/router" +require "jam_websockets/server" + +module JamWebsockets + # Your code goes here... +end diff --git a/lib/jam_websockets/client_context.rb b/lib/jam_websockets/client_context.rb new file mode 100644 index 000000000..43eac7c2c --- /dev/null +++ b/lib/jam_websockets/client_context.rb @@ -0,0 +1,13 @@ +module JamWebsockets + class ClientContext + + attr_accessor :user_id, :user_queue, :session_topic, :subscription + + def initialize(user_id, user_queue, subscription) + @user_id = user_id + @user_queue = user_queue + @subscription = subscription + @session_topic = nil + end + end +end diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb new file mode 100644 index 000000000..574e0241e --- /dev/null +++ b/lib/jam_websockets/router.rb @@ -0,0 +1,350 @@ +require 'set' +require 'hot_bunnies' + +include Jampb + +module JamWebsockets + class Router + + def initialize(options={}) + @log = Logging.logger[self] + @pending_clients = Set.new # clients that have connected to server, but not logged in. + @clients = {} # clients that have logged in + @sessions = nil + @connection = nil + @channel = nil + @endpoints = nil + @message_factory = MessageFactory.new + end + + def start(options = {}) + + @log.debug "startup" + + begin + @connection = HotBunnies.connect(:host => options[:host], :port => options[:port]) + @channel = @connection.create_channel + @channel.prefetch = 10 + + @endpoints = @channel.exchange('client_endpoints', :type => :direct) + @sessions = @channel.exchange('client_sessions', :type => :topic) + rescue => e + cleanup + raise e + end + + end + + def cleanup() + + @clients.each do |key, context| + begin + # todo delegate to client cleanup method + if context.subscription.active? + context.subscription.shutdown! + end + rescue => e + @log.debug "unable to cancel subscription on cleanup: #{e}" + end + + #context.user_queue.unbind(@endpoints) + end + + if !@channel.nil? + @channel.close + end + + if !@connection.nil? + @connection.close + end + end + + def stop + @log.debug "shutdown" + cleanup + end + + def new_client(client) + + @pending_clients.add(client) + + client.onopen { + @log.debug "client connected #{client}" + } + + client.onclose { + @log.debug "Connection closed" + + cleanup_client(client) + } + + client.onerror { |error| + if error.kind_of?(EM::WebSocket::WebSocketError) + @log.error "websockets error: #{error}" + else + @log.error "generic error #{error}" + end + + cleanup_client(client) + } + + client.onmessage { |msg| + @log.debug("msg received") + + # TODO: set a max message size before we put it through PB? + # TODO: rate limit? + + pb_msg = Jampb::ClientMessage.parse(msg.to_s) + begin + self.route(pb_msg, client) + rescue => e + @log.debug "ending client session due to error: #{e.to_s}" + + begin + # wrap the message up and send it down + client.send(@message_factory.server_generic_error(e.to_s).to_s) + ensure + cleanup_client(client) + end + + end + + } + + end + + def cleanup_client(client) + @log.debug "cleaning up client #{client}" + begin + @pending_clients.delete(client) + + context = @clients.delete(client) + + if !context.nil? + mark_context_for_deletion(context) + end + + ensure + client.close + end + end + + # we want to eventually not do all the work here until a grace reconnect period has elasped + def mark_context_for_deletion(context) + # TODO handle notifies to sessions and friends about disconnect + end + + def extract_inner_message(client_msg) + msg = client_msg.fields[client_msg.type] + + if msg.nil? + raise "inner message is null. type: #{client_msg.type}, target: #{client_msg.target}" + end + + return msg + end + + def route(client_msg, client) + @log.debug("msg #{client_msg.type} #{client_msg.target}") + + if client_msg.target.nil? + raise 'client_msg.target is null' + end + + if @pending_clients.include? client and client_msg.type != ClientMessage::Type::LOGIN + # this client has not logged in and is trying to send a non-login message + raise "must 'Login' first" + end + + if @message_factory.server_directed? client_msg + + handle_server_directed(client_msg, client) + + elsif @message_factory.session_directed? client_msg + + session = client_msg.target[MessageFactory::SESSION_PREFIX_TARGET.length..-1] + handle_session_directed(session, client_msg, client) + + elsif @message_factory.user_directed? client_msg + + user = client_msg.target[MessageFactory::USER_PREFIX_TARGET.length..-1] + handle_user_directed(user, client_msg, client) + + else + raise "client_msg.target is unknown type: #{client_msg.target}" + end + + end + + def handle_server_directed(client_msg, client) + type, inner_msg = extract_inner_message(client_msg) + + if client_msg.type == ClientMessage::Type::LOGIN + + handle_login(client_msg.login, client) + + elsif client_msg.type == ClientMessage::Type::LOGIN_JAM_SESSION + + handle_join_jam_session(client_msg.join_jam_session, client) + + elsif client_msg.type == ClientMessage::Type::LEAVE_JAM_SESSION + + handle_leave_jam_session(client_msg.leave_jam_session, client) + + else + raise "unknown message type '#{client_msg.type}' for #{client_msg.target}-directed message" + end + end + + def handle_login(login, client) + username = login.username + token = login.token + password = login.password + + user = valid_login(username, password, token) + + if !user.nil? + + @log.debug "user #{user.email} logged in" + + # create a queue for just this user + queue = @channel.queue(user.id) + queue.bind(@endpoints, :routing_key => user.id) + queue.purge + + # TODO: alert friends + + # subscribe for any messages to self + subscription = queue.subscribe(:ack => true, :blocking => false) do |headers, msg| + client.send(msg) + headers.ack + end + + # respond with LOGIN_ACK to let client know it was successful + client.send(@message_factory.login_ack(client.ip).to_s) + + # remove from pending_queue + @pending_clients.delete(client) + + # add a tracker for this user + context = ClientContext.new(user, queue, subscription) + @clients[client] = context + else + raise 'invalid login' + end + end + + def handle_join_jam_session(join_jam_session, client) + # verify that the current user has the rights to actually join the jam session + context = @clients[client] + + session_id = join_jam_session.jam_session; + + begin + access_jam_session?(session_id) + rescue => e + # send back a failure ack and bail + client.send(@message_factory.login_jam_session_ack(true, e.to_s).to_s) + return + end + + topic = @channel.queue(session_id) + topic.bind(@sessions, :routing_key => session_id) + topic.purge + + # subscribe for any messages to session + subscription = topic.subscribe(:ack => false, :blocking => false) do |headers, msg| + client.send(msg) + #headers.ack + end + + old_session = context.session_topic + if !old_session.nil? && old_session.active? + # remove subscription to previous session + @log.debug "auto-removing user from previous session" + old_session.shutdown! + end + + context.session_topic = subscription + + # respond with LOGIN_JAM_SESSION_ACK to let client know it was successful + client.send(@message_factory.login_jam_session_ack(false, nil) + + # send 'new client' message + + end + + end + + def handle_leave_jam_session(leae_jam_session, client) + + context = @clients[client] + + raise 'unsupported' + end + + def valid_login(username, password, token) + + if !username.nil? and !password.nil? + # attempt login with username and password + user = User.find_by_email(username) + + if !user.nil? && user.authenticate(password) + @log.debug "#{username} login via password" + return user + else + @log.debug "#{username} login failure" + return nil + end + elsif !token.nil? + # attempt login with token + user = User.find_by_remember_token(token) + + if user.nil? + @log.debug "no user found with token" + return false + else + @log.debug "#{username} login via token" + return nil + end + else + raise 'no login data was found in Login message' + end + + end + + def access_jam_session?(jam_session_id, user) + jam_session = JamSession.find(jam_session_id) + + if jam_session.nil? + raise 'specified session not found' + end + + if !jam_session.access? user + raise 'not allowed to join the specified session' + end + + return jam_sesson + end + + def handle_session_directed(session, client_msg, client) + type, inner_msg = extract_inner_message(client_msg) + + context = @clients[client] + + # by not catching any exception here, this will kill the connection + # if for some reason the client is trying to send to a session that it doesn't + # belong to + access_jam_session?(session, context.user) + + # put it on the topic exchange for sessions + @sessions.publish(client_msg.to_s, :routing_key => session) + end + + def handle_user_directed(user, client_msg, client) + type, inner_msg = extract_inner_message(client_msg) + + end +end +end + diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb new file mode 100644 index 000000000..b5e945742 --- /dev/null +++ b/lib/jam_websockets/server.rb @@ -0,0 +1,27 @@ +require 'em-websocket' + +module JamWebsockets + class Server + + def initialize(options={}) + @log = Logging.logger[self] + @count=0 + @router = Router.new :transport => self + end + + def run(options={}) + + host = "0.0.0.0" + port = options[:port] + + @log.debug "starting server #{host}:#{port}" + + EventMachine.run { + EventMachine::WebSocket.start(:host => "0.0.0.0", :port => options[:port], :debug => options[:debug]) do |ws| + @router.new_client(ws) + end + } + end + end + +end diff --git a/lib/jam_websockets/version.rb b/lib/jam_websockets/version.rb new file mode 100644 index 000000000..67a2193ef --- /dev/null +++ b/lib/jam_websockets/version.rb @@ -0,0 +1,3 @@ +module JamWebsockets + VERSION = "0.0.1" +end diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 000000000..856d9e1bf --- /dev/null +++ b/spec/factories.rb @@ -0,0 +1,20 @@ +FactoryGirl.define do + factory :user, :class => JamRuby::User do + sequence(:name) { |n| "Person #{n}" } + sequence(:email) { |n| "person_#{n}@example.com"} + password "foobar" + password_confirmation "foobar" + + factory :admin do + admin true + end + end + + factory :jam_session, :class => JamRuby::JamSession do + sequence(:name) { |n| "Jam Session #{n}" } + end + + factory :jam_session_member, :class => JamRuby::JamSessionMember do + + end +end \ No newline at end of file diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb new file mode 100644 index 000000000..45e584d13 --- /dev/null +++ b/spec/jam_websockets/router_spec.rb @@ -0,0 +1,186 @@ +require 'spec_helper' +require 'thread' + +describe Router do + + message_factory = MessageFactory.new + + before do + + @router = Router.new() + @router.start() + + end + + subject { @router } + + after do + @router.stop + end + + + describe "servicability" do + it "should start and stop", :mq => true do + end + + it "should register for client events", :mq => true do + client = double("client") + client.should_receive(:onopen) + client.should_receive(:onclose) + client.should_receive(:onerror) + client.should_receive(:onmessage) + + @router.new_client(client) + + end + end + + + describe "login" do + it "should not allow login of bogus user", :mq => true do + TestClient = Class.new do + + attr_accessor :onmsgblock, :onopenblock + + def initiaize() + + end + + def onopen(&block) + @onopenblock = block + end + + def onmessage(&block) + @onmsgblock = block + end + + def close() + + end + end + + client = TestClient.new + + error_msg = message_factory.server_generic_error("invalid login") + + client.should_receive(:send).with(error_msg.to_s) + client.should_receive(:close) + client.should_receive(:onclose) + client.should_receive(:onerror) + + @router.new_client(client) + client.onopenblock.call + + # create a login message, and pass it into the router via onmsgblock.call + login = message_factory.login_with_user_pass("baduser@example.com", "foobar") + + client.onmsgblock.call login.to_s + + end + + it "should allow login of valid user", :mq => true do + @user = User.new(:name => "Example User", :email => "user@example.com", + :password => "foobar", :password_confirmation => "foobar") + @user.save + + + TestClient = Class.new do + + attr_accessor :onmsgblock, :onopenblock + + def initiaize() + + end + + def onopen(&block) + @onopenblock = block + end + + def onmessage(&block) + @onmsgblock = block + end + + end + + client = TestClient.new + + login_ack = message_factory.login_ack("1.1.1.1") + + client.should_receive(:send).with(login_ack.to_s) + client.should_receive(:close).exactly(0).times # close would occur on error, but this is good path + client.should_receive(:onclose) + client.should_receive(:onerror) + client.should_receive(:ip).and_return("1.1.1.1") + + @router.new_client(client) + client.onopenblock.call + + # create a login message, and pass it into the router via onmsgblock.call + login = message_factory.login_with_user_pass("user@example.com", "foobar") + + # attempt to log in, causing chain of events + client.onmsgblock.call login.to_s + + end + + + it "should allow jam_session_join of valid user", :mq => true do + + user1 = FactoryGirl.create(:user) # in the jam session + user2 = FactoryGirl.create(:user) # in the jam session + user3 = FactoryGirl.create(:user) # not in the jam session + + jam_session = FactoryGirl.create(:jam_session, :user => user1) + + jam_session_member1 = FactoryGirl.create(:jam_session_member, :user => user1, :jam_session => jam_session) + jam_session_member2 = FactoryGirl.create(:jam_session_member, :user => user2, :jam_session => jam_session) + + # make a jam_session and define two members + + + TestClient = Class.new do + + attr_accessor :onmsgblock, :onopenblock + + def initiaize() + + end + + def onopen(&block) + @onopenblock = block + end + + def onmessage(&block) + @onmsgblock = block + end + end + + client = TestClient.new + + login_ack = message_factory.login_ack("1.1.1.1") + + client.should_receive(:send).with(login_ack.to_s) + #client.should_receive(:close) + client.should_receive(:onclose) + client.should_receive(:onerror) + client.should_receive(:ip).and_return("1.1.1.1") + + @router.new_client(client) + client.onopenblock.call + + # create a login message, and pass it into the router via onmsgblock.call + login = message_factory.login_with_user_pass("baduser@example.com", "foobar") + + # first log in + client.onmsgblock.call login.to_s + + # then join jam session + login_jam_session = message_factory.login_jam_session(jam_session.id) + client.onmsgblock.call login_jam_session.to_s + + end + end + + +end + diff --git a/spec/spec_db.rb b/spec/spec_db.rb new file mode 100644 index 000000000..dabd970cb --- /dev/null +++ b/spec/spec_db.rb @@ -0,0 +1,11 @@ +class SpecDb + + TEST_DB_NAME="jam_websockets_test" + + def self.recreate_database + conn = PG::Connection.open("dbname=postgres") + conn.exec("DROP DATABASE IF EXISTS #{TEST_DB_NAME}") + conn.exec("CREATE DATABASE #{TEST_DB_NAME}") + JamDb::Migrator.new.migrate(:dbname => TEST_DB_NAME) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 000000000..d0d3d0f4d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,96 @@ + +require 'active_record' +require 'jam_db' +require 'spec_db' +require 'jam_websockets' +require 'timeout' + + +jamenv = ENV['JAMENV'] +jamenv ||= 'development' + +config = YAML::load(File.open('config/application.yml'))[jamenv] + + +if config["verbose"] + Logging.logger.root.level = :debug +else + Logging.logger.root.level = :info +end + +Logging.logger.root.appenders = Logging.appenders.stdout + +# recreate test database and migrate it +SpecDb::recreate_database +# initialize ActiveRecord's db connection +ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.yml'))["test"]) + + +require 'jam_ruby' +require 'jampb' +require 'rubygems' +require 'spork' +require 'database_cleaner' +require 'factory_girl' +require 'factories' + +include JamRuby +include JamWebsockets +include Jampb + + +#uncomment the following line to use spork with the debugger +#require 'spork/ext/ruby-debug' + + +Spork.prefork do + # Loading more in this block will cause your tests to run faster. However, + # if you change any configuration or code from libraries loaded here, you'll + # need to restart spork for it take effect. +# This file is copied to spec/ when you run 'rails generate rspec:install' + #ENV["RAILS_ENV"] ||= 'test' + #require File.expand_path("../../config/environment", __FILE__) + require 'rspec/autorun' + #require 'rspec/rails' +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# Require this file using `require "spec_helper"` to ensure that it is only +# loaded once. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + RSpec.configure do |config| + config.treat_symbols_as_metadata_keys_with_true_values = true + config.run_all_when_everything_filtered = true + config.filter_run :focus + + config.before(:suite) do + DatabaseCleaner.strategy = :transaction + DatabaseCleaner.clean_with(:truncation) + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.after(:each) do + DatabaseCleaner.clean + end + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + #config.use_transactional_fixtures = true + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = 'random' + end +end + + +Spork.each_run do + # This code will be run each time you run your specs. + +end From 11e403deb32aed32840294ab73acb8440adb4007 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 21 Aug 2012 22:08:01 -0500 Subject: [PATCH 02/98] * no longer using 'pg' gem for ActiveRecord, because it's not tested for jruby; also making all tests pass --- .rvmrc | 2 +- Gemfile | 6 ++++-- config/database.yml | 6 ++++-- lib/jam_websockets/client_context.rb | 6 +++--- lib/jam_websockets/router.rb | 12 +++++------- spec/factories.rb | 2 +- spec/jam_websockets/router_spec.rb | 13 +++++++++---- spec/spec_db.rb | 17 ++++++++++++++++- spec/spec_helper.rb | 26 ++++++++++++++------------ 9 files changed, 57 insertions(+), 33 deletions(-) diff --git a/.rvmrc b/.rvmrc index ad94dd625..a5e31abc5 100644 --- a/.rvmrc +++ b/.rvmrc @@ -1,2 +1,2 @@ -rvm use jruby-1.7.0.preview1@websockets --create +rvm use jruby-head@websockets --create #rvm use ruby-1.9.3@websockets --create diff --git a/Gemfile b/Gemfile index 598a7113f..ac3361db9 100644 --- a/Gemfile +++ b/Gemfile @@ -11,17 +11,19 @@ gem 'pg_migrate', '0.1.4' gem 'jam_db', :path => '~/workspace/jam-db/target/ruby_package' gem 'jam_ruby', :path => '~/workspace/jam-ruby' gem 'jampb', :path => '~/workspace/jam-pb/target/ruby/jampb' -gem 'em-websocket', :path=> '~/workspace/em-websocket' +gem 'em-websocket' # :path=> '~/workspace/em-websocket' gem 'hot_bunnies', '1.3.8' gem 'activerecord', '3.2.7' gem 'logging' #gem 'em-http-request' +gem 'activerecord-jdbc-adapter' +gem 'activerecord-jdbcpostgresql-adapter' group :test do gem 'cucumber' gem 'rspec' gem 'factory_girl' - gem 'spork', '0.9.0' + #gem 'spork', '0.9.0' gem 'database_cleaner', '0.7.0' gem 'guard', '>= 0.10.0' gem 'guard-rspec', '>= 0.7.3' diff --git a/config/database.yml b/config/database.yml index 303d4b907..5a6edeacd 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,8 +1,10 @@ test: - adapter: postgresql + adapter: jdbcpostgresql database: jam_websockets_test + host: localhost + port: 5432 pool: 3 username: postgres password: postgres timeout: 2000 - encoding: unicode \ No newline at end of file + encoding: unicode diff --git a/lib/jam_websockets/client_context.rb b/lib/jam_websockets/client_context.rb index 43eac7c2c..ddcf6e5cf 100644 --- a/lib/jam_websockets/client_context.rb +++ b/lib/jam_websockets/client_context.rb @@ -1,10 +1,10 @@ module JamWebsockets class ClientContext - attr_accessor :user_id, :user_queue, :session_topic, :subscription + attr_accessor :user, :user_queue, :session_topic, :subscription - def initialize(user_id, user_queue, subscription) - @user_id = user_id + def initialize(user, user_queue, subscription) + @user = user @user_queue = user_queue @subscription = subscription @session_topic = nil diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 574e0241e..f88686da1 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -98,7 +98,7 @@ module JamWebsockets begin self.route(pb_msg, client) rescue => e - @log.debug "ending client session due to error: #{e.to_s}" + @log.debug "ending client session due to error: #{e.to_s} #{e.backtrace}" begin # wrap the message up and send it down @@ -162,7 +162,7 @@ module JamWebsockets elsif @message_factory.session_directed? client_msg - session = client_msg.target[MessageFactory::SESSION_PREFIX_TARGET.length..-1] + session = client_msg.target[MessageFactory::SESSION_TARGET_PREFIX.length..-1] handle_session_directed(session, client_msg, client) elsif @message_factory.user_directed? client_msg @@ -268,7 +268,7 @@ module JamWebsockets context.session_topic = subscription # respond with LOGIN_JAM_SESSION_ACK to let client know it was successful - client.send(@message_factory.login_jam_session_ack(false, nil) + client.send(@message_factory.login_jam_session_ack(false, nil)) # send 'new client' message @@ -276,7 +276,7 @@ module JamWebsockets end - def handle_leave_jam_session(leae_jam_session, client) + def handle_leave_jam_session(leave_jam_session, client) context = @clients[client] @@ -324,7 +324,7 @@ module JamWebsockets raise 'not allowed to join the specified session' end - return jam_sesson + return jam_session end def handle_session_directed(session, client_msg, client) @@ -346,5 +346,3 @@ module JamWebsockets end end -end - diff --git a/spec/factories.rb b/spec/factories.rb index 856d9e1bf..37aad9841 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -17,4 +17,4 @@ FactoryGirl.define do factory :jam_session_member, :class => JamRuby::JamSessionMember do end -end \ No newline at end of file +end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 45e584d13..2d6733a80 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -86,7 +86,7 @@ describe Router do TestClient = Class.new do - attr_accessor :onmsgblock, :onopenblock + attr_accessor :onmsgblock, :onopenblock, :oncloseblock def initiaize() @@ -100,6 +100,9 @@ describe Router do @onmsgblock = block end + def close(&block) + @oncloseblock = block + end end client = TestClient.new @@ -153,6 +156,10 @@ describe Router do def onmessage(&block) @onmsgblock = block end + + def close(&block) + @oncloseblock = block + end end client = TestClient.new @@ -169,7 +176,7 @@ describe Router do client.onopenblock.call # create a login message, and pass it into the router via onmsgblock.call - login = message_factory.login_with_user_pass("baduser@example.com", "foobar") + login = message_factory.login_with_user_pass(user1.email, "foobar") # first log in client.onmsgblock.call login.to_s @@ -180,7 +187,5 @@ describe Router do end end - - end diff --git a/spec/spec_db.rb b/spec/spec_db.rb index dabd970cb..7ac708075 100644 --- a/spec/spec_db.rb +++ b/spec/spec_db.rb @@ -2,7 +2,22 @@ class SpecDb TEST_DB_NAME="jam_websockets_test" - def self.recreate_database + def self.recreate_database(db_config) + recreate_database_jdbc(db_config) + end + + def self.recreate_database_jdbc(db_config) + original = db_config["database"] + db_config["database"] = "postgres" + ActiveRecord::Base.establish_connection(db_config) + ActiveRecord::Base.connection.execute("DROP DATABASE IF EXISTS #{TEST_DB_NAME}") + ActiveRecord::Base.connection.execute("CREATE DATABASE #{TEST_DB_NAME}") + JamDb::Migrator.new.migrate(:dbname => TEST_DB_NAME) + db_config["database"] = original + end + + def self.recreate_database_pg + conn = PG::Connection.open("dbname=postgres") conn.exec("DROP DATABASE IF EXISTS #{TEST_DB_NAME}") conn.exec("CREATE DATABASE #{TEST_DB_NAME}") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d0d3d0f4d..d1c8af6a1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,15 +21,16 @@ end Logging.logger.root.appenders = Logging.appenders.stdout # recreate test database and migrate it -SpecDb::recreate_database -# initialize ActiveRecord's db connection -ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.yml'))["test"]) +db_config = YAML::load(File.open('config/database.yml'))["test"] +SpecDb::recreate_database(db_config) +# initialize ActiveRecord's db connection +ActiveRecord::Base.establish_connection(db_config) require 'jam_ruby' require 'jampb' require 'rubygems' -require 'spork' +#require 'spork' require 'database_cleaner' require 'factory_girl' require 'factories' @@ -43,7 +44,7 @@ include Jampb #require 'spork/ext/ruby-debug' -Spork.prefork do +#Spork.prefork do # Loading more in this block will cause your tests to run faster. However, # if you change any configuration or code from libraries loaded here, you'll # need to restart spork for it take effect. @@ -64,16 +65,17 @@ Spork.prefork do config.filter_run :focus config.before(:suite) do - DatabaseCleaner.strategy = :transaction - DatabaseCleaner.clean_with(:truncation) + # DatabaseCleaner.strategy = :transaction + # DatabaseCleaner.clean_with(:truncation) end config.before(:each) do - DatabaseCleaner.start + # DatabaseCleaner.start end config.after(:each) do - DatabaseCleaner.clean + ActiveRecord::Base.connection.execute('select truncate_tables()') + # DatabaseCleaner.clean end # If you're not using ActiveRecord, or you'd prefer not to run each of your @@ -87,10 +89,10 @@ Spork.prefork do # --seed 1234 config.order = 'random' end -end +#end -Spork.each_run do +#Spork.each_run do # This code will be run each time you run your specs. -end +#end From 89a4be2321a7d7eea0ac67b6b34f8898943eeb8f Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 23 Aug 2012 21:46:58 -0500 Subject: [PATCH 03/98] * websocket gateway allows login to server and to session --- .pg_migrate | 1 + Gemfile | 2 +- bin/{jam_websockets => websocket_gateway} | 2 + ...jam_websockets.sh => websocket_gateway.sh} | 2 +- config/database.yml | 11 +++ lib/jam_websockets.rb | 3 + lib/jam_websockets/router.rb | 92 +++++++++++-------- lib/jam_websockets/server.rb | 11 ++- migrate.sh | 3 + 9 files changed, 86 insertions(+), 41 deletions(-) create mode 100644 .pg_migrate rename bin/{jam_websockets => websocket_gateway} (79%) rename bin/{jam_websockets.sh => websocket_gateway.sh} (52%) create mode 100755 migrate.sh diff --git a/.pg_migrate b/.pg_migrate new file mode 100644 index 000000000..1a32f454a --- /dev/null +++ b/.pg_migrate @@ -0,0 +1 @@ +up.connopts=dbname:jam diff --git a/Gemfile b/Gemfile index ac3361db9..1b800a230 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'jruby-openssl' gem 'ruby-protocol-buffers', '1.2.2' -gem 'pg_migrate', '0.1.4' +gem 'pg_migrate','0.1.5' #:path => '~/workspace/pg_migrate_ruby' #'0.1.4' gem 'jam_db', :path => '~/workspace/jam-db/target/ruby_package' gem 'jam_ruby', :path => '~/workspace/jam-ruby' gem 'jampb', :path => '~/workspace/jam-pb/target/ruby/jampb' diff --git a/bin/jam_websockets b/bin/websocket_gateway similarity index 79% rename from bin/jam_websockets rename to bin/websocket_gateway index 41db8e466..7cb03c5af 100755 --- a/bin/jam_websockets +++ b/bin/websocket_gateway @@ -10,6 +10,7 @@ jamenv = ENV['JAMENV'] jamenv ||= 'development' config = YAML::load(File.open('config/application.yml'))[jamenv] +db_config = YAML::load(File.open('config/database.yml'))[jamenv] if config["verbose"] @@ -20,4 +21,5 @@ end Logging.logger.root.appenders = Logging.appenders.stdout +ActiveRecord::Base.establish_connection(db_config) Server.new.run :port => config["port"], :debug => false#=> config["verbose"] diff --git a/bin/jam_websockets.sh b/bin/websocket_gateway.sh similarity index 52% rename from bin/jam_websockets.sh rename to bin/websocket_gateway.sh index f23e59142..affad8beb 100755 --- a/bin/jam_websockets.sh +++ b/bin/websocket_gateway.sh @@ -1,4 +1,4 @@ #!/bin/sh # this script is used only in development -bundle exec ruby -Ilib bin/jam_websockets $* +bundle exec ruby -Ilib bin/websocket_gateway $* diff --git a/config/database.yml b/config/database.yml index 5a6edeacd..518037a56 100644 --- a/config/database.yml +++ b/config/database.yml @@ -8,3 +8,14 @@ test: password: postgres timeout: 2000 encoding: unicode + +development: + adapter: jdbcpostgresql + database: jam + host: localhost + port: 5432 + pool: 3 + username: postgres + password: postgres + timeout: 2000 + encoding: unicode diff --git a/lib/jam_websockets.rb b/lib/jam_websockets.rb index c9342516d..6c249661a 100644 --- a/lib/jam_websockets.rb +++ b/lib/jam_websockets.rb @@ -1,9 +1,12 @@ require "logging" +require "jam_ruby" require "jam_websockets/version" require "jam_websockets/client_context" require "jam_websockets/router" require "jam_websockets/server" +include JamRuby + module JamWebsockets # Your code goes here... end diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index f88686da1..20cac0e87 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -14,7 +14,7 @@ module JamWebsockets @connection = nil @channel = nil @endpoints = nil - @message_factory = MessageFactory.new + @message_factory = JamRuby::MessageFactory.new end def start(options = {}) @@ -36,18 +36,9 @@ module JamWebsockets end def cleanup() - - @clients.each do |key, context| - begin - # todo delegate to client cleanup method - if context.subscription.active? - context.subscription.shutdown! - end - rescue => e - @log.debug "unable to cancel subscription on cleanup: #{e}" - end - - #context.user_queue.unbind(@endpoints) + @clients.each do |client, context| + cleanup_client(client) + #context.user_queue.unbind(@endpoints) end if !@channel.nil? @@ -56,33 +47,33 @@ module JamWebsockets if !@connection.nil? @connection.close - end - end + end + end - def stop - @log.debug "shutdown" - cleanup - end + def stop + @log.debug "shutdown" + cleanup + end - def new_client(client) + def new_client(client) - @pending_clients.add(client) + @pending_clients.add(client) - client.onopen { - @log.debug "client connected #{client}" - } + client.onopen { + @log.debug "client connected #{client}" + } - client.onclose { - @log.debug "Connection closed" + client.onclose { + @log.debug "Connection closed" - cleanup_client(client) + cleanup_client(client) } client.onerror { |error| if error.kind_of?(EM::WebSocket::WebSocketError) @log.error "websockets error: #{error}" else - @log.error "generic error #{error}" + @log.error "generic error: #{error} #{error.backtrace}" end cleanup_client(client) @@ -94,11 +85,12 @@ module JamWebsockets # TODO: set a max message size before we put it through PB? # TODO: rate limit? - pb_msg = Jampb::ClientMessage.parse(msg.to_s) begin + pb_msg = Jampb::ClientMessage.parse(msg.to_s) self.route(pb_msg, client) rescue => e - @log.debug "ending client session due to error: #{e.to_s} #{e.backtrace}" + @log.debug "ending client session due to error: #{e.to_s}" + @log.debug e begin # wrap the message up and send it down @@ -116,16 +108,31 @@ module JamWebsockets def cleanup_client(client) @log.debug "cleaning up client #{client}" begin - @pending_clients.delete(client) + @pending_clients.delete(client) context = @clients.delete(client) + + if !context.nil? + begin + if context.subscription.active? + @log.debug "cleaning up user subscription" + context.subscription.shutdown! + end + + if !context.session_topic.nil? && context.session_topic.active? + @log.debug "cleaning up session subscription" + context.session_topic.shutdown! + end + + rescue => e + @log.debug "unable to cancel subscription on cleanup: #{e}" + end - if !context.nil? mark_context_for_deletion(context) end ensure - client.close + client.close_websocket end end @@ -135,7 +142,7 @@ module JamWebsockets end def extract_inner_message(client_msg) - msg = client_msg.fields[client_msg.type] + msg = client_msg.value_for_tag(client_msg.type) if msg.nil? raise "inner message is null. type: #{client_msg.type}, target: #{client_msg.target}" @@ -183,9 +190,13 @@ module JamWebsockets handle_login(client_msg.login, client) + elsif client_msg.type == ClientMessage::Type::HEARTBEAT + + handle_heartbeat(client_msg.heartbeat, client) + elsif client_msg.type == ClientMessage::Type::LOGIN_JAM_SESSION - handle_join_jam_session(client_msg.join_jam_session, client) + handle_join_jam_session(client_msg.login_jam_session, client) elsif client_msg.type == ClientMessage::Type::LEAVE_JAM_SESSION @@ -221,7 +232,7 @@ module JamWebsockets end # respond with LOGIN_ACK to let client know it was successful - client.send(@message_factory.login_ack(client.ip).to_s) + client.send(@message_factory.login_ack(client.request["origin"]).to_s) # remove from pending_queue @pending_clients.delete(client) @@ -234,6 +245,10 @@ module JamWebsockets end end + def handle_heartbeat(heartbeat, client) + # todo + end + def handle_join_jam_session(join_jam_session, client) # verify that the current user has the rights to actually join the jam session context = @clients[client] @@ -241,9 +256,10 @@ module JamWebsockets session_id = join_jam_session.jam_session; begin - access_jam_session?(session_id) + access_jam_session?(session_id, context.user) rescue => e # send back a failure ack and bail + @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" client.send(@message_factory.login_jam_session_ack(true, e.to_s).to_s) return end @@ -314,7 +330,7 @@ module JamWebsockets end def access_jam_session?(jam_session_id, user) - jam_session = JamSession.find(jam_session_id) + jam_session = JamSession.find_by_id(jam_session_id) if jam_session.nil? raise 'specified session not found' diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index b5e945742..968ccc1e3 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -15,9 +15,18 @@ module JamWebsockets port = options[:port] @log.debug "starting server #{host}:#{port}" - + + @router.start + + # if you don't do this, the app won't exit unless you kill -9 + at_exit do + @log.debug "cleaning up server" + @router.cleanup + end + EventMachine.run { EventMachine::WebSocket.start(:host => "0.0.0.0", :port => options[:port], :debug => options[:debug]) do |ws| + @log.debug "new client #{ws}" @router.new_client(ws) end } diff --git a/migrate.sh b/migrate.sh new file mode 100755 index 000000000..04b31b1bc --- /dev/null +++ b/migrate.sh @@ -0,0 +1,3 @@ + + +bundle exec jam_db up --connopts=dbname:jam host:localhost user:postgres password:postgres --verbose From b7fd331c2b603ddb277e2e3cbbda2c3abe94fb50 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 26 Aug 2012 13:42:22 -0500 Subject: [PATCH 04/98] * login and login_jam_session working with tests. logging cleaned up --- Gemfile | 2 +- README.md | 4 +- bin/websocket_gateway | 2 +- config/application.yml | 1 + lib/jam_websockets.rb | 1 + lib/jam_websockets/client_context.rb | 15 +- lib/jam_websockets/router.rb | 467 +++++++++++++++++---------- lib/jam_websockets/server.rb | 8 +- lib/jam_websockets/session_error.rb | 4 + spec/jam_websockets/router_spec.rb | 214 ++++++------ 10 files changed, 451 insertions(+), 267 deletions(-) create mode 100644 lib/jam_websockets/session_error.rb diff --git a/Gemfile b/Gemfile index 1b800a230..987c4a2b0 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,7 @@ gem 'pg_migrate','0.1.5' #:path => '~/workspace/pg_migrate_ruby' #'0.1.4' gem 'jam_db', :path => '~/workspace/jam-db/target/ruby_package' gem 'jam_ruby', :path => '~/workspace/jam-ruby' gem 'jampb', :path => '~/workspace/jam-pb/target/ruby/jampb' -gem 'em-websocket' # :path=> '~/workspace/em-websocket' +gem 'em-websocket', :path=> '~/workspace/em-websocket' gem 'hot_bunnies', '1.3.8' gem 'activerecord', '3.2.7' gem 'logging' diff --git a/README.md b/README.md index dd14c7de9..cd61f2a75 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ TODO & DESIGN LIMITATIONS * The rabbitmq connection isn't pooled. Throughput limitation (but could be resolved by just starting more instances of JamWebsocket behind Haproxy) * The database connection isn't pooled. Throughput limitation (but could be resolved by just starting more instances of JamWebsocket behind Haproxy) +* We make just one user topic registration and session registration for all users/sessions. If ever we had 10 of servers, it could be wasteful. It just depends on how fast the bogus messaging can be ignored +* The database connection is pooled. * The user model is stored in memory, meaning periodically it should be reloaded from the database (in case a user was marked inactive and you want them knocked out of the system) * The user could easily join to multiple sessions. Currently, though, the ClientContext object only tracks one jam session topic subscription. This is minimial to change. -* peek logic not implemented on server for protoc messages; this could be done to save cost of deserialization and serialization for session/user directed messages \ No newline at end of file +* peek logic not implemented on server for protoc messages; this could be done to save cost of deserialization and serialization for session/user directed messages diff --git a/bin/websocket_gateway b/bin/websocket_gateway index 7cb03c5af..8087fdb46 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -22,4 +22,4 @@ end Logging.logger.root.appenders = Logging.appenders.stdout ActiveRecord::Base.establish_connection(db_config) -Server.new.run :port => config["port"], :debug => false#=> config["verbose"] +Server.new.run :port => config["port"], :debug => true# config["debug"] diff --git a/config/application.yml b/config/application.yml index cc37caeb7..34bf0c45d 100644 --- a/config/application.yml +++ b/config/application.yml @@ -1,6 +1,7 @@ development: port: 6767 verbose: true + emwebsocket_debug: false test: port: 6769 diff --git a/lib/jam_websockets.rb b/lib/jam_websockets.rb index 6c249661a..b09be5a42 100644 --- a/lib/jam_websockets.rb +++ b/lib/jam_websockets.rb @@ -1,6 +1,7 @@ require "logging" require "jam_ruby" require "jam_websockets/version" +require "jam_websockets/session_error" require "jam_websockets/client_context" require "jam_websockets/router" require "jam_websockets/server" diff --git a/lib/jam_websockets/client_context.rb b/lib/jam_websockets/client_context.rb index ddcf6e5cf..eb40fb215 100644 --- a/lib/jam_websockets/client_context.rb +++ b/lib/jam_websockets/client_context.rb @@ -1,13 +1,18 @@ module JamWebsockets class ClientContext - attr_accessor :user, :user_queue, :session_topic, :subscription + attr_accessor :user, :client, :msg_count, :session - def initialize(user, user_queue, subscription) + def initialize(user, client) @user = user - @user_queue = user_queue - @subscription = subscription - @session_topic = nil + @client = client + @msg_count = 0 + @session = nil end + + def to_s + return "Client[user:#{@user} client:#{@client} msgs:#{@msg_count} session:#{@session}]" + end + end end diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 20cac0e87..5150fa918 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -1,33 +1,46 @@ require 'set' require 'hot_bunnies' +require 'thread' + +import java.util.concurrent.Executors include Jampb module JamWebsockets class Router + attr_accessor :user_context_lookup, :session_context_lookup + def initialize(options={}) @log = Logging.logger[self] @pending_clients = Set.new # clients that have connected to server, but not logged in. @clients = {} # clients that have logged in - @sessions = nil + @user_context_lookup = {} # lookup a set of client_contexts by user_id + @session_context_lookup = {} # lookup a set of client_contexts by session_id + @sessions_exchange = nil @connection = nil @channel = nil - @endpoints = nil + @users_exchange = nil @message_factory = JamRuby::MessageFactory.new + @semaphore = Mutex.new + @user_topic = nil + @user_subscription = nil + @session_topic = nil + @session_subscription = nil + @thread_pool = nil end def start(options = {}) - @log.debug "startup" + @log.info "startup" begin + @thread_pool = Executors.new_fixed_thread_pool(8) @connection = HotBunnies.connect(:host => options[:host], :port => options[:port]) @channel = @connection.create_channel @channel.prefetch = 10 - @endpoints = @channel.exchange('client_endpoints', :type => :direct) - @sessions = @channel.exchange('client_sessions', :type => :topic) + register_topics rescue => e cleanup raise e @@ -35,29 +48,180 @@ module JamWebsockets end - def cleanup() - @clients.each do |client, context| - cleanup_client(client) - #context.user_queue.unbind(@endpoints) - end + def add_user(context) + user_contexts = @user_context_lookup[context.user.id] + if user_contexts.nil? + user_contexts = Set.new + @user_context_lookup[context.user.id] = user_contexts + end - if !@channel.nil? - @channel.close - end + user_contexts.add(context) + end - if !@connection.nil? - @connection.close + def remove_user(context) + user_contexts = @user_context_lookup[context.user.id] + if user_contexts.nil? + @log.warn "user can not be removed #{context}" + else + # delete the context from set of user contexts + user_contexts.delete(context) + + # if last user context, delete entire set (memory leak concern) + if user_contexts.length == 0 + @user_context_lookup.delete(context.user.id) + end end end + def add_session(context) + session_contexts = @session_context_lookup[context.session.id] + if session_contexts.nil? + session_contexts = Set.new + @session_context_lookup[context.session.id] = session_contexts + end + + session_contexts.add(context) + end + + def remove_session(context) + session_contexts = @session_context_lookup[context.session.id] + if session_contexts.nil? + @log.warn "session can not be removed #{context}" + else + # delete the context from set of session contexts + session_contexts.delete(context) + + # if last session context, delete entire set (memory leak concern) + if session_contexts.length == 0 + @session_context_lookup.delete(context.session.id) + end + + context.session = nil + end + end + + + # register topic for user messages and session messages + def register_topics + @users_exchange = @channel.exchange('users', :type => :topic) + @sessions_exchange = @channel.exchange('sessions', :type => :topic) + + # create user messaging topic + @user_topic = @channel.queue("", :auto_delete => true) + @user_topic.bind(@users_exchange, :routing_key => "user.#") + @user_topic.purge + + # TODO: alert friends + + # subscribe for any messages to users + + #@user_subscription = @user_topic.subscribe(:ack => false, :blocking => false, :executor => @threadpool) do |headers, msg| + @user_subscription = @user_topic.subscribe(:ack => false) + @user_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| + begin + routing_key = headers.envelope.routing_key + user_id = routing_key["user.".length..-1] + @sempahore.synchronize do + contexts = @user_context_lookup[user_id] + + unless contexts.nil? + + @log.debug "received user-directed message for session: #{user_id}" + + contexts.each do |context| + @log.debug "sending user message to #{context}" + EM.schedule do + context.client.instance_variable_get(:@handler).send_frame(:binary, msg) + end + end + end + end + + rescue => e + @log.error "unhandled error in messaging to client" + end + end + + @session_topic = @channel.queue("", :auto_delete => true) + @session_topic.bind(@sessions_exchange, :routing_key => "session.#") + @session_topic.purge + + # subscribe for any messages to session + #@session_subscription = @session_topic.subscribe(:ack => false, :blocking => false) do |headers, msg| + @session_subscription = @session_topic.subscribe(:ack => false) + @session_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| + begin + routing_key = headers.envelope.routing_key + session_id = routing_key["session.".length..-1] + @semaphore.synchronize do + contexts = @session_context_lookup[session_id] + + unless contexts.nil? + + @log.debug "received session-directed message for session: #{session_id}" + + contexts.each do |context| + @log.debug "sending session message to #{context}" + EM.schedule do + @log.debug "ONTUHNOTEHU" + context.client.instance_variable_get(:@handler).send_frame(:binary, msg) + @log.debug "gross" + end + end + end + end + + rescue => e + @log.error "unhandled error in messaging to client" + end + end + end + + def cleanup() + # shutdown topic listeners and mq connection + begin + if !@user_subscription.nil? && @user_subscription.active? + @log.debug "cleaning up user subscription" + @user_subscription.cancel + @user_subscription.shutdown! + end + + if !@session_subscription.nil? && @session_subscription.active? + @log.debug "cleaning up session subscription" + @session_subscription.cancel + @session_subscription.shutdown! + end + + rescue => e + @log.debug "unable to cancel subscription on cleanup: #{e}" + end + + @thread_pool.shutdown + + if !@channel.nil? + @channel.close + end + + if !@connection.nil? + @connection.close + end + + # tear down each individual client + @clients.each do |client, context| + cleanup_client(client) + end + end + def stop - @log.debug "shutdown" + @log.info "shutdown" cleanup end def new_client(client) - @pending_clients.add(client) + @semaphore.synchronize do + @pending_clients.add(client) + end client.onopen { @log.debug "client connected #{client}" @@ -77,6 +241,7 @@ module JamWebsockets end cleanup_client(client) + client.close_websocket } client.onmessage { |msg| @@ -88,79 +253,70 @@ module JamWebsockets begin pb_msg = Jampb::ClientMessage.parse(msg.to_s) self.route(pb_msg, client) + rescue SessionError => e + @log.info "ending client session deliberately due to malformed client behavior. reason=#{e}" + begin + # wrap the message up and send it down + client.send(@message_factory.server_rejection_error(e.to_s).to_s) + ensure + client.close_websocket + cleanup_client(client) + end rescue => e - @log.debug "ending client session due to error: #{e.to_s}" - @log.debug e - - begin + @log.error "ending client session due to server programming or runtime error. reason=#{e.to_s}" + @log.error e + + begin # wrap the message up and send it down client.send(@message_factory.server_generic_error(e.to_s).to_s) ensure + client.close_websocket cleanup_client(client) end - end } end + # removes all resources associated with a client def cleanup_client(client) - @log.debug "cleaning up client #{client}" - begin - @pending_clients.delete(client) - context = @clients.delete(client) - - if !context.nil? - begin - if context.subscription.active? - @log.debug "cleaning up user subscription" - context.subscription.shutdown! - end + @semaphore.synchronize do + pending = @pending_clients.delete?(client) - if !context.session_topic.nil? && context.session_topic.active? - @log.debug "cleaning up session subscription" - context.session_topic.shutdown! - end + if !pending.nil? + @log.debug "cleaning up pending client #{client}" + else + context = @clients.delete(client) - rescue => e - @log.debug "unable to cancel subscription on cleanup: #{e}" + if !context.nil? + + remove_user(context) + + if !context.session.nil? + remove_session(context) + end + else + @log.debug "skipping duplicate cleanup attempt of authorized client" end - mark_context_for_deletion(context) - end - - ensure - client.close_websocket - end - end - - # we want to eventually not do all the work here until a grace reconnect period has elasped - def mark_context_for_deletion(context) - # TODO handle notifies to sessions and friends about disconnect - end - - def extract_inner_message(client_msg) - msg = client_msg.value_for_tag(client_msg.type) - - if msg.nil? - raise "inner message is null. type: #{client_msg.type}, target: #{client_msg.target}" - end - - return msg + end + end end def route(client_msg, client) - @log.debug("msg #{client_msg.type} #{client_msg.target}") + message_type = @message_factory.get_message_type(client_msg) + + raise SessionError, "unknown message type received: #{client_msg.type}" if message_type.nil? + + @log.debug("msg received #{message_type}") - if client_msg.target.nil? - raise 'client_msg.target is null' - end + raise SessionError, 'client_msg.target is null' if client_msg.target.nil? if @pending_clients.include? client and client_msg.type != ClientMessage::Type::LOGIN # this client has not logged in and is trying to send a non-login message - raise "must 'Login' first" + raise SessionError, "must 'Login' first" end if @message_factory.server_directed? client_msg @@ -178,13 +334,12 @@ module JamWebsockets handle_user_directed(user, client_msg, client) else - raise "client_msg.target is unknown type: #{client_msg.target}" + raise SessionError, "client_msg.target is unknown type: #{client_msg.target}" end end def handle_server_directed(client_msg, client) - type, inner_msg = extract_inner_message(client_msg) if client_msg.type == ClientMessage::Type::LOGIN @@ -203,7 +358,7 @@ module JamWebsockets handle_leave_jam_session(client_msg.leave_jam_session, client) else - raise "unknown message type '#{client_msg.type}' for #{client_msg.target}-directed message" + raise SessionError, "unknown message type '#{client_msg.type}' for #{client_msg.target}-directed message" end end @@ -218,45 +373,45 @@ module JamWebsockets @log.debug "user #{user.email} logged in" - # create a queue for just this user - queue = @channel.queue(user.id) - queue.bind(@endpoints, :routing_key => user.id) - queue.purge - - # TODO: alert friends - - # subscribe for any messages to self - subscription = queue.subscribe(:ack => true, :blocking => false) do |headers, msg| - client.send(msg) - headers.ack - end - # respond with LOGIN_ACK to let client know it was successful client.send(@message_factory.login_ack(client.request["origin"]).to_s) # remove from pending_queue - @pending_clients.delete(client) + @semaphore.synchronize do + @pending_clients.delete(client) - # add a tracker for this user - context = ClientContext.new(user, queue, subscription) - @clients[client] = context + # add a tracker for this user + context = ClientContext.new(user, client) + @clients[client] = context + add_user(context) + end else - raise 'invalid login' + raise SessionError, 'invalid login' end end def handle_heartbeat(heartbeat, client) - # todo + # todo: manage staleness end def handle_join_jam_session(join_jam_session, client) # verify that the current user has the rights to actually join the jam session context = @clients[client] - session_id = join_jam_session.jam_session; + session_id = join_jam_session.jam_session begin - access_jam_session?(session_id, context.user) + session = access_jam_session?(session_id, context.user) + @log.debug "user #{context} joining new session #{session}" + @semaphore.synchronize do + old_session = context.session + if !old_session.nil? + @log.debug "#{context} is already in session. auto-logging out to join new session." + remove_session(context) + end + context.session = session + add_session(context) + end rescue => e # send back a failure ack and bail @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" @@ -264,101 +419,83 @@ module JamWebsockets return end - topic = @channel.queue(session_id) - topic.bind(@sessions, :routing_key => session_id) - topic.purge - - # subscribe for any messages to session - subscription = topic.subscribe(:ack => false, :blocking => false) do |headers, msg| - client.send(msg) - #headers.ack - end - - old_session = context.session_topic - if !old_session.nil? && old_session.active? - # remove subscription to previous session - @log.debug "auto-removing user from previous session" - old_session.shutdown! - end - - context.session_topic = subscription - # respond with LOGIN_JAM_SESSION_ACK to let client know it was successful - client.send(@message_factory.login_jam_session_ack(false, nil)) + client.send(@message_factory.login_jam_session_ack(false, nil).to_s) - # send 'new client' message - + # send 'new client' message to other members in the session + handle_session_directed(session_id, + @message_factory.user_joined_jam_session(context.user.id, context.user.name), + client) end - end + def handle_leave_jam_session(leave_jam_session, client) - def handle_leave_jam_session(leave_jam_session, client) + context = @clients[client] - context = @clients[client] + raise SessionError, "unsupported" + end - raise 'unsupported' - end + def valid_login(username, password, token) - def valid_login(username, password, token) + if !username.nil? and !password.nil? + # attempt login with username and password + user = User.find_by_email(username) - if !username.nil? and !password.nil? - # attempt login with username and password - user = User.find_by_email(username) + if !user.nil? && user.authenticate(password) + @log.debug "#{username} login via password" + return user + else + @log.debug "#{username} login failure" + return nil + end + elsif !token.nil? + # attempt login with token + user = User.find_by_remember_token(token) - if !user.nil? && user.authenticate(password) - @log.debug "#{username} login via password" - return user - else - @log.debug "#{username} login failure" - return nil - end - elsif !token.nil? - # attempt login with token - user = User.find_by_remember_token(token) + if user.nil? + @log.debug "no user found with token" + return false + else + @log.debug "#{username} login via token" + return nil + end + else + raise SessionError, 'no login data was found in Login message' + end - if user.nil? - @log.debug "no user found with token" - return false - else - @log.debug "#{username} login via token" - return nil - end - else - raise 'no login data was found in Login message' - end + end - end + def access_jam_session?(jam_session_id, user) + jam_session = JamSession.find_by_id(jam_session_id) - def access_jam_session?(jam_session_id, user) - jam_session = JamSession.find_by_id(jam_session_id) + if jam_session.nil? + raise SessionError, 'specified session not found' + end - if jam_session.nil? - raise 'specified session not found' - end + if !jam_session.access? user + raise SessionError, 'not allowed to join the specified session' + end - if !jam_session.access? user - raise 'not allowed to join the specified session' - end + return jam_session + end - return jam_session - end + def handle_session_directed(session_id, client_msg, client) - def handle_session_directed(session, client_msg, client) - type, inner_msg = extract_inner_message(client_msg) + context = @clients[client] - context = @clients[client] + # by not catching any exception here, this will kill the connection + # if for some reason the client is trying to send to a session that it doesn't + # belong to + session = access_jam_session?(session_id, context.user) - # by not catching any exception here, this will kill the connection - # if for some reason the client is trying to send to a session that it doesn't - # belong to - access_jam_session?(session, context.user) + @log.debug "publishing to session #{session}" + # put it on the topic exchange for sessions + @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}") + end - # put it on the topic exchange for sessions - @sessions.publish(client_msg.to_s, :routing_key => session) - end + def handle_user_directed(user, client_msg, client) - def handle_user_directed(user, client_msg, client) - type, inner_msg = extract_inner_message(client_msg) - - end + raise SessionError, 'not implemented' + end + end end diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index 968ccc1e3..470eaecb9 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -14,19 +14,19 @@ module JamWebsockets host = "0.0.0.0" port = options[:port] - @log.debug "starting server #{host}:#{port}" + @log.info "starting server #{host}:#{port}" @router.start # if you don't do this, the app won't exit unless you kill -9 at_exit do - @log.debug "cleaning up server" + @log.info "cleaning up server" @router.cleanup end EventMachine.run { - EventMachine::WebSocket.start(:host => "0.0.0.0", :port => options[:port], :debug => options[:debug]) do |ws| - @log.debug "new client #{ws}" + EventMachine::WebSocket.start(:host => "0.0.0.0", :port => options[:port], :debug => options[:emwebsocket_debug]) do |ws| + @log.info "new client #{ws}" @router.new_client(ws) end } diff --git a/lib/jam_websockets/session_error.rb b/lib/jam_websockets/session_error.rb new file mode 100644 index 000000000..4d8a82166 --- /dev/null +++ b/lib/jam_websockets/session_error.rb @@ -0,0 +1,4 @@ +class SessionError < Exception + +end + diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 2d6733a80..55af3d486 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -1,9 +1,79 @@ require 'spec_helper' require 'thread' + +LoginClient = Class.new do + attr_accessor :onmsgblock, :onopenblock + + def initiaize() + + end + + def onopen(&block) + @onopenblock = block + end + + def onmessage(&block) + @onmsgblock = block + end + + def close(&block) + @oncloseblock = block + end + + def close_websocket() + + end + + def send(msg) + puts msg + end + + def request() + return { "origin" => "1.1.1.1"} + end + +end + + +# does a login and returns client +def login(router, user, password) + + message_factory = MessageFactory.new + client = LoginClient.new + + login_ack = message_factory.login_ack("1.1.1.1") + + client.should_receive(:send).with(login_ack.to_s) + client.should_receive(:onclose) + client.should_receive(:onerror) + client.should_receive(:request).and_return({"origin" => "1.1.1.1"}) + + @router.new_client(client) + client.onopenblock.call + + # create a login message, and pass it into the router via onmsgblock.call + login = message_factory.login_with_user_pass(user.email, password) + + # first log in + client.onmsgblock.call login.to_s + + # then join jam session + return client +end + +def login_jam_session(router, client, jam_session) + message_factory = MessageFactory.new + login_jam_session = message_factory.login_jam_session(jam_session.id) + login_ack = message_factory.login_jam_session_ack(false, nil); + client.should_receive(:send).with(login_ack.to_s) + client.onmsgblock.call login_jam_session.to_s +end + + describe Router do - message_factory = MessageFactory.new + message_factory = MessageFactory.new before do @@ -35,6 +105,25 @@ describe Router do end end + describe "topic routing helpers" do + it "create and delete user lookup set" do + user = double(User) + user.should_receive(:id).any_number_of_times.and_return("1") + client = double("client") + context = ClientContext.new(user, client) + + @router.user_context_lookup.length.should == 0 + + @router.add_user(context) + + @router.user_context_lookup.length.should == 1 + + @router.remove_user(context) + + @router.user_context_lookup.length.should == 0 + end + end + describe "login" do it "should not allow login of bogus user", :mq => true do @@ -54,17 +143,19 @@ describe Router do @onmsgblock = block end - def close() + def close_websocket() + end + def close() end end client = TestClient.new - error_msg = message_factory.server_generic_error("invalid login") + error_msg = message_factory.server_rejection_error("invalid login") client.should_receive(:send).with(error_msg.to_s) - client.should_receive(:close) + client.should_receive(:close_websocket) client.should_receive(:onclose) client.should_receive(:onerror) @@ -83,47 +174,7 @@ describe Router do :password => "foobar", :password_confirmation => "foobar") @user.save - - TestClient = Class.new do - - attr_accessor :onmsgblock, :onopenblock, :oncloseblock - - def initiaize() - - end - - def onopen(&block) - @onopenblock = block - end - - def onmessage(&block) - @onmsgblock = block - end - - def close(&block) - @oncloseblock = block - end - end - - client = TestClient.new - - login_ack = message_factory.login_ack("1.1.1.1") - - client.should_receive(:send).with(login_ack.to_s) - client.should_receive(:close).exactly(0).times # close would occur on error, but this is good path - client.should_receive(:onclose) - client.should_receive(:onerror) - client.should_receive(:ip).and_return("1.1.1.1") - - @router.new_client(client) - client.onopenblock.call - - # create a login message, and pass it into the router via onmsgblock.call - login = message_factory.login_with_user_pass("user@example.com", "foobar") - - # attempt to log in, causing chain of events - client.onmsgblock.call login.to_s - + client1 = login(@router, @user, "foobar") end @@ -140,52 +191,35 @@ describe Router do # make a jam_session and define two members - - TestClient = Class.new do - - attr_accessor :onmsgblock, :onopenblock - - def initiaize() - - end - - def onopen(&block) - @onopenblock = block - end - - def onmessage(&block) - @onmsgblock = block - end - - def close(&block) - @oncloseblock = block - end - end - - client = TestClient.new - - login_ack = message_factory.login_ack("1.1.1.1") - - client.should_receive(:send).with(login_ack.to_s) - #client.should_receive(:close) - client.should_receive(:onclose) - client.should_receive(:onerror) - client.should_receive(:ip).and_return("1.1.1.1") - - @router.new_client(client) - client.onopenblock.call - - # create a login message, and pass it into the router via onmsgblock.call - login = message_factory.login_with_user_pass(user1.email, "foobar") - - # first log in - client.onmsgblock.call login.to_s - - # then join jam session - login_jam_session = message_factory.login_jam_session(jam_session.id) - client.onmsgblock.call login_jam_session.to_s - + # create client 1, log him in, and log him in to jam session + client1 = login(@router, user1, "foobar") + login_jam_session(@router, client1, jam_session) end + + it "should allow two valid subscribers to communicate with session-directed messages", :mq => true do + + EventMachine.run do + user1 = FactoryGirl.create(:user) # in the jam session + user2 = FactoryGirl.create(:user) # in the jam session + + jam_session = FactoryGirl.create(:jam_session, :user => user1) + + jam_session_member1 = FactoryGirl.create(:jam_session_member, :user => user1, :jam_session => jam_session) + jam_session_member2 = FactoryGirl.create(:jam_session_member, :user => user2, :jam_session => jam_session) + + # make a jam_session and define two members + + + # create client 1, log him in, and log him in to jam session + client1 = login(@router, user1, "foobar") + login_jam_session(@router, client1, jam_session) + + client2 = login(@router, user2, "foobar") + login_jam_session(@router, client2, jam_session) + EM.stop + end + end + end end From 59664caa393d869d75d631c13888231e4117a150 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 26 Aug 2012 14:35:50 -0500 Subject: [PATCH 05/98] * using workspace env var instead of hard-coded paths --- Gemfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 987c4a2b0..610aee4f4 100644 --- a/Gemfile +++ b/Gemfile @@ -7,11 +7,11 @@ gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'jruby-openssl' gem 'ruby-protocol-buffers', '1.2.2' -gem 'pg_migrate','0.1.5' #:path => '~/workspace/pg_migrate_ruby' #'0.1.4' -gem 'jam_db', :path => '~/workspace/jam-db/target/ruby_package' -gem 'jam_ruby', :path => '~/workspace/jam-ruby' -gem 'jampb', :path => '~/workspace/jam-pb/target/ruby/jampb' -gem 'em-websocket', :path=> '~/workspace/em-websocket' +gem 'pg_migrate','0.1.5' #:path => "#{workspace}/pg_migrate_ruby" +gem 'jam_db', :path => "#{workspace}/jam-db/target/ruby_package" +gem 'jam_ruby', :path => "#{workspace}/jam-ruby" +gem 'jampb', :path => "#{workspace}/jam-pb/target/ruby/jampb" +gem 'em-websocket', :path=> "#{workspace}/em-websocket-jam" gem 'hot_bunnies', '1.3.8' gem 'activerecord', '3.2.7' gem 'logging' From 484b5f7496dc84d0eaa40db84087a19bfd55de52 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 26 Aug 2012 22:00:03 -0500 Subject: [PATCH 06/98] * all messages are finally binary encoded --- bin/websocket_gateway | 2 +- lib/jam_websockets/router.rb | 32 ++++++++++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/bin/websocket_gateway b/bin/websocket_gateway index 8087fdb46..789c35f1d 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -22,4 +22,4 @@ end Logging.logger.root.appenders = Logging.appenders.stdout ActiveRecord::Base.establish_connection(db_config) -Server.new.run :port => config["port"], :debug => true# config["debug"] +Server.new.run :port => config["port"], :emwebsocket_debug => config["emwebsocket_debug"] diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 5150fa918..de5b54770 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -129,9 +129,9 @@ module JamWebsockets @log.debug "received user-directed message for session: #{user_id}" contexts.each do |context| - @log.debug "sending user message to #{context}" EM.schedule do - context.client.instance_variable_get(:@handler).send_frame(:binary, msg) + @log.debug "sending user message to #{context}" + send_to_client(context.client, msg) end end end @@ -161,11 +161,9 @@ module JamWebsockets @log.debug "received session-directed message for session: #{session_id}" contexts.each do |context| - @log.debug "sending session message to #{context}" EM.schedule do - @log.debug "ONTUHNOTEHU" - context.client.instance_variable_get(:@handler).send_frame(:binary, msg) - @log.debug "gross" + @log.debug "sending session message to #{context}" + send_to_client(context.client, msg) end end end @@ -177,6 +175,11 @@ module JamWebsockets end end + def send_to_client(client, msg) + # this is so odd that this is necessary. but searching through the source code... it's all I could find in em-websocket + client.instance_variable_get(:@handler).send_frame(:binary, msg) + end + def cleanup() # shutdown topic listeners and mq connection begin @@ -257,7 +260,8 @@ module JamWebsockets @log.info "ending client session deliberately due to malformed client behavior. reason=#{e}" begin # wrap the message up and send it down - client.send(@message_factory.server_rejection_error(e.to_s).to_s) + error_msg = @message_factory.server_rejection_error(e.to_s).to_s + send_to_client(client, error_msg) ensure client.close_websocket cleanup_client(client) @@ -268,7 +272,8 @@ module JamWebsockets begin # wrap the message up and send it down - client.send(@message_factory.server_generic_error(e.to_s).to_s) + error_msg = @message_factory.server_generic_error(e.to_s).to_s + send_to_client(client, error_msg) ensure client.close_websocket cleanup_client(client) @@ -374,7 +379,8 @@ module JamWebsockets @log.debug "user #{user.email} logged in" # respond with LOGIN_ACK to let client know it was successful - client.send(@message_factory.login_ack(client.request["origin"]).to_s) + login_ack = @message_factory.login_ack(client.request["origin"]).to_s + send_to_client(client, login_ack) # remove from pending_queue @semaphore.synchronize do @@ -415,13 +421,15 @@ module JamWebsockets rescue => e # send back a failure ack and bail @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" - client.send(@message_factory.login_jam_session_ack(true, e.to_s).to_s) + login_jam_session = @message_factory.login_jam_session_ack(true, e.to_s).to_s + send_to_client(client, login_jam_session) return end # respond with LOGIN_JAM_SESSION_ACK to let client know it was successful - client.send(@message_factory.login_jam_session_ack(false, nil).to_s) - + login_jam_session = @message_factory.login_jam_session_ack(false, nil).to_s + send_to_client(client, login_jam_session) + # send 'new client' message to other members in the session handle_session_directed(session_id, @message_factory.user_joined_jam_session(context.user.id, context.user.name), From b65ccd60600f838fef02a068cc80d166bf05ecaa Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 27 Aug 2012 08:56:02 -0500 Subject: [PATCH 07/98] * fixing tests made for switch all send messages to binary. tests broke because of some mocks --- Gemfile | 4 ++++ lib/jam_websockets/router.rb | 5 ++++- spec/jam_websockets/router_spec.rb | 16 ++++++++-------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index 610aee4f4..a3221afa9 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,10 @@ gem 'logging' gem 'activerecord-jdbc-adapter' gem 'activerecord-jdbcpostgresql-adapter' +group :development do + gem 'pry' +end + group :test do gem 'cucumber' gem 'rspec' diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index de5b54770..3eefb729e 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -1,3 +1,4 @@ +require 'pry' require 'set' require 'hot_bunnies' require 'thread' @@ -379,7 +380,9 @@ module JamWebsockets @log.debug "user #{user.email} logged in" # respond with LOGIN_ACK to let client know it was successful - login_ack = @message_factory.login_ack(client.request["origin"]).to_s + #binding.pry + remote_port, remote_ip = Socket.unpack_sockaddr_in(client.get_peername) + login_ack = @message_factory.login_ack(remote_ip).to_s send_to_client(client, login_ack) # remove from pending_queue diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 55af3d486..e0fc699a2 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' require 'thread' - LoginClient = Class.new do attr_accessor :onmsgblock, :onopenblock @@ -29,8 +28,8 @@ LoginClient = Class.new do puts msg end - def request() - return { "origin" => "1.1.1.1"} + def get_peername + return "\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" # 37643, "localhost" end end @@ -42,12 +41,12 @@ def login(router, user, password) message_factory = MessageFactory.new client = LoginClient.new - login_ack = message_factory.login_ack("1.1.1.1") + login_ack = message_factory.login_ack("127.0.0.1") - client.should_receive(:send).with(login_ack.to_s) + router.should_receive(:send_to_client).with(client, login_ack.to_s) client.should_receive(:onclose) client.should_receive(:onerror) - client.should_receive(:request).and_return({"origin" => "1.1.1.1"}) + client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") @router.new_client(client) client.onopenblock.call @@ -66,7 +65,7 @@ def login_jam_session(router, client, jam_session) message_factory = MessageFactory.new login_jam_session = message_factory.login_jam_session(jam_session.id) login_ack = message_factory.login_jam_session_ack(false, nil); - client.should_receive(:send).with(login_ack.to_s) + router.should_receive(:send_to_client).with(client, login_ack.to_s) client.onmsgblock.call login_jam_session.to_s end @@ -154,11 +153,12 @@ describe Router do error_msg = message_factory.server_rejection_error("invalid login") - client.should_receive(:send).with(error_msg.to_s) + @router.should_receive(:send_to_client).with(client, error_msg.to_s) client.should_receive(:close_websocket) client.should_receive(:onclose) client.should_receive(:onerror) + @router.new_client(client) client.onopenblock.call From 5b44da8ef248cd6254d4227285ea6c93bccded3f Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 2 Sep 2012 21:45:18 -0500 Subject: [PATCH 08/98] * adding partially implemented json support to gateway to resume work on other comp (yes, master branch abuse) --- lib/jam_websockets.rb | 2 +- lib/jam_websockets/message.rb | 45 +++++++++++++++++++++++++++++++++++ lib/jam_websockets/router.rb | 20 ++++++++++++++-- 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 lib/jam_websockets/message.rb diff --git a/lib/jam_websockets.rb b/lib/jam_websockets.rb index b09be5a42..a2e24ee9e 100644 --- a/lib/jam_websockets.rb +++ b/lib/jam_websockets.rb @@ -3,11 +3,11 @@ require "jam_ruby" require "jam_websockets/version" require "jam_websockets/session_error" require "jam_websockets/client_context" +require "jam_websockets/message" require "jam_websockets/router" require "jam_websockets/server" include JamRuby - module JamWebsockets # Your code goes here... end diff --git a/lib/jam_websockets/message.rb b/lib/jam_websockets/message.rb new file mode 100644 index 000000000..edfb31482 --- /dev/null +++ b/lib/jam_websockets/message.rb @@ -0,0 +1,45 @@ +require 'json' +require 'protocol_buffers' +require 'protocol_buffers/compiler' + +class ProtocolBuffers::Message + + def to_json(*args) + hash = {'json_class' => self.class.name} + + # simpler version, includes all fields in the output, using the default + # values if unset. also includes empty repeated fields as empty arrays. + # fields.each do |tag, field| + # hash[field.name] = value_for_tag(field.tag) + # end + + # prettier output, only includes non-empty repeated fields and set fields + fields.each do |tag, field| + if field.repeated? + value = value_for_tag(field.tag) + hash[field.name] = value unless value.empty? + else + hash[field.name] = value_for_tag(field.tag) if value_for_tag?(field.tag) + end + end + hash.to_json(*args) + end + + def self.json_create(hash) + hash.delete('json_class') + + # initialize takes a hash of { attribute_name => value } so you can just + # pass the hash into the constructor. but we're supposed to be showing off + # reflection, here. plus, that raises an exception if there is an unknown + # key in the hash. + # new(hash) + + message = new + fields.each do |tag, field| + if value = hash[field.name.to_s] + message.set_value_for_tag(field.tag, value) + end + end + message + end +end diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 3eefb729e..4f2512231 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -2,6 +2,7 @@ require 'pry' require 'set' require 'hot_bunnies' require 'thread' +require 'json' import java.util.concurrent.Executors @@ -227,8 +228,14 @@ module JamWebsockets @pending_clients.add(client) end + is_json = false + client.onopen { + #binding.pry @log.debug "client connected #{client}" + + p client.request["query"] + is_json = !!client.request["query"]["json"] } client.onclose { @@ -254,9 +261,18 @@ module JamWebsockets # TODO: set a max message size before we put it through PB? # TODO: rate limit? + begin - pb_msg = Jampb::ClientMessage.parse(msg.to_s) - self.route(pb_msg, client) + if is_json +#{"type":100, "target":"server", "Login" : {"username":"hi"}} + parse = JSON.parse(msg) + p parse + pb_msg = Jampb::ClientMessage.json_create(parse) + p pb_msg + else + pb_msg = Jampb::ClientMessage.parse(msg.to_s) + self.route(pb_msg, client) + end rescue SessionError => e @log.info "ending client session deliberately due to malformed client behavior. reason=#{e}" begin From e64f3a4d0c03234245568dd4ecbf902781126e2f Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 3 Sep 2012 20:22:46 -0500 Subject: [PATCH 09/98] * fully supporting json concurrently with protocol buffers. specify ?pb=true to web URI --- bin/websocket_gateway | 12 +++- lib/jam_websockets/message.rb | 38 ++++++++++-- lib/jam_websockets/router.rb | 92 +++++++++++++++++++----------- spec/jam_websockets/router_spec.rb | 17 +++--- 4 files changed, 112 insertions(+), 47 deletions(-) diff --git a/bin/websocket_gateway b/bin/websocket_gateway index 789c35f1d..480ca4f1a 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -9,9 +9,17 @@ include JamWebsockets jamenv = ENV['JAMENV'] jamenv ||= 'development' -config = YAML::load(File.open('config/application.yml'))[jamenv] -db_config = YAML::load(File.open('config/database.yml'))[jamenv] +bin_dir = File.expand_path(File.dirname(__FILE__)) +app_config_file = File.join(bin_dir, '..', 'config', 'application.yml') +db_config_file = File.join(bin_dir, '..', 'config', 'database.yml') + +# limit execution in base dir of project +#app_config_file = File.join('config', 'application.yml') +#db_config_file = File.join('config', 'database.yml') + +config = YAML::load(File.open(app_config_file))[jamenv] +db_config = YAML::load(File.open(db_config_file))[jamenv] if config["verbose"] Logging.logger.root.level = :debug diff --git a/lib/jam_websockets/message.rb b/lib/jam_websockets/message.rb index edfb31482..91c877a8f 100644 --- a/lib/jam_websockets/message.rb +++ b/lib/jam_websockets/message.rb @@ -5,7 +5,14 @@ require 'protocol_buffers/compiler' class ProtocolBuffers::Message def to_json(*args) - hash = {'json_class' => self.class.name} + + json = to_json_ready_object() + + json.to_json(*args) + end + + def to_json_ready_object() + hash = {} # simpler version, includes all fields in the output, using the default # values if unset. also includes empty repeated fields as empty arrays. @@ -19,15 +26,26 @@ class ProtocolBuffers::Message value = value_for_tag(field.tag) hash[field.name] = value unless value.empty? else - hash[field.name] = value_for_tag(field.tag) if value_for_tag?(field.tag) + if value_for_tag?(field.tag) + value = value_for_tag(field.tag) + if field.instance_of? ProtocolBuffers::Field::EnumField # if value is enum, resolve string value as ruby const + hash[field.name] = field.value_to_name[value] + else + proxy_class = field.instance_variable_get(:@proxy_class) + if proxy_class.nil? + hash[field.name] = value + else + hash[field.name] = value.to_json_ready_object + end + end + end end end - hash.to_json(*args) + + return hash end def self.json_create(hash) - hash.delete('json_class') - # initialize takes a hash of { attribute_name => value } so you can just # pass the hash into the constructor. but we're supposed to be showing off # reflection, here. plus, that raises an exception if there is an unknown @@ -37,7 +55,15 @@ class ProtocolBuffers::Message message = new fields.each do |tag, field| if value = hash[field.name.to_s] - message.set_value_for_tag(field.tag, value) + if value.instance_of? Hash # if the value is a Hash, descend down into PB hierachy + inner_class = field.instance_variable_get(:@proxy_class) + value = inner_class.json_create(value) + message.set_value_for_tag(field.tag, value) + elsif field.instance_of? ProtocolBuffers::Field::EnumField # if value is enum, resolve string value as ruby const + message.set_value_for_tag(field.tag, field.instance_variable_get(:@proxy_enum).const_get(value)) + else + message.set_value_for_tag(field.tag, value) + end end end message diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 4f2512231..83e29d16f 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -3,12 +3,24 @@ require 'set' require 'hot_bunnies' require 'thread' require 'json' +require 'eventmachine' import java.util.concurrent.Executors include Jampb +# add new field to client connection +module EventMachine +module WebSocket + class Connection < EventMachine::Connection + attr_accessor :encode_json + end +end +end + module JamWebsockets + + class Router attr_accessor :user_context_lookup, :session_context_lookup @@ -130,6 +142,7 @@ module JamWebsockets @log.debug "received user-directed message for session: #{user_id}" + msg = Jampb::ClientMessage.parse(msg) contexts.each do |context| EM.schedule do @log.debug "sending user message to #{context}" @@ -162,6 +175,7 @@ module JamWebsockets @log.debug "received session-directed message for session: #{session_id}" + msg = Jampb::ClientMessage.parse(msg) contexts.each do |context| EM.schedule do @log.debug "sending session message to #{context}" @@ -178,8 +192,12 @@ module JamWebsockets end def send_to_client(client, msg) - # this is so odd that this is necessary. but searching through the source code... it's all I could find in em-websocket - client.instance_variable_get(:@handler).send_frame(:binary, msg) + if client.encode_json + client.send(msg.to_json.to_s) + else + # this is so odd that this is necessary from an API perspective. but searching through the source code... it's all I could find in em-websocket for allowing a binary message to be sent + client.instance_variable_get(:@handler).send_frame(:binary, msg.to_s) + end end def cleanup() @@ -228,15 +246,21 @@ module JamWebsockets @pending_clients.add(client) end - is_json = false + # default to using json instead of pb + client.encode_json = true client.onopen { #binding.pry @log.debug "client connected #{client}" + + # check for '?pb' or '?pb=true' in url query parameters + query_pb = client.request["query"]["pb"] - p client.request["query"] - is_json = !!client.request["query"]["json"] - } + if !query_pb.nil? && (query_pb == "" || query_pb == "true") + client.encode_json = false + end + + } client.onclose { @log.debug "Connection closed" @@ -263,12 +287,11 @@ module JamWebsockets begin - if is_json -#{"type":100, "target":"server", "Login" : {"username":"hi"}} + if client.encode_json + #example: {"type":"LOGIN", "target":"server", "login" : {"username":"hi"}} parse = JSON.parse(msg) - p parse pb_msg = Jampb::ClientMessage.json_create(parse) - p pb_msg + self.route(pb_msg, client) else pb_msg = Jampb::ClientMessage.parse(msg.to_s) self.route(pb_msg, client) @@ -277,7 +300,7 @@ module JamWebsockets @log.info "ending client session deliberately due to malformed client behavior. reason=#{e}" begin # wrap the message up and send it down - error_msg = @message_factory.server_rejection_error(e.to_s).to_s + error_msg = @message_factory.server_rejection_error(e.to_s) send_to_client(client, error_msg) ensure client.close_websocket @@ -289,7 +312,7 @@ module JamWebsockets begin # wrap the message up and send it down - error_msg = @message_factory.server_generic_error(e.to_s).to_s + error_msg = @message_factory.server_generic_error(e.to_s) send_to_client(client, error_msg) ensure client.close_websocket @@ -385,9 +408,10 @@ module JamWebsockets end def handle_login(login, client) - username = login.username - token = login.token - password = login.password + + username = login.username if login.value_for_tag(1) + password = login.password if login.value_for_tag(2) + token = login.token if login.value_for_tag(3) user = valid_login(username, password, token) @@ -398,7 +422,7 @@ module JamWebsockets # respond with LOGIN_ACK to let client know it was successful #binding.pry remote_port, remote_ip = Socket.unpack_sockaddr_in(client.get_peername) - login_ack = @message_factory.login_ack(remote_ip).to_s + login_ack = @message_factory.login_ack(remote_ip) send_to_client(client, login_ack) # remove from pending_queue @@ -440,13 +464,13 @@ module JamWebsockets rescue => e # send back a failure ack and bail @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" - login_jam_session = @message_factory.login_jam_session_ack(true, e.to_s).to_s + login_jam_session = @message_factory.login_jam_session_ack(true, e.to_s) send_to_client(client, login_jam_session) return end # respond with LOGIN_JAM_SESSION_ACK to let client know it was successful - login_jam_session = @message_factory.login_jam_session_ack(false, nil).to_s + login_jam_session = @message_factory.login_jam_session_ack(false, nil) send_to_client(client, login_jam_session) # send 'new client' message to other members in the session @@ -464,18 +488,8 @@ module JamWebsockets def valid_login(username, password, token) - if !username.nil? and !password.nil? - # attempt login with username and password - user = User.find_by_email(username) - - if !user.nil? && user.authenticate(password) - @log.debug "#{username} login via password" - return user - else - @log.debug "#{username} login failure" - return nil - end - elsif !token.nil? + if !token.nil? && token != '' + @log.debug "logging in via token" # attempt login with token user = User.find_by_remember_token(token) @@ -483,10 +497,24 @@ module JamWebsockets @log.debug "no user found with token" return false else - @log.debug "#{username} login via token" + @log.debug "#{user} login via token" + return user + end + + elsif !username.nil? and !password.nil? + + @log.debug "logging in via user/pass '#{username}' '#{password}'" + # attempt login with username and password + user = User.find_by_email(username) + + if !user.nil? && user.authenticate(password) + @log.debug "#{user} login via password" + return user + else + @log.debug "#{username} login failure" return nil end - else + else raise SessionError, 'no login data was found in Login message' end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index e0fc699a2..529cdea78 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require 'thread' LoginClient = Class.new do - attr_accessor :onmsgblock, :onopenblock + attr_accessor :onmsgblock, :onopenblock, :encode_json def initiaize() @@ -43,9 +43,10 @@ def login(router, user, password) login_ack = message_factory.login_ack("127.0.0.1") - router.should_receive(:send_to_client).with(client, login_ack.to_s) + router.should_receive(:send_to_client).with(client, login_ack) client.should_receive(:onclose) client.should_receive(:onerror) + client.should_receive(:request).and_return({ "query" => { "pb" => "true" } }) client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") @router.new_client(client) @@ -65,7 +66,7 @@ def login_jam_session(router, client, jam_session) message_factory = MessageFactory.new login_jam_session = message_factory.login_jam_session(jam_session.id) login_ack = message_factory.login_jam_session_ack(false, nil); - router.should_receive(:send_to_client).with(client, login_ack.to_s) + router.should_receive(:send_to_client).with(client, login_ack) client.onmsgblock.call login_jam_session.to_s end @@ -98,6 +99,7 @@ describe Router do client.should_receive(:onclose) client.should_receive(:onerror) client.should_receive(:onmessage) + client.should_receive(:encode_json=) @router.new_client(client) @@ -128,7 +130,7 @@ describe Router do it "should not allow login of bogus user", :mq => true do TestClient = Class.new do - attr_accessor :onmsgblock, :onopenblock + attr_accessor :onmsgblock, :onopenblock, :encode_json def initiaize() @@ -153,10 +155,11 @@ describe Router do error_msg = message_factory.server_rejection_error("invalid login") - @router.should_receive(:send_to_client).with(client, error_msg.to_s) + @router.should_receive(:send_to_client).with(client, error_msg) client.should_receive(:close_websocket) client.should_receive(:onclose) client.should_receive(:onerror) + client.should_receive(:request).and_return({ "query" => { "pb" => "true" } }) @router.new_client(client) @@ -184,7 +187,7 @@ describe Router do user2 = FactoryGirl.create(:user) # in the jam session user3 = FactoryGirl.create(:user) # not in the jam session - jam_session = FactoryGirl.create(:jam_session, :user => user1) + jam_session = FactoryGirl.create(:jam_session, :creator => user1) jam_session_member1 = FactoryGirl.create(:jam_session_member, :user => user1, :jam_session => jam_session) jam_session_member2 = FactoryGirl.create(:jam_session_member, :user => user2, :jam_session => jam_session) @@ -202,7 +205,7 @@ describe Router do user1 = FactoryGirl.create(:user) # in the jam session user2 = FactoryGirl.create(:user) # in the jam session - jam_session = FactoryGirl.create(:jam_session, :user => user1) + jam_session = FactoryGirl.create(:jam_session, :creator => user1) jam_session_member1 = FactoryGirl.create(:jam_session_member, :user => user1, :jam_session => jam_session) jam_session_member2 = FactoryGirl.create(:jam_session_member, :user => user2, :jam_session => jam_session) From b53b916218dad0196a064ef436778ccb898067db Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 4 Sep 2012 20:23:34 -0500 Subject: [PATCH 10/98] * adding check in session-topic blast to clients--if you originate a message, you shouldn't have it bounce back to you --- lib/jam_websockets/router.rb | 32 +++++++++++++++++++++++------- spec/jam_websockets/router_spec.rb | 5 +++-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 83e29d16f..afac38298 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -13,7 +13,7 @@ include Jampb module EventMachine module WebSocket class Connection < EventMachine::Connection - attr_accessor :encode_json + attr_accessor :encode_json, :client_id # client_id is uuid we give to each client to track them as we like end end end @@ -176,11 +176,25 @@ module JamWebsockets @log.debug "received session-directed message for session: #{session_id}" msg = Jampb::ClientMessage.parse(msg) + + # ok, its very odd to have your own message that you sent bounce back to you. + # In one small favor to the client, we purposefully disallow messages a client + # sent from bouncing back to itself. + properties = headers.properties unless headers.nil? + inner_headers = properties.headers unless properties.nil? + origin_client_id = inner_headers["client_id"] + + # counter-intuitively, even though a string is passed in when you send the header, an (apparently) auto-generated class is sent back which, if you to_s, returns the original value + origin_client_id = origin_client_id.to_s unless origin_client_id.nil? + + @log.debug "message received from client #{origin_client_id}" contexts.each do |context| - EM.schedule do - @log.debug "sending session message to #{context}" - send_to_client(context.client, msg) - end + if context.client.client_id != origin_client_id + EM.schedule do + @log.debug "sending session message to #{context}" + send_to_client(context.client, msg) + end + end end end end @@ -242,6 +256,10 @@ module JamWebsockets def new_client(client) + # give a unique ID to this client. This is used to prevent session messages + # from echoing back to the sender, for instance. + client.client_id = UUIDTools::UUID.random_create.to_s + @semaphore.synchronize do @pending_clients.add(client) end @@ -543,9 +561,9 @@ module JamWebsockets # belong to session = access_jam_session?(session_id, context.user) - @log.debug "publishing to session #{session}" + @log.debug "publishing to session #{session} from client_id #{client.client_id}" # put it on the topic exchange for sessions - @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}") + @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}", :properties => { :headers => { "client_id" => client.client_id } } ) end def handle_user_directed(user, client_msg, client) diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 529cdea78..7c33e23fe 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require 'thread' LoginClient = Class.new do - attr_accessor :onmsgblock, :onopenblock, :encode_json + attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id def initiaize() @@ -100,6 +100,7 @@ describe Router do client.should_receive(:onerror) client.should_receive(:onmessage) client.should_receive(:encode_json=) + client.should_receive(:client_id=) @router.new_client(client) @@ -130,7 +131,7 @@ describe Router do it "should not allow login of bogus user", :mq => true do TestClient = Class.new do - attr_accessor :onmsgblock, :onopenblock, :encode_json + attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id def initiaize() From 7d353d4b7d3cea2c5b28012811227e7f92b80f0e Mon Sep 17 00:00:00 2001 From: "seth@jamkazam.com" Date: Sat, 15 Sep 2012 19:03:05 -0500 Subject: [PATCH 11/98] * moving em-websocket dependency back to standard gem, not our fork because not using subprotocol atm --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index a3221afa9..83c0c4a66 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,7 @@ gem 'pg_migrate','0.1.5' #:path => "#{workspace}/pg_migrate_ruby" gem 'jam_db', :path => "#{workspace}/jam-db/target/ruby_package" gem 'jam_ruby', :path => "#{workspace}/jam-ruby" gem 'jampb', :path => "#{workspace}/jam-pb/target/ruby/jampb" -gem 'em-websocket', :path=> "#{workspace}/em-websocket-jam" +gem 'em-websocket'#, :path=> "#{workspace}/em-websocket-jam" gem 'hot_bunnies', '1.3.8' gem 'activerecord', '3.2.7' gem 'logging' From 2533c6732d0930ed599180d2d8acb548fceb28c5 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 20 Sep 2012 21:08:29 -0500 Subject: [PATCH 12/98] * adding windows specific run script --- Gemfile | 7 +++---- bin/websocket_gateway | 2 +- bin/websocket_gateway_win.sh | 10 ++++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 bin/websocket_gateway_win.sh diff --git a/Gemfile b/Gemfile index 83c0c4a66..682b2ce94 100644 --- a/Gemfile +++ b/Gemfile @@ -2,13 +2,10 @@ source 'https://rubygems.org' # Look for $WORKSPACE, otherwise use "workspace" as dev path. workspace = ENV["WORKSPACE"] || "~/workspace" - gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'jruby-openssl' gem 'ruby-protocol-buffers', '1.2.2' -gem 'pg_migrate','0.1.5' #:path => "#{workspace}/pg_migrate_ruby" -gem 'jam_db', :path => "#{workspace}/jam-db/target/ruby_package" gem 'jam_ruby', :path => "#{workspace}/jam-ruby" gem 'jampb', :path => "#{workspace}/jam-pb/target/ruby/jampb" gem 'em-websocket'#, :path=> "#{workspace}/em-websocket-jam" @@ -24,6 +21,7 @@ group :development do end group :test do + gem 'jam_db', :path => "#{workspace}/jam-db/target/ruby_package" gem 'cucumber' gem 'rspec' gem 'factory_girl' @@ -31,5 +29,6 @@ group :test do gem 'database_cleaner', '0.7.0' gem 'guard', '>= 0.10.0' gem 'guard-rspec', '>= 0.7.3' -gem 'guard-jruby-rspec' + gem 'pg_migrate','0.1.5' #:path => "#{workspace}/pg_migrate_ruby" + gem 'guard-jruby-rspec' end diff --git a/bin/websocket_gateway b/bin/websocket_gateway index 480ca4f1a..b82d6e035 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +#!/usr/bin/env jruby require 'jam_websockets' diff --git a/bin/websocket_gateway_win.sh b/bin/websocket_gateway_win.sh new file mode 100644 index 000000000..b6c741b4e --- /dev/null +++ b/bin/websocket_gateway_win.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# this script is used only in development + +# Look for $WORKSPACE, otherwise use "workspace" as dev path. +if [ -z "$WORKSPACE" ]; then + WORKSPACE="~/workspace" +fi + + +jruby -I"$WORKSPACE/jam-ruby/lib" -I"$WORKSPACE/jam-pb/target/ruby/jampb/lib" -I"$WORKSPACE/jam-db/target/ruby_package/lib" -Ilib bin/websocket_gateway $* From cd723ac964f9afbdb73a8702b9b0c368a99e98ce Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 1 Oct 2012 17:31:47 -0400 Subject: [PATCH 13/98] create connection row on login --- lib/jam_websockets/router.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index afac38298..c8ac46f01 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -443,6 +443,12 @@ module JamWebsockets login_ack = @message_factory.login_ack(remote_ip) send_to_client(client, login_ack) + # log this connection in the database + connection = Connection.new() + connection.user_id = user.id + connection.client_id = client.client_id + connection.save + # remove from pending_queue @semaphore.synchronize do @pending_clients.delete(client) @@ -535,7 +541,7 @@ module JamWebsockets else raise SessionError, 'no login data was found in Login message' end - +:properties => { :headers => { "client_id" => client.client_id } } ) end def access_jam_session?(jam_session_id, user) From 849c6d3b20122f5d513a184c1b917894edb55945 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 1 Oct 2012 17:34:29 -0400 Subject: [PATCH 14/98] formatting fix --- lib/jam_websockets/router.rb | 550 +++++++++++++++++------------------ 1 file changed, 275 insertions(+), 275 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index c8ac46f01..8358c2669 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -23,25 +23,25 @@ module JamWebsockets class Router - attr_accessor :user_context_lookup, :session_context_lookup - + attr_accessor :user_context_lookup, :session_context_lookup + def initialize(options={}) @log = Logging.logger[self] @pending_clients = Set.new # clients that have connected to server, but not logged in. @clients = {} # clients that have logged in - @user_context_lookup = {} # lookup a set of client_contexts by user_id - @session_context_lookup = {} # lookup a set of client_contexts by session_id + @user_context_lookup = {} # lookup a set of client_contexts by user_id + @session_context_lookup = {} # lookup a set of client_contexts by session_id @sessions_exchange = nil @connection = nil @channel = nil @users_exchange = nil @message_factory = JamRuby::MessageFactory.new - @semaphore = Mutex.new - @user_topic = nil - @user_subscription = nil - @session_topic = nil - @session_subscription = nil - @thread_pool = nil + @semaphore = Mutex.new + @user_topic = nil + @user_subscription = nil + @session_topic = nil + @session_subscription = nil + @thread_pool = nil end def start(options = {}) @@ -49,7 +49,7 @@ module JamWebsockets @log.info "startup" begin - @thread_pool = Executors.new_fixed_thread_pool(8) + @thread_pool = Executors.new_fixed_thread_pool(8) @connection = HotBunnies.connect(:host => options[:host], :port => options[:port]) @channel = @connection.create_channel @channel.prefetch = 10 @@ -62,119 +62,119 @@ module JamWebsockets end - def add_user(context) - user_contexts = @user_context_lookup[context.user.id] - if user_contexts.nil? - user_contexts = Set.new - @user_context_lookup[context.user.id] = user_contexts - end + def add_user(context) + user_contexts = @user_context_lookup[context.user.id] + if user_contexts.nil? + user_contexts = Set.new + @user_context_lookup[context.user.id] = user_contexts + end - user_contexts.add(context) - end + user_contexts.add(context) + end - def remove_user(context) - user_contexts = @user_context_lookup[context.user.id] - if user_contexts.nil? - @log.warn "user can not be removed #{context}" - else - # delete the context from set of user contexts - user_contexts.delete(context) + def remove_user(context) + user_contexts = @user_context_lookup[context.user.id] + if user_contexts.nil? + @log.warn "user can not be removed #{context}" + else + # delete the context from set of user contexts + user_contexts.delete(context) - # if last user context, delete entire set (memory leak concern) - if user_contexts.length == 0 - @user_context_lookup.delete(context.user.id) - end - end - end + # if last user context, delete entire set (memory leak concern) + if user_contexts.length == 0 + @user_context_lookup.delete(context.user.id) + end + end + end - def add_session(context) - session_contexts = @session_context_lookup[context.session.id] - if session_contexts.nil? - session_contexts = Set.new - @session_context_lookup[context.session.id] = session_contexts - end + def add_session(context) + session_contexts = @session_context_lookup[context.session.id] + if session_contexts.nil? + session_contexts = Set.new + @session_context_lookup[context.session.id] = session_contexts + end - session_contexts.add(context) - end + session_contexts.add(context) + end - def remove_session(context) - session_contexts = @session_context_lookup[context.session.id] - if session_contexts.nil? - @log.warn "session can not be removed #{context}" - else - # delete the context from set of session contexts - session_contexts.delete(context) + def remove_session(context) + session_contexts = @session_context_lookup[context.session.id] + if session_contexts.nil? + @log.warn "session can not be removed #{context}" + else + # delete the context from set of session contexts + session_contexts.delete(context) - # if last session context, delete entire set (memory leak concern) - if session_contexts.length == 0 - @session_context_lookup.delete(context.session.id) - end + # if last session context, delete entire set (memory leak concern) + if session_contexts.length == 0 + @session_context_lookup.delete(context.session.id) + end - context.session = nil - end - end + context.session = nil + end + end - # register topic for user messages and session messages - def register_topics - @users_exchange = @channel.exchange('users', :type => :topic) + # register topic for user messages and session messages + def register_topics + @users_exchange = @channel.exchange('users', :type => :topic) @sessions_exchange = @channel.exchange('sessions', :type => :topic) - - # create user messaging topic + + # create user messaging topic @user_topic = @channel.queue("", :auto_delete => true) @user_topic.bind(@users_exchange, :routing_key => "user.#") @user_topic.purge # TODO: alert friends - # subscribe for any messages to users - - #@user_subscription = @user_topic.subscribe(:ack => false, :blocking => false, :executor => @threadpool) do |headers, msg| - @user_subscription = @user_topic.subscribe(:ack => false) - @user_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| - begin - routing_key = headers.envelope.routing_key - user_id = routing_key["user.".length..-1] - @sempahore.synchronize do - contexts = @user_context_lookup[user_id] + # subscribe for any messages to users + + #@user_subscription = @user_topic.subscribe(:ack => false, :blocking => false, :executor => @threadpool) do |headers, msg| + @user_subscription = @user_topic.subscribe(:ack => false) + @user_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| + begin + routing_key = headers.envelope.routing_key + user_id = routing_key["user.".length..-1] + @sempahore.synchronize do + contexts = @user_context_lookup[user_id] - unless contexts.nil? - - @log.debug "received user-directed message for session: #{user_id}" - + unless contexts.nil? + + @log.debug "received user-directed message for session: #{user_id}" + msg = Jampb::ClientMessage.parse(msg) - contexts.each do |context| - EM.schedule do - @log.debug "sending user message to #{context}" - send_to_client(context.client, msg) - end - end - end - end + contexts.each do |context| + EM.schedule do + @log.debug "sending user message to #{context}" + send_to_client(context.client, msg) + end + end + end + end - rescue => e - @log.error "unhandled error in messaging to client" - end - end + rescue => e + @log.error "unhandled error in messaging to client" + end + end - @session_topic = @channel.queue("", :auto_delete => true) + @session_topic = @channel.queue("", :auto_delete => true) @session_topic.bind(@sessions_exchange, :routing_key => "session.#") @session_topic.purge # subscribe for any messages to session #@session_subscription = @session_topic.subscribe(:ack => false, :blocking => false) do |headers, msg| @session_subscription = @session_topic.subscribe(:ack => false) - @session_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| - begin - routing_key = headers.envelope.routing_key - session_id = routing_key["session.".length..-1] - @semaphore.synchronize do - contexts = @session_context_lookup[session_id] + @session_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| + begin + routing_key = headers.envelope.routing_key + session_id = routing_key["session.".length..-1] + @semaphore.synchronize do + contexts = @session_context_lookup[session_id] - unless contexts.nil? - - @log.debug "received session-directed message for session: #{session_id}" - + unless contexts.nil? + + @log.debug "received session-directed message for session: #{session_id}" + msg = Jampb::ClientMessage.parse(msg) # ok, its very odd to have your own message that you sent bounce back to you. @@ -188,88 +188,88 @@ module JamWebsockets origin_client_id = origin_client_id.to_s unless origin_client_id.nil? @log.debug "message received from client #{origin_client_id}" - contexts.each do |context| + contexts.each do |context| if context.client.client_id != origin_client_id EM.schedule do @log.debug "sending session message to #{context}" send_to_client(context.client, msg) end end - end - end - end + end + end + end - rescue => e - @log.error "unhandled error in messaging to client" - end + rescue => e + @log.error "unhandled error in messaging to client" + end end - end + end - def send_to_client(client, msg) + def send_to_client(client, msg) if client.encode_json client.send(msg.to_json.to_s) else - # this is so odd that this is necessary from an API perspective. but searching through the source code... it's all I could find in em-websocket for allowing a binary message to be sent - client.instance_variable_get(:@handler).send_frame(:binary, msg.to_s) + # this is so odd that this is necessary from an API perspective. but searching through the source code... it's all I could find in em-websocket for allowing a binary message to be sent + client.instance_variable_get(:@handler).send_frame(:binary, msg.to_s) end - end + end def cleanup() - # shutdown topic listeners and mq connection - begin - if !@user_subscription.nil? && @user_subscription.active? - @log.debug "cleaning up user subscription" - @user_subscription.cancel - @user_subscription.shutdown! - end + # shutdown topic listeners and mq connection + begin + if !@user_subscription.nil? && @user_subscription.active? + @log.debug "cleaning up user subscription" + @user_subscription.cancel + @user_subscription.shutdown! + end - if !@session_subscription.nil? && @session_subscription.active? - @log.debug "cleaning up session subscription" - @session_subscription.cancel - @session_subscription.shutdown! - end + if !@session_subscription.nil? && @session_subscription.active? + @log.debug "cleaning up session subscription" + @session_subscription.cancel + @session_subscription.shutdown! + end - rescue => e - @log.debug "unable to cancel subscription on cleanup: #{e}" - end + rescue => e + @log.debug "unable to cancel subscription on cleanup: #{e}" + end - @thread_pool.shutdown + @thread_pool.shutdown - if !@channel.nil? - @channel.close - end + if !@channel.nil? + @channel.close + end - if !@connection.nil? - @connection.close - end + if !@connection.nil? + @connection.close + end - # tear down each individual client - @clients.each do |client, context| - cleanup_client(client) - end - end + # tear down each individual client + @clients.each do |client, context| + cleanup_client(client) + end + end - def stop - @log.info "shutdown" - cleanup - end + def stop + @log.info "shutdown" + cleanup + end - def new_client(client) + def new_client(client) # give a unique ID to this client. This is used to prevent session messages # from echoing back to the sender, for instance. client.client_id = UUIDTools::UUID.random_create.to_s - @semaphore.synchronize do - @pending_clients.add(client) - end + @semaphore.synchronize do + @pending_clients.add(client) + end # default to using json instead of pb - client.encode_json = true + client.encode_json = true - client.onopen { - #binding.pry - @log.debug "client connected #{client}" + client.onopen { + #binding.pry + @log.debug "client connected #{client}" # check for '?pb' or '?pb=true' in url query parameters query_pb = client.request["query"]["pb"] @@ -280,10 +280,10 @@ module JamWebsockets } - client.onclose { - @log.debug "Connection closed" + client.onclose { + @log.debug "Connection closed" - cleanup_client(client) + cleanup_client(client) } client.onerror { |error| @@ -294,7 +294,7 @@ module JamWebsockets end cleanup_client(client) - client.close_websocket + client.close_websocket } client.onmessage { |msg| @@ -303,37 +303,37 @@ module JamWebsockets # TODO: set a max message size before we put it through PB? # TODO: rate limit? - + begin - if client.encode_json + if client.encode_json #example: {"type":"LOGIN", "target":"server", "login" : {"username":"hi"}} - parse = JSON.parse(msg) - pb_msg = Jampb::ClientMessage.json_create(parse) + parse = JSON.parse(msg) + pb_msg = Jampb::ClientMessage.json_create(parse) self.route(pb_msg, client) - else - pb_msg = Jampb::ClientMessage.parse(msg.to_s) - self.route(pb_msg, client) - end - rescue SessionError => e - @log.info "ending client session deliberately due to malformed client behavior. reason=#{e}" - begin + else + pb_msg = Jampb::ClientMessage.parse(msg.to_s) + self.route(pb_msg, client) + end + rescue SessionError => e + @log.info "ending client session deliberately due to malformed client behavior. reason=#{e}" + begin # wrap the message up and send it down error_msg = @message_factory.server_rejection_error(e.to_s) - send_to_client(client, error_msg) + send_to_client(client, error_msg) ensure - client.close_websocket + client.close_websocket cleanup_client(client) end rescue => e @log.error "ending client session due to server programming or runtime error. reason=#{e.to_s}" - @log.error e - - begin + @log.error e + + begin # wrap the message up and send it down error_msg = @message_factory.server_generic_error(e.to_s) - send_to_client(client, error_msg) + send_to_client(client, error_msg) ensure - client.close_websocket + client.close_websocket cleanup_client(client) end end @@ -342,38 +342,38 @@ module JamWebsockets end - # removes all resources associated with a client + # removes all resources associated with a client def cleanup_client(client) - @semaphore.synchronize do - pending = @pending_clients.delete?(client) + @semaphore.synchronize do + pending = @pending_clients.delete?(client) - if !pending.nil? - @log.debug "cleaning up pending client #{client}" - else - context = @clients.delete(client) + if !pending.nil? + @log.debug "cleaning up pending client #{client}" + else + context = @clients.delete(client) - if !context.nil? - - remove_user(context) + if !context.nil? + + remove_user(context) - if !context.session.nil? - remove_session(context) - end - else - @log.debug "skipping duplicate cleanup attempt of authorized client" - end + if !context.session.nil? + remove_session(context) + end + else + @log.debug "skipping duplicate cleanup attempt of authorized client" + end - end - end + end + end end def route(client_msg, client) - message_type = @message_factory.get_message_type(client_msg) - - raise SessionError, "unknown message type received: #{client_msg.type}" if message_type.nil? + message_type = @message_factory.get_message_type(client_msg) - @log.debug("msg received #{message_type}") + raise SessionError, "unknown message type received: #{client_msg.type}" if message_type.nil? + + @log.debug("msg received #{message_type}") raise SessionError, 'client_msg.target is null' if client_msg.target.nil? @@ -408,9 +408,9 @@ module JamWebsockets handle_login(client_msg.login, client) - elsif client_msg.type == ClientMessage::Type::HEARTBEAT + elsif client_msg.type == ClientMessage::Type::HEARTBEAT - handle_heartbeat(client_msg.heartbeat, client) + handle_heartbeat(client_msg.heartbeat, client) elsif client_msg.type == ClientMessage::Type::LOGIN_JAM_SESSION @@ -438,10 +438,10 @@ module JamWebsockets @log.debug "user #{user.email} logged in" # respond with LOGIN_ACK to let client know it was successful - #binding.pry - remote_port, remote_ip = Socket.unpack_sockaddr_in(client.get_peername) - login_ack = @message_factory.login_ack(remote_ip) - send_to_client(client, login_ack) + #binding.pry + remote_port, remote_ip = Socket.unpack_sockaddr_in(client.get_peername) + login_ack = @message_factory.login_ack(remote_ip) + send_to_client(client, login_ack) # log this connection in the database connection = Connection.new() @@ -450,22 +450,22 @@ module JamWebsockets connection.save # remove from pending_queue - @semaphore.synchronize do - @pending_clients.delete(client) + @semaphore.synchronize do + @pending_clients.delete(client) - # add a tracker for this user - context = ClientContext.new(user, client) - @clients[client] = context - add_user(context) - end + # add a tracker for this user + context = ClientContext.new(user, client) + @clients[client] = context + add_user(context) + end else raise SessionError, 'invalid login' end end - def handle_heartbeat(heartbeat, client) - # todo: manage staleness - end + def handle_heartbeat(heartbeat, client) + # todo: manage staleness + end def handle_join_jam_session(join_jam_session, client) # verify that the current user has the rights to actually join the jam session @@ -475,106 +475,106 @@ module JamWebsockets begin session = access_jam_session?(session_id, context.user) - @log.debug "user #{context} joining new session #{session}" - @semaphore.synchronize do - old_session = context.session - if !old_session.nil? - @log.debug "#{context} is already in session. auto-logging out to join new session." - remove_session(context) - end - context.session = session - add_session(context) - end + @log.debug "user #{context} joining new session #{session}" + @semaphore.synchronize do + old_session = context.session + if !old_session.nil? + @log.debug "#{context} is already in session. auto-logging out to join new session." + remove_session(context) + end + context.session = session + add_session(context) + end rescue => e # send back a failure ack and bail - @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" + @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" login_jam_session = @message_factory.login_jam_session_ack(true, e.to_s) - send_to_client(client, login_jam_session) + send_to_client(client, login_jam_session) return end # respond with LOGIN_JAM_SESSION_ACK to let client know it was successful login_jam_session = @message_factory.login_jam_session_ack(false, nil) - send_to_client(client, login_jam_session) - + send_to_client(client, login_jam_session) + # send 'new client' message to other members in the session handle_session_directed(session_id, - @message_factory.user_joined_jam_session(context.user.id, context.user.name), - client) + @message_factory.user_joined_jam_session(context.user.id, context.user.name), + client) end - def handle_leave_jam_session(leave_jam_session, client) + def handle_leave_jam_session(leave_jam_session, client) - context = @clients[client] + context = @clients[client] - raise SessionError, "unsupported" - end + raise SessionError, "unsupported" + end - def valid_login(username, password, token) + def valid_login(username, password, token) if !token.nil? && token != '' @log.debug "logging in via token" - # attempt login with token - user = User.find_by_remember_token(token) + # attempt login with token + user = User.find_by_remember_token(token) - if user.nil? - @log.debug "no user found with token" - return false - else - @log.debug "#{user} login via token" - return user - end + if user.nil? + @log.debug "no user found with token" + return false + else + @log.debug "#{user} login via token" + return user + end - elsif !username.nil? and !password.nil? + elsif !username.nil? and !password.nil? @log.debug "logging in via user/pass '#{username}' '#{password}'" - # attempt login with username and password - user = User.find_by_email(username) + # attempt login with username and password + user = User.find_by_email(username) - if !user.nil? && user.authenticate(password) - @log.debug "#{user} login via password" - return user - else - @log.debug "#{username} login failure" - return nil - end - else - raise SessionError, 'no login data was found in Login message' - end + if !user.nil? && user.authenticate(password) + @log.debug "#{user} login via password" + return user + else + @log.debug "#{username} login failure" + return nil + end + else + raise SessionError, 'no login data was found in Login message' + end :properties => { :headers => { "client_id" => client.client_id } } ) - end + end - def access_jam_session?(jam_session_id, user) - jam_session = JamSession.find_by_id(jam_session_id) + def access_jam_session?(jam_session_id, user) + jam_session = JamSession.find_by_id(jam_session_id) - if jam_session.nil? - raise SessionError, 'specified session not found' - end + if jam_session.nil? + raise SessionError, 'specified session not found' + end - if !jam_session.access? user - raise SessionError, 'not allowed to join the specified session' - end + if !jam_session.access? user + raise SessionError, 'not allowed to join the specified session' + end - return jam_session - end + return jam_session + end - def handle_session_directed(session_id, client_msg, client) + def handle_session_directed(session_id, client_msg, client) - context = @clients[client] + context = @clients[client] - # by not catching any exception here, this will kill the connection - # if for some reason the client is trying to send to a session that it doesn't - # belong to - session = access_jam_session?(session_id, context.user) + # by not catching any exception here, this will kill the connection + # if for some reason the client is trying to send to a session that it doesn't + # belong to + session = access_jam_session?(session_id, context.user) - @log.debug "publishing to session #{session} from client_id #{client.client_id}" - # put it on the topic exchange for sessions - @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}", :properties => { :headers => { "client_id" => client.client_id } } ) - end + @log.debug "publishing to session #{session} from client_id #{client.client_id}" + # put it on the topic exchange for sessions + @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}", :properties => { :headers => { "client_id" => client.client_id } } ) + end - def handle_user_directed(user, client_msg, client) + def handle_user_directed(user, client_msg, client) - raise SessionError, 'not implemented' - end - end + raise SessionError, 'not implemented' + end + end end From af4a94c8751bcbf05493378daca5f2cd61f26cb0 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 1 Oct 2012 17:36:51 -0400 Subject: [PATCH 15/98] removed copy/paste error --- lib/jam_websockets/router.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 8358c2669..cc5edb92e 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -541,7 +541,6 @@ module JamWebsockets else raise SessionError, 'no login data was found in Login message' end -:properties => { :headers => { "client_id" => client.client_id } } ) end def access_jam_session?(jam_session_id, user) From 8451f8653aa1b1f71a92deb0cf1f8092d8a56e2a Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 2 Oct 2012 01:03:08 -0400 Subject: [PATCH 16/98] user presence development --- lib/jam_websockets/client_context.rb | 12 +++--- lib/jam_websockets/router.rb | 55 ++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/lib/jam_websockets/client_context.rb b/lib/jam_websockets/client_context.rb index eb40fb215..706e578b6 100644 --- a/lib/jam_websockets/client_context.rb +++ b/lib/jam_websockets/client_context.rb @@ -5,14 +5,14 @@ module JamWebsockets def initialize(user, client) @user = user - @client = client - @msg_count = 0 - @session = nil + @client = client + @msg_count = 0 + @session = nil end - def to_s - return "Client[user:#{@user} client:#{@client} msgs:#{@msg_count} session:#{@session}]" - end + def to_s + return "Client[user:#{@user} client:#{@client} msgs:#{@msg_count} session:#{@session}]" + end end end diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index cc5edb92e..efade4a56 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -355,11 +355,15 @@ module JamWebsockets if !context.nil? - remove_user(context) + remove_user(context) - if !context.session.nil? - remove_session(context) - end + # remove this connection from the database + connection = Connection.delete_all "user_id = '#{context.user.id}' AND client_id = '#{context.client.client_id}'" + send_friend_update(user, false) + + if !context.session.nil? + remove_session(context) + end else @log.debug "skipping duplicate cleanup attempt of authorized client" end @@ -443,12 +447,6 @@ module JamWebsockets login_ack = @message_factory.login_ack(remote_ip) send_to_client(client, login_ack) - # log this connection in the database - connection = Connection.new() - connection.user_id = user.id - connection.client_id = client.client_id - connection.save - # remove from pending_queue @semaphore.synchronize do @pending_clients.delete(client) @@ -457,14 +455,49 @@ module JamWebsockets context = ClientContext.new(user, client) @clients[client] = context add_user(context) + + # log this connection in the database + connection = Connection.new(user.id, client.id) + + if connection.save? + send_friend_update(user, true) + end end else raise SessionError, 'invalid login' end end + def send_friend_update(user, online) + unless user.friends.nil? + @log.debug "sending friend update message to friends" + + # create the friend_update message + friend_update = @message_factory.friend_update(user.id, online) + + # send the friend_update to each friend that has active connections + user.friends.each do |friend| + # only send to friends that have active connections + active_connections = @user_context_lookup[friend.id] + unless active_connections.nil? + # send the update to each active connection of this friend + active_connections.each do |context| + EM.schedule do + @log.debug "sending friend update message to #{friend}" + send_to_client(context.client, friend_update) + end + end + end + end + end + end + def handle_heartbeat(heartbeat, client) - # todo: manage staleness + context = @clients[client] + @log.debug "updating timestamp for user #{context}" + connection = Connection.find_by_user_id_and_client_id(context.user.user_id, context.client.client_id) + connection.updated_at = DateTime.now + connection.save end def handle_join_jam_session(join_jam_session, client) From 9a055fd2c282f1a98c1d463b02e891a20484dcfd Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 4 Oct 2012 21:34:10 -0500 Subject: [PATCH 17/98] * updating protocol buffer messages to use 'Music' instead of 'Jam' --- lib/jam_websockets/router.rb | 42 +++++++++++++------------- spec/factories.rb | 8 ++--- spec/jam_websockets/router_spec.rb | 48 +++++++++++++++--------------- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index afac38298..875078498 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -412,13 +412,13 @@ module JamWebsockets handle_heartbeat(client_msg.heartbeat, client) - elsif client_msg.type == ClientMessage::Type::LOGIN_JAM_SESSION + elsif client_msg.type == ClientMessage::Type::LOGIN_MUSIC_SESSION - handle_join_jam_session(client_msg.login_jam_session, client) + handle_join_music_session(client_msg.login_music_session, client) - elsif client_msg.type == ClientMessage::Type::LEAVE_JAM_SESSION + elsif client_msg.type == ClientMessage::Type::LEAVE_MUSIC_SESSION - handle_leave_jam_session(client_msg.leave_jam_session, client) + handle_leave_music_session(client_msg.leave_music_session, client) else raise SessionError, "unknown message type '#{client_msg.type}' for #{client_msg.target}-directed message" @@ -461,14 +461,14 @@ module JamWebsockets # todo: manage staleness end - def handle_join_jam_session(join_jam_session, client) - # verify that the current user has the rights to actually join the jam session + def handle_join_music_session(join_music_session, client) + # verify that the current user has the rights to actually join the music session context = @clients[client] - session_id = join_jam_session.jam_session + session_id = join_music_session.music_session begin - session = access_jam_session?(session_id, context.user) + session = access_music_session?(session_id, context.user) @log.debug "user #{context} joining new session #{session}" @semaphore.synchronize do old_session = context.session @@ -482,22 +482,22 @@ module JamWebsockets rescue => e # send back a failure ack and bail @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" - login_jam_session = @message_factory.login_jam_session_ack(true, e.to_s) - send_to_client(client, login_jam_session) + login_music_session = @message_factory.login_music_session_ack(true, e.to_s) + send_to_client(client, login_music_session) return end - # respond with LOGIN_JAM_SESSION_ACK to let client know it was successful - login_jam_session = @message_factory.login_jam_session_ack(false, nil) - send_to_client(client, login_jam_session) + # respond with LOGIN_MUSIC_SESSION_ACK to let client know it was successful + login_music_session = @message_factory.login_music_session_ack(false, nil) + send_to_client(client, login_music_session) # send 'new client' message to other members in the session handle_session_directed(session_id, - @message_factory.user_joined_jam_session(context.user.id, context.user.name), + @message_factory.user_joined_music_session(context.user.id, context.user.name), client) end - def handle_leave_jam_session(leave_jam_session, client) + def handle_leave_music_session(leave_music_session, client) context = @clients[client] @@ -538,18 +538,18 @@ module JamWebsockets end - def access_jam_session?(jam_session_id, user) - jam_session = JamSession.find_by_id(jam_session_id) + def access_music_session?(music_session_id, user) + music_session = MusicSession.find_by_id(music_session_id) - if jam_session.nil? + if music_session.nil? raise SessionError, 'specified session not found' end - if !jam_session.access? user + if !music_session.access? user raise SessionError, 'not allowed to join the specified session' end - return jam_session + return music_session end def handle_session_directed(session_id, client_msg, client) @@ -559,7 +559,7 @@ module JamWebsockets # by not catching any exception here, this will kill the connection # if for some reason the client is trying to send to a session that it doesn't # belong to - session = access_jam_session?(session_id, context.user) + session = access_music_session?(session_id, context.user) @log.debug "publishing to session #{session} from client_id #{client.client_id}" # put it on the topic exchange for sessions diff --git a/spec/factories.rb b/spec/factories.rb index 37aad9841..a5e6f4c1e 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -10,11 +10,11 @@ FactoryGirl.define do end end - factory :jam_session, :class => JamRuby::JamSession do - sequence(:name) { |n| "Jam Session #{n}" } + factory :music_session, :class => JamRuby::MusicSession do + sequence(:description) { |n| "Jam Session #{n}" } end - factory :jam_session_member, :class => JamRuby::JamSessionMember do - + factory :music_session_client, :class => JamRuby::MusicSessionClient do + ip_address "1.1.1.1" end end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 7c33e23fe..6736ca0e7 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -58,16 +58,16 @@ def login(router, user, password) # first log in client.onmsgblock.call login.to_s - # then join jam session + # then join music session return client end -def login_jam_session(router, client, jam_session) +def login_music_session(router, client, music_session) message_factory = MessageFactory.new - login_jam_session = message_factory.login_jam_session(jam_session.id) - login_ack = message_factory.login_jam_session_ack(false, nil); + login_music_session = message_factory.login_music_session(music_session.id) + login_ack = message_factory.login_music_session_ack(false, nil); router.should_receive(:send_to_client).with(client, login_ack) - client.onmsgblock.call login_jam_session.to_s + client.onmsgblock.call login_music_session.to_s end @@ -182,44 +182,44 @@ describe Router do end - it "should allow jam_session_join of valid user", :mq => true do + it "should allow music_session_join of valid user", :mq => true do - user1 = FactoryGirl.create(:user) # in the jam session - user2 = FactoryGirl.create(:user) # in the jam session - user3 = FactoryGirl.create(:user) # not in the jam session + user1 = FactoryGirl.create(:user) # in the music session + user2 = FactoryGirl.create(:user) # in the music session + user3 = FactoryGirl.create(:user) # not in the music session - jam_session = FactoryGirl.create(:jam_session, :creator => user1) + music_session = FactoryGirl.create(:music_session, :creator => user1) - jam_session_member1 = FactoryGirl.create(:jam_session_member, :user => user1, :jam_session => jam_session) - jam_session_member2 = FactoryGirl.create(:jam_session_member, :user => user2, :jam_session => jam_session) + music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session) + music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session) - # make a jam_session and define two members + # make a music_session and define two members - # create client 1, log him in, and log him in to jam session + # create client 1, log him in, and log him in to music session client1 = login(@router, user1, "foobar") - login_jam_session(@router, client1, jam_session) + login_music_session(@router, client1, music_session) end it "should allow two valid subscribers to communicate with session-directed messages", :mq => true do EventMachine.run do - user1 = FactoryGirl.create(:user) # in the jam session - user2 = FactoryGirl.create(:user) # in the jam session + user1 = FactoryGirl.create(:user) # in the music session + user2 = FactoryGirl.create(:user) # in the music session - jam_session = FactoryGirl.create(:jam_session, :creator => user1) + music_session = FactoryGirl.create(:music_session, :creator => user1) - jam_session_member1 = FactoryGirl.create(:jam_session_member, :user => user1, :jam_session => jam_session) - jam_session_member2 = FactoryGirl.create(:jam_session_member, :user => user2, :jam_session => jam_session) + music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session) + music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session) - # make a jam_session and define two members + # make a music_session and define two members - # create client 1, log him in, and log him in to jam session + # create client 1, log him in, and log him in to music session client1 = login(@router, user1, "foobar") - login_jam_session(@router, client1, jam_session) + login_music_session(@router, client1, music_session) client2 = login(@router, user2, "foobar") - login_jam_session(@router, client2, jam_session) + login_music_session(@router, client2, music_session) EM.stop end end From cbb9508b6b38989aef43277ea5b8f000de13d69e Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 6 Oct 2012 23:38:38 -0400 Subject: [PATCH 18/98] more changes for user presence --- lib/jam_websockets/client_context.rb | 2 +- lib/jam_websockets/router.rb | 267 ++++++++++++++------------- lib/jam_websockets/server.rb | 1 + 3 files changed, 139 insertions(+), 131 deletions(-) diff --git a/lib/jam_websockets/client_context.rb b/lib/jam_websockets/client_context.rb index 706e578b6..46cb87a92 100644 --- a/lib/jam_websockets/client_context.rb +++ b/lib/jam_websockets/client_context.rb @@ -1,4 +1,4 @@ -module JamWebsockets + module JamWebsockets class ClientContext attr_accessor :user, :client, :msg_count, :session diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index efade4a56..0d322db1c 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -20,7 +20,6 @@ end module JamWebsockets - class Router attr_accessor :user_context_lookup, :session_context_lookup @@ -62,75 +61,24 @@ module JamWebsockets end - def add_user(context) - user_contexts = @user_context_lookup[context.user.id] - if user_contexts.nil? - user_contexts = Set.new - @user_context_lookup[context.user.id] = user_contexts - end - - user_contexts.add(context) - end - - def remove_user(context) - user_contexts = @user_context_lookup[context.user.id] - if user_contexts.nil? - @log.warn "user can not be removed #{context}" - else - # delete the context from set of user contexts - user_contexts.delete(context) - - # if last user context, delete entire set (memory leak concern) - if user_contexts.length == 0 - @user_context_lookup.delete(context.user.id) - end - end - end - - def add_session(context) - session_contexts = @session_context_lookup[context.session.id] - if session_contexts.nil? - session_contexts = Set.new - @session_context_lookup[context.session.id] = session_contexts - end - - session_contexts.add(context) - end - - def remove_session(context) - session_contexts = @session_context_lookup[context.session.id] - if session_contexts.nil? - @log.warn "session can not be removed #{context}" - else - # delete the context from set of session contexts - session_contexts.delete(context) - - # if last session context, delete entire set (memory leak concern) - if session_contexts.length == 0 - @session_context_lookup.delete(context.session.id) - end - - context.session = nil - end - end - - # register topic for user messages and session messages def register_topics + + ######################## USER MESSAGING ########################### + + # create user exchange @users_exchange = @channel.exchange('users', :type => :topic) - @sessions_exchange = @channel.exchange('sessions', :type => :topic) # create user messaging topic @user_topic = @channel.queue("", :auto_delete => true) @user_topic.bind(@users_exchange, :routing_key => "user.#") @user_topic.purge - # TODO: alert friends - - # subscribe for any messages to users - + # subscribe for any messages to users #@user_subscription = @user_topic.subscribe(:ack => false, :blocking => false, :executor => @threadpool) do |headers, msg| @user_subscription = @user_topic.subscribe(:ack => false) + + # this code serves as a callback that dequeues messages and processes them @user_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| begin routing_key = headers.envelope.routing_key @@ -140,7 +88,7 @@ module JamWebsockets unless contexts.nil? - @log.debug "received user-directed message for session: #{user_id}" + @log.debug "received user-directed message for user: #{user_id}" msg = Jampb::ClientMessage.parse(msg) contexts.each do |context| @@ -157,6 +105,12 @@ module JamWebsockets end end + ######################## SESSION MESSAGING ########################### + + # create session exchange + @sessions_exchange = @channel.exchange('sessions', :type => :topic) + + # create session messaging topic @session_topic = @channel.queue("", :auto_delete => true) @session_topic.bind(@sessions_exchange, :routing_key => "session.#") @session_topic.purge @@ -164,6 +118,8 @@ module JamWebsockets # subscribe for any messages to session #@session_subscription = @session_topic.subscribe(:ack => false, :blocking => false) do |headers, msg| @session_subscription = @session_topic.subscribe(:ack => false) + + # this code serves as a callback that dequeues messages and processes them @session_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| begin routing_key = headers.envelope.routing_key @@ -174,8 +130,6 @@ module JamWebsockets unless contexts.nil? @log.debug "received session-directed message for session: #{session_id}" - - msg = Jampb::ClientMessage.parse(msg) # ok, its very odd to have your own message that you sent bounce back to you. # In one small favor to the client, we purposefully disallow messages a client @@ -187,7 +141,9 @@ module JamWebsockets # counter-intuitively, even though a string is passed in when you send the header, an (apparently) auto-generated class is sent back which, if you to_s, returns the original value origin_client_id = origin_client_id.to_s unless origin_client_id.nil? - @log.debug "message received from client #{origin_client_id}" + @log.debug "session message received from client #{origin_client_id}" + + msg = Jampb::ClientMessage.parse(msg) contexts.each do |context| if context.client.client_id != origin_client_id EM.schedule do @@ -205,55 +161,6 @@ module JamWebsockets end end - def send_to_client(client, msg) - if client.encode_json - client.send(msg.to_json.to_s) - else - # this is so odd that this is necessary from an API perspective. but searching through the source code... it's all I could find in em-websocket for allowing a binary message to be sent - client.instance_variable_get(:@handler).send_frame(:binary, msg.to_s) - end - end - - def cleanup() - # shutdown topic listeners and mq connection - begin - if !@user_subscription.nil? && @user_subscription.active? - @log.debug "cleaning up user subscription" - @user_subscription.cancel - @user_subscription.shutdown! - end - - if !@session_subscription.nil? && @session_subscription.active? - @log.debug "cleaning up session subscription" - @session_subscription.cancel - @session_subscription.shutdown! - end - - rescue => e - @log.debug "unable to cancel subscription on cleanup: #{e}" - end - - @thread_pool.shutdown - - if !@channel.nil? - @channel.close - end - - if !@connection.nil? - @connection.close - end - - # tear down each individual client - @clients.each do |client, context| - cleanup_client(client) - end - end - - def stop - @log.info "shutdown" - cleanup - end - def new_client(client) # give a unique ID to this client. This is used to prevent session messages @@ -342,6 +249,113 @@ module JamWebsockets end + def add_user(client_context) + user_contexts = @user_context_lookup[client_context.user.id] + + if user_contexts.nil? + user_contexts = Set.new + @user_context_lookup[client_context.user.id] = user_contexts + end + + user_contexts.add(client_context) + end + + def remove_user(client_context) + user_contexts = @user_context_lookup[client_context.user.id] + + if user_contexts.nil? + @log.warn "user can not be removed #{client_context}" + else + # delete the context from set of user contexts + user_contexts.delete(client_context) + + # if last user context, delete entire set (memory leak concern) + if user_contexts.length == 0 + @user_context_lookup.delete(client_context.user.id) + end + + client_context.user = nil + end + end + + def add_session(client_context) + session_contexts = @session_context_lookup[client_context.session.id] + + if session_contexts.nil? + session_contexts = Set.new + @session_context_lookup[client_context.session.id] = session_contexts + end + + session_contexts.add(client_context) + end + + def remove_session(client_context) + session_contexts = @session_context_lookup[client_context.session.id] + + if session_contexts.nil? + @log.warn "session can not be removed #{client_context}" + else + # delete the context from set of session contexts + session_contexts.delete(client_context) + + # if last session context, delete entire set (memory leak concern) + if session_contexts.length == 0 + @session_context_lookup.delete(client_context.session.id) + end + + client_context.session = nil + end + end + + def send_to_client(client, msg) + if client.encode_json + client.send(msg.to_json.to_s) + else + # this is so odd that this is necessary from an API perspective. but searching through the source code... it's all I could find in em-websocket for allowing a binary message to be sent + client.instance_variable_get(:@handler).send_frame(:binary, msg.to_s) + end + end + + def cleanup() + # shutdown topic listeners and mq connection + begin + if !@user_subscription.nil? && @user_subscription.active? + @log.debug "cleaning up user subscription" + @user_subscription.cancel + @user_subscription.shutdown! + end + + if !@session_subscription.nil? && @session_subscription.active? + @log.debug "cleaning up session subscription" + @session_subscription.cancel + @session_subscription.shutdown! + end + + rescue => e + @log.debug "unable to cancel subscription on cleanup: #{e}" + end + + @thread_pool.shutdown + + if !@channel.nil? + @channel.close + end + + if !@connection.nil? + @connection.close + end + + # tear down each individual client + @clients.each do |client, context| + cleanup_client(client) + end + end + + def stop + @log.info "shutdown" + cleanup + end + # removes all resources associated with a client def cleanup_client(client) @@ -358,8 +372,8 @@ module JamWebsockets remove_user(context) # remove this connection from the database - connection = Connection.delete_all "user_id = '#{context.user.id}' AND client_id = '#{context.client.client_id}'" - send_friend_update(user, false) + connection = JamRuby::Connection.delete_all "user_id = '#{context.user.id}' AND client_id = '#{context.client.client_id}'" + send_friend_update(client, user, false) if !context.session.nil? remove_session(context) @@ -447,8 +461,8 @@ module JamWebsockets login_ack = @message_factory.login_ack(remote_ip) send_to_client(client, login_ack) - # remove from pending_queue @semaphore.synchronize do + # remove from pending_queue @pending_clients.delete(client) # add a tracker for this user @@ -457,10 +471,10 @@ module JamWebsockets add_user(context) # log this connection in the database - connection = Connection.new(user.id, client.id) + connection = JamRuby::Connection.new(user.id, client.id) if connection.save? - send_friend_update(user, true) + send_friend_update(client, user, true) end end else @@ -468,26 +482,19 @@ module JamWebsockets end end - def send_friend_update(user, online) + def send_friend_update(client, user, online) unless user.friends.nil? @log.debug "sending friend update message to friends" # create the friend_update message - friend_update = @message_factory.friend_update(user.id, online) + friend_update_msg = @message_factory.friend_update(user.id, online) # send the friend_update to each friend that has active connections user.friends.each do |friend| - # only send to friends that have active connections - active_connections = @user_context_lookup[friend.id] - unless active_connections.nil? - # send the update to each active connection of this friend - active_connections.each do |context| - EM.schedule do - @log.debug "sending friend update message to #{friend}" - send_to_client(context.client, friend_update) - end - end - end + @log.debug "sending friend update message to #{context}" + + # put it on the topic exchange for users + @users_exchange.publish(friend_update_msg.to_s, :routing_key => "user.#{friend.id}") end end end diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index 470eaecb9..3b7f0c9c0 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -1,6 +1,7 @@ require 'em-websocket' module JamWebsockets + class Server def initialize(options={}) From c3616a0cb3ab4db1d55cb6d5c7f85d9b0b8fb751 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 7 Oct 2012 14:01:48 -0400 Subject: [PATCH 19/98] fixed issues found in unit tests --- lib/jam_websockets/router.rb | 30 +++--- lib/jam_websockets/server.rb | 14 +-- spec/jam_websockets/router_spec.rb | 161 +++++++++++++++-------------- 3 files changed, 106 insertions(+), 99 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 17e02d57f..33ddf6361 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -372,8 +372,8 @@ module JamWebsockets remove_user(context) # remove this connection from the database - connection = JamRuby::Connection.delete_all "user_id = '#{context.user.id}' AND client_id = '#{context.client.client_id}'" - send_friend_update(client, user, false) + #JamRuby::Connection.delete_all "user_id = '#{context.user.id}' AND client_id = '#{context.client.client_id}'" + #send_friend_update(context.user, false) if !context.session.nil? remove_session(context) @@ -471,10 +471,11 @@ module JamWebsockets add_user(context) # log this connection in the database - connection = JamRuby::Connection.new(user.id, client.id) + connection = JamRuby::Connection.new(:user => user, :client_id => client.client_id) + @log.debug "Created connection => #{connection.user}, #{connection.client_id}" - if connection.save? - send_friend_update(client, user, true) + if connection.save + send_friend_update(user, true) end end else @@ -482,16 +483,18 @@ module JamWebsockets end end - def send_friend_update(client, user, online) - unless user.friends.nil? - @log.debug "sending friend update message to friends" + def send_friend_update(user, online) + @log.debug "sending friend update for user #{user} online = #{online}" + + if user.friends.exists? + @log.debug "user has friends - sending friend updates" # create the friend_update message friend_update_msg = @message_factory.friend_update(user.id, online) # send the friend_update to each friend that has active connections user.friends.each do |friend| - @log.debug "sending friend update message to #{context}" + @log.debug "sending friend update message to #{friend}" # put it on the topic exchange for users @users_exchange.publish(friend_update_msg.to_s, :routing_key => "user.#{friend.id}") @@ -503,8 +506,11 @@ module JamWebsockets context = @clients[client] @log.debug "updating timestamp for user #{context}" connection = Connection.find_by_user_id_and_client_id(context.user.user_id, context.client.client_id) - connection.updated_at = DateTime.now - connection.save + + unless connection.nil? + connection.updated_at = DateTime.now + connection.save + end end def handle_join_music_session(join_music_session, client) @@ -539,7 +545,7 @@ module JamWebsockets # send 'new client' message to other members in the session handle_session_directed(session_id, - @message_factory.user_joined_jam_session(context.user.id, context.user.name), + @message_factory.user_joined_music_session(context.user.id, context.user.name), client) end diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index 3b7f0c9c0..dcc5a37e3 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -17,17 +17,17 @@ module JamWebsockets @log.info "starting server #{host}:#{port}" - @router.start + @router.start - # if you don't do this, the app won't exit unless you kill -9 - at_exit do - @log.info "cleaning up server" - @router.cleanup - end + # if you don't do this, the app won't exit unless you kill -9 + at_exit do + @log.info "cleaning up server" + @router.cleanup + end EventMachine.run { EventMachine::WebSocket.start(:host => "0.0.0.0", :port => options[:port], :debug => options[:emwebsocket_debug]) do |ws| - @log.info "new client #{ws}" + @log.info "new client #{ws}" @router.new_client(ws) end } diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 6736ca0e7..af05ad0b2 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -2,35 +2,35 @@ require 'spec_helper' require 'thread' LoginClient = Class.new do - attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id + attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id - def initiaize() + def initialize() - end + end - def onopen(&block) - @onopenblock = block - end + def onopen(&block) + @onopenblock = block + end - def onmessage(&block) - @onmsgblock = block - end + def onmessage(&block) + @onmsgblock = block + end - def close(&block) - @oncloseblock = block - end + def close(&block) + @oncloseblock = block + end - def close_websocket() - - end + def close_websocket() + + end - def send(msg) - puts msg - end + def send(msg) + puts msg + end - def get_peername - return "\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" # 37643, "localhost" - end + def get_peername + return "\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" # 37643, "localhost" + end end @@ -38,42 +38,43 @@ end # does a login and returns client def login(router, user, password) - message_factory = MessageFactory.new - client = LoginClient.new + message_factory = MessageFactory.new + client = LoginClient.new - login_ack = message_factory.login_ack("127.0.0.1") + login_ack = message_factory.login_ack("127.0.0.1") - router.should_receive(:send_to_client).with(client, login_ack) - client.should_receive(:onclose) - client.should_receive(:onerror) + router.should_receive(:send_to_client).with(client, login_ack) + client.should_receive(:onclose) + client.should_receive(:onerror) client.should_receive(:request).and_return({ "query" => { "pb" => "true" } }) - client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") + client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") - @router.new_client(client) - client.onopenblock.call + @router.new_client(client) + client.onopenblock.call - # create a login message, and pass it into the router via onmsgblock.call - login = message_factory.login_with_user_pass(user.email, password) + # create a login message, and pass it into the router via onmsgblock.call + login = message_factory.login_with_user_pass(user.email, password) - # first log in - client.onmsgblock.call login.to_s + # first log in + client.onmsgblock.call login.to_s - # then join music session - return client + # then join music session + return client end + def login_music_session(router, client, music_session) - message_factory = MessageFactory.new - login_music_session = message_factory.login_music_session(music_session.id) - login_ack = message_factory.login_music_session_ack(false, nil); - router.should_receive(:send_to_client).with(client, login_ack) - client.onmsgblock.call login_music_session.to_s + message_factory = MessageFactory.new + login_music_session = message_factory.login_music_session(music_session.id) + login_ack = message_factory.login_music_session_ack(false, nil); + router.should_receive(:send_to_client).with(client, login_ack) + client.onmsgblock.call login_music_session.to_s end describe Router do - message_factory = MessageFactory.new + message_factory = MessageFactory.new before do @@ -107,24 +108,24 @@ describe Router do end end - describe "topic routing helpers" do - it "create and delete user lookup set" do - user = double(User) - user.should_receive(:id).any_number_of_times.and_return("1") - client = double("client") - context = ClientContext.new(user, client) + describe "topic routing helpers" do + it "create and delete user lookup set" do + user = double(User) + user.should_receive(:id).any_number_of_times.and_return("1") + client = double("client") + context = ClientContext.new(user, client) - @router.user_context_lookup.length.should == 0 + @router.user_context_lookup.length.should == 0 - @router.add_user(context) - - @router.user_context_lookup.length.should == 1 + @router.add_user(context) + + @router.user_context_lookup.length.should == 1 - @router.remove_user(context) + @router.remove_user(context) - @router.user_context_lookup.length.should == 0 - end - end + @router.user_context_lookup.length.should == 0 + end + end describe "login" do @@ -133,7 +134,7 @@ describe Router do attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id - def initiaize() + def initialize() end @@ -145,8 +146,8 @@ describe Router do @onmsgblock = block end - def close_websocket() - end + def close_websocket() + end def close() end @@ -178,9 +179,10 @@ describe Router do :password => "foobar", :password_confirmation => "foobar") @user.save - client1 = login(@router, @user, "foobar") - end + puts "USER ID = #{@user.id}" + client1 = login(@router, @user, "foobar") + end it "should allow music_session_join of valid user", :mq => true do @@ -195,35 +197,34 @@ describe Router do # make a music_session and define two members - # create client 1, log him in, and log him in to music session - client1 = login(@router, user1, "foobar") - login_music_session(@router, client1, music_session) + # create client 1, log him in, and log him in to music session + client1 = login(@router, user1, "foobar") + login_music_session(@router, client1, music_session) end - it "should allow two valid subscribers to communicate with session-directed messages", :mq => true do + it "should allow two valid subscribers to communicate with session-directed messages", :mq => true do - EventMachine.run do - user1 = FactoryGirl.create(:user) # in the music session - user2 = FactoryGirl.create(:user) # in the music session + EventMachine.run do + user1 = FactoryGirl.create(:user) # in the music session + user2 = FactoryGirl.create(:user) # in the music session - music_session = FactoryGirl.create(:music_session, :creator => user1) + music_session = FactoryGirl.create(:music_session, :creator => user1) - music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session) - music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session) + music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session) + music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session) - # make a music_session and define two members + # make a music_session and define two members - # create client 1, log him in, and log him in to music session - client1 = login(@router, user1, "foobar") - login_music_session(@router, client1, music_session) + # create client 1, log him in, and log him in to music session + client1 = login(@router, user1, "foobar") + login_music_session(@router, client1, music_session) - client2 = login(@router, user2, "foobar") - login_music_session(@router, client2, music_session) - EM.stop - end + client2 = login(@router, user2, "foobar") + login_music_session(@router, client2, music_session) + EM.stop + end end - end end From 76883a0d7e78d195b326a901f17416f5f934592c Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 7 Oct 2012 16:51:43 -0400 Subject: [PATCH 20/98] minor refactor to be consistent with session directed messages --- lib/jam_websockets/router.rb | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 33ddf6361..c07c1ebd0 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -372,8 +372,11 @@ module JamWebsockets remove_user(context) # remove this connection from the database - #JamRuby::Connection.delete_all "user_id = '#{context.user.id}' AND client_id = '#{context.client.client_id}'" - #send_friend_update(context.user, false) + if !context.user.nil? && !context.client.nil? + JamRuby::Connection.delete_all "user_id = '#{context.user.id}' AND client_id = '#{context.client.client_id}'" + end + + send_friend_update(context.user, false, context.client) if !context.session.nil? remove_session(context) @@ -406,13 +409,13 @@ module JamWebsockets elsif @message_factory.session_directed? client_msg - session = client_msg.target[MessageFactory::SESSION_TARGET_PREFIX.length..-1] - handle_session_directed(session, client_msg, client) + session_id = client_msg.target[MessageFactory::SESSION_TARGET_PREFIX.length..-1] + handle_session_directed(session_id, client_msg, client) elsif @message_factory.user_directed? client_msg - user = client_msg.target[MessageFactory::USER_PREFIX_TARGET.length..-1] - handle_user_directed(user, client_msg, client) + user_id = client_msg.target[MessageFactory::USER_PREFIX_TARGET.length..-1] + handle_user_directed(user_id, client_msg, client) else raise SessionError, "client_msg.target is unknown type: #{client_msg.target}" @@ -475,7 +478,7 @@ module JamWebsockets @log.debug "Created connection => #{connection.user}, #{connection.client_id}" if connection.save - send_friend_update(user, true) + send_friend_update(user, true, context.client) end end else @@ -483,10 +486,10 @@ module JamWebsockets end end - def send_friend_update(user, online) + def send_friend_update(user, online, client) @log.debug "sending friend update for user #{user} online = #{online}" - if user.friends.exists? + if !user.nil? && user.friends.exists? @log.debug "user has friends - sending friend updates" # create the friend_update message @@ -496,8 +499,7 @@ module JamWebsockets user.friends.each do |friend| @log.debug "sending friend update message to #{friend}" - # put it on the topic exchange for users - @users_exchange.publish(friend_update_msg.to_s, :routing_key => "user.#{friend.id}") + handle_user_directed(friend.id, friend_update_msg, client) end end end @@ -617,9 +619,12 @@ module JamWebsockets @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}", :properties => { :headers => { "client_id" => client.client_id } } ) end - def handle_user_directed(user, client_msg, client) + def handle_user_directed(user_id, client_msg, client) - raise SessionError, 'not implemented' + @log.debug "publishing to user #{user_id} from client_id #{client.client_id}" + + # put it on the topic exchange for users + @users_exchange.publish(client_msg.to_s, :routing_key => "user.#{user_id}") end end end From c388f9e169a73242027ad7d446998f10cbf5ce4b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 8 Oct 2012 21:26:06 -0400 Subject: [PATCH 21/98] fixed bug --- lib/jam_websockets/router.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index c07c1ebd0..e9fd58e1d 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -84,8 +84,10 @@ module JamWebsockets routing_key = headers.envelope.routing_key user_id = routing_key["user.".length..-1] @sempahore.synchronize do + @log.debug "USER ID = #{user_id}" contexts = @user_context_lookup[user_id] + @log.debug "CONTEXT = #{contexts}" unless contexts.nil? @log.debug "received user-directed message for user: #{user_id}" @@ -507,7 +509,7 @@ module JamWebsockets def handle_heartbeat(heartbeat, client) context = @clients[client] @log.debug "updating timestamp for user #{context}" - connection = Connection.find_by_user_id_and_client_id(context.user.user_id, context.client.client_id) + connection = Connection.find_by_user_id_and_client_id(context.user.id, context.client.client_id) unless connection.nil? connection.updated_at = DateTime.now From 007b1e67d2bfda37b0e9e7012a3a8269d2768c5b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 8 Oct 2012 23:06:39 -0400 Subject: [PATCH 22/98] fixed issue with connection cleanup --- lib/jam_websockets/router.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index e9fd58e1d..15e184d14 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -371,8 +371,6 @@ module JamWebsockets if !context.nil? - remove_user(context) - # remove this connection from the database if !context.user.nil? && !context.client.nil? JamRuby::Connection.delete_all "user_id = '#{context.user.id}' AND client_id = '#{context.client.client_id}'" @@ -380,6 +378,8 @@ module JamWebsockets send_friend_update(context.user, false, context.client) + remove_user(context) + if !context.session.nil? remove_session(context) end From ed87614309c0ecc7adfb551ac9d1734445b5f2ef Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 9 Oct 2012 01:03:49 -0400 Subject: [PATCH 23/98] corrected misspelled semaphore variable --- lib/jam_websockets/router.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 15e184d14..ca18aa07b 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -83,27 +83,29 @@ module JamWebsockets begin routing_key = headers.envelope.routing_key user_id = routing_key["user.".length..-1] - @sempahore.synchronize do - @log.debug "USER ID = #{user_id}" + + @semaphore.synchronize do contexts = @user_context_lookup[user_id] - @log.debug "CONTEXT = #{contexts}" - unless contexts.nil? + if !contexts.nil? @log.debug "received user-directed message for user: #{user_id}" msg = Jampb::ClientMessage.parse(msg) + contexts.each do |context| EM.schedule do @log.debug "sending user message to #{context}" send_to_client(context.client, msg) end end + else + @log.debug "Context is null" end end rescue => e - @log.error "unhandled error in messaging to client" + @log.error e end end @@ -158,6 +160,7 @@ module JamWebsockets end rescue => e + @log.error "EXCEPTION = #{e}" @log.error "unhandled error in messaging to client" end end @@ -496,7 +499,7 @@ module JamWebsockets # create the friend_update message friend_update_msg = @message_factory.friend_update(user.id, online) - + # send the friend_update to each friend that has active connections user.friends.each do |friend| @log.debug "sending friend update message to #{friend}" @@ -617,6 +620,7 @@ module JamWebsockets session = access_music_session?(session_id, context.user) @log.debug "publishing to session #{session} from client_id #{client.client_id}" + # put it on the topic exchange for sessions @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}", :properties => { :headers => { "client_id" => client.client_id } } ) end From c711fa932b3a294b1674d3a19cb8428be24ca7ef Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 10 Oct 2012 22:04:43 -0500 Subject: [PATCH 24/98] * p2p initial version implemented and particially tested --- lib/jam_websockets.rb | 1 + lib/jam_websockets/permission_error.rb | 4 + lib/jam_websockets/router.rb | 721 +++++++++++++++---------- spec/jam_websockets/router_spec.rb | 210 ++++--- 4 files changed, 556 insertions(+), 380 deletions(-) create mode 100644 lib/jam_websockets/permission_error.rb diff --git a/lib/jam_websockets.rb b/lib/jam_websockets.rb index a2e24ee9e..f57f55206 100644 --- a/lib/jam_websockets.rb +++ b/lib/jam_websockets.rb @@ -2,6 +2,7 @@ require "logging" require "jam_ruby" require "jam_websockets/version" require "jam_websockets/session_error" +require "jam_websockets/permission_error" require "jam_websockets/client_context" require "jam_websockets/message" require "jam_websockets/router" diff --git a/lib/jam_websockets/permission_error.rb b/lib/jam_websockets/permission_error.rb new file mode 100644 index 000000000..83a0af45a --- /dev/null +++ b/lib/jam_websockets/permission_error.rb @@ -0,0 +1,4 @@ +class PermissionError < Exception + +end + diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 875078498..92eb0d49d 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -11,37 +11,40 @@ include Jampb # add new field to client connection module EventMachine -module WebSocket - class Connection < EventMachine::Connection + module WebSocket + class Connection < EventMachine::Connection attr_accessor :encode_json, :client_id # client_id is uuid we give to each client to track them as we like + end end end -end module JamWebsockets class Router - attr_accessor :user_context_lookup, :session_context_lookup - + attr_accessor :user_context_lookup, :session_context_lookup + def initialize(options={}) @log = Logging.logger[self] @pending_clients = Set.new # clients that have connected to server, but not logged in. @clients = {} # clients that have logged in - @user_context_lookup = {} # lookup a set of client_contexts by user_id - @session_context_lookup = {} # lookup a set of client_contexts by session_id + @user_context_lookup = {} # lookup a set of client_contexts by user_id + @session_context_lookup = {} # lookup a set of client_contexts by session_id + @client_lookup = {} # lookup a client by client_id @sessions_exchange = nil @connection = nil @channel = nil @users_exchange = nil @message_factory = JamRuby::MessageFactory.new - @semaphore = Mutex.new - @user_topic = nil - @user_subscription = nil - @session_topic = nil - @session_subscription = nil - @thread_pool = nil + @semaphore = Mutex.new + @user_topic = nil + @user_subscription = nil + @session_topic = nil + @session_subscription = nil + @client_topic = nil + @client_subscription = nil + @thread_pool = nil end def start(options = {}) @@ -49,7 +52,7 @@ module JamWebsockets @log.info "startup" begin - @thread_pool = Executors.new_fixed_thread_pool(8) + @thread_pool = Executors.new_fixed_thread_pool(8) @connection = HotBunnies.connect(:host => options[:host], :port => options[:port]) @channel = @connection.create_channel @channel.prefetch = 10 @@ -62,119 +65,143 @@ module JamWebsockets end - def add_user(context) - user_contexts = @user_context_lookup[context.user.id] - if user_contexts.nil? - user_contexts = Set.new - @user_context_lookup[context.user.id] = user_contexts - end + def add_client(client_id, client) - user_contexts.add(context) - end + # should never occur + if @client_lookup.has_key?(client_id) + @log.warn "client_id #{client_id} connected while the old connection has not yet been terminated" + end - def remove_user(context) - user_contexts = @user_context_lookup[context.user.id] - if user_contexts.nil? - @log.warn "user can not be removed #{context}" - else - # delete the context from set of user contexts - user_contexts.delete(context) + @client_lookup[client_id] = client + end - # if last user context, delete entire set (memory leak concern) - if user_contexts.length == 0 - @user_context_lookup.delete(context.user.id) - end - end - end + def remove_client(client_id, client) + deleted = @client_lookup.delete(client_id) - def add_session(context) - session_contexts = @session_context_lookup[context.session.id] - if session_contexts.nil? - session_contexts = Set.new - @session_context_lookup[context.session.id] = session_contexts - end + if deleted.nil? + @log.warn "unable to delete #{client_id} from client_lookup" + elsif deleted == client + # put it back--this is only possible if add_client hit the 'old connection' path + # so in other words if this happens: + # add_client(1, clientX) + # add_client(1, clientY) # but clientX is essentially defunct - this could happen due to a bug in client, or EM doesn't notify always of connection close in time + # remove_client(1, clientX) -- this check maintains that clientY stays as the current client in the hash + @client_lookup[client_id] = client + end + end - session_contexts.add(context) - end + def add_user(context) + user_contexts = @user_context_lookup[context.user.id] + if user_contexts.nil? + user_contexts = Set.new + @user_context_lookup[context.user.id] = user_contexts + end - def remove_session(context) - session_contexts = @session_context_lookup[context.session.id] - if session_contexts.nil? - @log.warn "session can not be removed #{context}" - else - # delete the context from set of session contexts - session_contexts.delete(context) + user_contexts.add(context) + end - # if last session context, delete entire set (memory leak concern) - if session_contexts.length == 0 - @session_context_lookup.delete(context.session.id) - end + def remove_user(context) + user_contexts = @user_context_lookup[context.user.id] + if user_contexts.nil? + @log.warn "user can not be removed #{context}" + else + # delete the context from set of user contexts + user_contexts.delete(context) - context.session = nil - end - end + # if last user context, delete entire set (memory leak concern) + if user_contexts.length == 0 + @user_context_lookup.delete(context.user.id) + end + end + end + def add_session(context) + session_contexts = @session_context_lookup[context.session.id] + if session_contexts.nil? + session_contexts = Set.new + @session_context_lookup[context.session.id] = session_contexts + end - # register topic for user messages and session messages - def register_topics - @users_exchange = @channel.exchange('users', :type => :topic) + session_contexts.add(context) + end + + def remove_session(context) + session_contexts = @session_context_lookup[context.session.id] + if session_contexts.nil? + @log.warn "session can not be removed #{context}" + else + # delete the context from set of session contexts + session_contexts.delete(context) + + # if last session context, delete entire set (memory leak concern) + if session_contexts.length == 0 + @session_context_lookup.delete(context.session.id) + end + + context.session = nil + end + end + + # register topic for user messages and session messages + def register_topics + @users_exchange = @channel.exchange('users', :type => :topic) @sessions_exchange = @channel.exchange('sessions', :type => :topic) - - # create user messaging topic + @clients_exchange = @channel.exchange('clients', :type => :topic) + + # create user messaging topic @user_topic = @channel.queue("", :auto_delete => true) @user_topic.bind(@users_exchange, :routing_key => "user.#") @user_topic.purge # TODO: alert friends - # subscribe for any messages to users - - #@user_subscription = @user_topic.subscribe(:ack => false, :blocking => false, :executor => @threadpool) do |headers, msg| - @user_subscription = @user_topic.subscribe(:ack => false) - @user_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| - begin - routing_key = headers.envelope.routing_key - user_id = routing_key["user.".length..-1] - @sempahore.synchronize do - contexts = @user_context_lookup[user_id] + # subscribe for any messages to users + + @user_subscription = @user_topic.subscribe(:ack => false) + @user_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| + begin + routing_key = headers.envelope.routing_key + user_id = routing_key["user.".length..-1] + @sempahore.synchronize do + contexts = @user_context_lookup[user_id] + + unless contexts.nil? + + @log.debug "received user-directed message for session: #{user_id}" - unless contexts.nil? - - @log.debug "received user-directed message for session: #{user_id}" - msg = Jampb::ClientMessage.parse(msg) - contexts.each do |context| - EM.schedule do - @log.debug "sending user message to #{context}" - send_to_client(context.client, msg) - end - end - end - end + contexts.each do |context| + EM.schedule do + @log.debug "sending user message to #{context}" + send_to_client(context.client, msg) + end + end + end + end - rescue => e - @log.error "unhandled error in messaging to client" - end - end + rescue => e + @log.error "unhandled error in messaging to client" + @log.error e + end + end - @session_topic = @channel.queue("", :auto_delete => true) + @session_topic = @channel.queue("", :auto_delete => true) @session_topic.bind(@sessions_exchange, :routing_key => "session.#") @session_topic.purge # subscribe for any messages to session - #@session_subscription = @session_topic.subscribe(:ack => false, :blocking => false) do |headers, msg| @session_subscription = @session_topic.subscribe(:ack => false) - @session_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| - begin - routing_key = headers.envelope.routing_key - session_id = routing_key["session.".length..-1] - @semaphore.synchronize do - contexts = @session_context_lookup[session_id] + @session_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| + begin + routing_key = headers.envelope.routing_key + session_id = routing_key["session.".length..-1] + @semaphore.synchronize do + contexts = @session_context_lookup[session_id] + + unless contexts.nil? + + @log.debug "received session-directed message for session: #{session_id}" - unless contexts.nil? - - @log.debug "received session-directed message for session: #{session_id}" - msg = Jampb::ClientMessage.parse(msg) # ok, its very odd to have your own message that you sent bounce back to you. @@ -183,94 +210,132 @@ module JamWebsockets properties = headers.properties unless headers.nil? inner_headers = properties.headers unless properties.nil? origin_client_id = inner_headers["client_id"] - + # counter-intuitively, even though a string is passed in when you send the header, an (apparently) auto-generated class is sent back which, if you to_s, returns the original value origin_client_id = origin_client_id.to_s unless origin_client_id.nil? - + @log.debug "message received from client #{origin_client_id}" - contexts.each do |context| + contexts.each do |context| if context.client.client_id != origin_client_id EM.schedule do @log.debug "sending session message to #{context}" send_to_client(context.client, msg) end end - end - end - end + end + end + end - rescue => e - @log.error "unhandled error in messaging to client" - end + rescue => e + @log.error "unhandled error in messaging to client" + @log.error e + end end - end - def send_to_client(client, msg) + @client_topic = @channel.queue("", :auto_delete => true) + @client_topic.bind(@clients_exchange, :routing_key => "client.#") + @client_topic.purge + + # subscribe for any p2p messages to a client + @client_subscription = @client_topic.subscribe(:ack => false) + @client_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| + begin + routing_key = headers.envelope.routing_key + client_id = routing_key["client.".length..-1] + @semaphore.synchronize do + client = @client_lookup[client_id] + + unless client.nil? + msg = Jampb::ClientMessage.parse(msg) + + properties = headers.properties unless headers.nil? + inner_headers = properties.headers unless properties.nil? + origin_client_id = inner_headers["client_id"] + + @log.debug "p2p message received from client #{origin_client_id} to client #{client_id}" + EM.schedule do + @log.debug "sending p2p message to #{client_id}" + send_to_client(client, msg) + end + end + end + + + rescue => e + @log.error "unhandled error in messaging to client" + @log.error e + end + end + end + + def send_to_client(client, msg) if client.encode_json client.send(msg.to_json.to_s) else - # this is so odd that this is necessary from an API perspective. but searching through the source code... it's all I could find in em-websocket for allowing a binary message to be sent - client.instance_variable_get(:@handler).send_frame(:binary, msg.to_s) + # this is so odd that this is necessary from an API perspective. but searching through the source code... it's all I could find in em-websocket for allowing a binary message to be sent + client.instance_variable_get(:@handler).send_frame(:binary, msg.to_s) end - end + end def cleanup() - # shutdown topic listeners and mq connection - begin - if !@user_subscription.nil? && @user_subscription.active? - @log.debug "cleaning up user subscription" - @user_subscription.cancel - @user_subscription.shutdown! - end + # shutdown topic listeners and mq connection + begin + if !@user_subscription.nil? && @user_subscription.active? + @log.debug "cleaning up user subscription" + @user_subscription.cancel + @user_subscription.shutdown! + end - if !@session_subscription.nil? && @session_subscription.active? - @log.debug "cleaning up session subscription" - @session_subscription.cancel - @session_subscription.shutdown! - end + if !@session_subscription.nil? && @session_subscription.active? + @log.debug "cleaning up session subscription" + @session_subscription.cancel + @session_subscription.shutdown! + end - rescue => e - @log.debug "unable to cancel subscription on cleanup: #{e}" - end + if !@client_subscription.nil? && @client_subscription.active? + @log.debug "cleaning up client subscription" + @client_subscription.cancel + @client_subscription.shutdown! + end - @thread_pool.shutdown + rescue => e + @log.debug "unable to cancel subscription on cleanup: #{e}" + end - if !@channel.nil? - @channel.close - end + @thread_pool.shutdown - if !@connection.nil? - @connection.close - end + if !@channel.nil? + @channel.close + end - # tear down each individual client - @clients.each do |client, context| - cleanup_client(client) - end - end + if !@connection.nil? + @connection.close + end - def stop - @log.info "shutdown" - cleanup - end + # tear down each individual client + @clients.each do |client, context| + cleanup_client(client) + end + end - def new_client(client) + def stop + @log.info "shutdown" + cleanup + end - # give a unique ID to this client. This is used to prevent session messages - # from echoing back to the sender, for instance. - client.client_id = UUIDTools::UUID.random_create.to_s + def new_client(client) - @semaphore.synchronize do - @pending_clients.add(client) - end + @semaphore.synchronize do + @pending_clients.add(client) + end # default to using json instead of pb - client.encode_json = true + client.encode_json = true + + client.onopen { + #binding.pry + @log.debug "client connected #{client}" - client.onopen { - #binding.pry - @log.debug "client connected #{client}" - # check for '?pb' or '?pb=true' in url query parameters query_pb = client.request["query"]["pb"] @@ -280,10 +345,10 @@ module JamWebsockets } - client.onclose { - @log.debug "Connection closed" + client.onclose { + @log.debug "Connection closed" - cleanup_client(client) + cleanup_client(client) } client.onerror { |error| @@ -294,7 +359,7 @@ module JamWebsockets end cleanup_client(client) - client.close_websocket + client.close_websocket } client.onmessage { |msg| @@ -303,79 +368,90 @@ module JamWebsockets # TODO: set a max message size before we put it through PB? # TODO: rate limit? - + begin - if client.encode_json - #example: {"type":"LOGIN", "target":"server", "login" : {"username":"hi"}} - parse = JSON.parse(msg) - pb_msg = Jampb::ClientMessage.json_create(parse) + if client.encode_json + #example: {"type":"LOGIN", "route_to":"server", "login" : {"username":"hi"}} + parse = JSON.parse(msg) + pb_msg = Jampb::ClientMessage.json_create(parse) self.route(pb_msg, client) - else - pb_msg = Jampb::ClientMessage.parse(msg.to_s) - self.route(pb_msg, client) - end - rescue SessionError => e - @log.info "ending client session deliberately due to malformed client behavior. reason=#{e}" - begin + else + pb_msg = Jampb::ClientMessage.parse(msg.to_s) + self.route(pb_msg, client) + end + rescue SessionError => e + @log.info "ending client session deliberately due to malformed client behavior. reason=#{e}" + begin # wrap the message up and send it down error_msg = @message_factory.server_rejection_error(e.to_s) - send_to_client(client, error_msg) + send_to_client(client, error_msg) ensure - client.close_websocket + client.close_websocket cleanup_client(client) end + rescue PermissionError => e + @log.info "permission error. reason=#{e.to_s}" + @log.info e + + # wrap the message up and send it down + error_msg = @message_factory.server_permission_error(msg.message_id, e.to_s) + send_to_client(client, error_msg) rescue => e @log.error "ending client session due to server programming or runtime error. reason=#{e.to_s}" - @log.error e - - begin + @log.error e + + begin # wrap the message up and send it down error_msg = @message_factory.server_generic_error(e.to_s) - send_to_client(client, error_msg) + send_to_client(client, error_msg) ensure - client.close_websocket + client.close_websocket cleanup_client(client) end end + } end - # removes all resources associated with a client + # removes all resources associated with a client def cleanup_client(client) - @semaphore.synchronize do - pending = @pending_clients.delete?(client) + @semaphore.synchronize do + pending = @pending_clients.delete?(client) - if !pending.nil? - @log.debug "cleaning up pending client #{client}" - else - context = @clients.delete(client) + if !pending.nil? + @log.debug "cleaning up pending client #{client}" + else - if !context.nil? - - remove_user(context) + remove_client(client.client_id, client) - if !context.session.nil? - remove_session(context) - end - else - @log.debug "skipping duplicate cleanup attempt of authorized client" - end + context = @clients.delete(client) - end - end + if !context.nil? + + remove_user(context) + + if !context.session.nil? + remove_session(context) + end + else + @log.debug "skipping duplicate cleanup attempt of authorized client" + end + + end + end end def route(client_msg, client) - message_type = @message_factory.get_message_type(client_msg) - - raise SessionError, "unknown message type received: #{client_msg.type}" if message_type.nil? - - @log.debug("msg received #{message_type}") + message_type = @message_factory.get_message_type(client_msg) - raise SessionError, 'client_msg.target is null' if client_msg.target.nil? + raise SessionError, "unknown message type received: #{client_msg.type}" if message_type.nil? + + @log.debug("msg received #{message_type}") + + raise SessionError, 'client_msg.route_to is null' if client_msg.route_to.nil? if @pending_clients.include? client and client_msg.type != ClientMessage::Type::LOGIN # this client has not logged in and is trying to send a non-login message @@ -386,18 +462,23 @@ module JamWebsockets handle_server_directed(client_msg, client) + elsif @message_factory.client_directed? client_msg + + to_client_id = client_msg.route_to[MessageFactory::CLIENT_TARGET_PREFIX.length..-1] + handle_client_directed(to_client_id, client_msg, client) + elsif @message_factory.session_directed? client_msg - session = client_msg.target[MessageFactory::SESSION_TARGET_PREFIX.length..-1] + session = client_msg.route_to[MessageFactory::SESSION_TARGET_PREFIX.length..-1] handle_session_directed(session, client_msg, client) elsif @message_factory.user_directed? client_msg - user = client_msg.target[MessageFactory::USER_PREFIX_TARGET.length..-1] + user = client_msg.route_to[MessageFactory::USER_PREFIX_TARGET.length..-1] handle_user_directed(user, client_msg, client) else - raise SessionError, "client_msg.target is unknown type: #{client_msg.target}" + raise SessionError, "client_msg.route_to is unknown type: #{client_msg.route_to}" end end @@ -408,9 +489,9 @@ module JamWebsockets handle_login(client_msg.login, client) - elsif client_msg.type == ClientMessage::Type::HEARTBEAT + elsif client_msg.type == ClientMessage::Type::HEARTBEAT - handle_heartbeat(client_msg.heartbeat, client) + handle_heartbeat(client_msg.heartbeat, client) elsif client_msg.type == ClientMessage::Type::LOGIN_MUSIC_SESSION @@ -421,7 +502,7 @@ module JamWebsockets handle_leave_music_session(client_msg.leave_music_session, client) else - raise SessionError, "unknown message type '#{client_msg.type}' for #{client_msg.target}-directed message" + raise SessionError, "unknown message type '#{client_msg.type}' for #{client_msg.route_to}-directed message" end end @@ -430,36 +511,47 @@ module JamWebsockets username = login.username if login.value_for_tag(1) password = login.password if login.value_for_tag(2) token = login.token if login.value_for_tag(3) + client_id = login.client_id if login.value_for_tag(4) - user = valid_login(username, password, token) + # you don't have to supply client_id in login--if you don't, we'll generate one + if client_id.nil? + # give a unique ID to this client. This is used to prevent session messages + # from echoing back to the sender, for instance. + client_id = UUIDTools::UUID.random_create.to_s + end + + client.client_id = client_id + + user = valid_login(username, password, token, client_id) if !user.nil? @log.debug "user #{user.email} logged in" # respond with LOGIN_ACK to let client know it was successful - #binding.pry - remote_port, remote_ip = Socket.unpack_sockaddr_in(client.get_peername) - login_ack = @message_factory.login_ack(remote_ip) - send_to_client(client, login_ack) + #binding.pry + remote_port, remote_ip = Socket.unpack_sockaddr_in(client.get_peername) + login_ack = @message_factory.login_ack(remote_ip, client_id, user.remember_token) + send_to_client(client, login_ack) # remove from pending_queue - @semaphore.synchronize do - @pending_clients.delete(client) + @semaphore.synchronize do + @pending_clients.delete(client) - # add a tracker for this user - context = ClientContext.new(user, client) - @clients[client] = context - add_user(context) - end + # add a tracker for this user + context = ClientContext.new(user, client) + @clients[client] = context + add_user(context) + add_client(client_id, client) + end else raise SessionError, 'invalid login' end end - def handle_heartbeat(heartbeat, client) - # todo: manage staleness - end + def handle_heartbeat(heartbeat, client) + # todo: manage staleness + end def handle_join_music_session(join_music_session, client) # verify that the current user has the rights to actually join the music session @@ -468,107 +560,146 @@ module JamWebsockets session_id = join_music_session.music_session begin - session = access_music_session?(session_id, context.user) - @log.debug "user #{context} joining new session #{session}" - @semaphore.synchronize do - old_session = context.session - if !old_session.nil? - @log.debug "#{context} is already in session. auto-logging out to join new session." - remove_session(context) - end - context.session = session - add_session(context) - end + session = access_music_session(session_id, context.user) + @log.debug "user #{context} joining new session #{session}" + @semaphore.synchronize do + old_session = context.session + if !old_session.nil? + @log.debug "#{context} is already in session. auto-logging out to join new session." + remove_session(context) + end + context.session = session + add_session(context) + end rescue => e # send back a failure ack and bail - @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" + @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" login_music_session = @message_factory.login_music_session_ack(true, e.to_s) - send_to_client(client, login_music_session) + send_to_client(client, login_music_session) return end # respond with LOGIN_MUSIC_SESSION_ACK to let client know it was successful login_music_session = @message_factory.login_music_session_ack(false, nil) - send_to_client(client, login_music_session) - + send_to_client(client, login_music_session) + # send 'new client' message to other members in the session - handle_session_directed(session_id, - @message_factory.user_joined_music_session(context.user.id, context.user.name), - client) + handle_session_directed(session_id, + @message_factory.user_joined_music_session(context.user.id, context.user.name), + client) end - def handle_leave_music_session(leave_music_session, client) + def handle_leave_music_session(leave_music_session, client) - context = @clients[client] + context = @clients[client] - raise SessionError, "unsupported" - end + raise SessionError, "unsupported" + end - def valid_login(username, password, token) + def valid_login(username, password, token, client_id) if !token.nil? && token != '' @log.debug "logging in via token" - # attempt login with token - user = User.find_by_remember_token(token) + # attempt login with token + user = User.find_by_remember_token(token) - if user.nil? - @log.debug "no user found with token" - return false - else - @log.debug "#{user} login via token" - return user - end + if user.nil? + @log.debug "no user found with token" + return false + else + @log.debug "#{user} login via token" + return user + end - elsif !username.nil? and !password.nil? + elsif !username.nil? and !password.nil? @log.debug "logging in via user/pass '#{username}' '#{password}'" - # attempt login with username and password - user = User.find_by_email(username) + # attempt login with username and password + user = User.find_by_email(username) - if !user.nil? && user.authenticate(password) - @log.debug "#{user} login via password" - return user - else - @log.debug "#{username} login failure" - return nil - end - else - raise SessionError, 'no login data was found in Login message' - end + if !user.nil? && user.authenticate(password) + @log.debug "#{user} login via password" + return user + else + @log.debug "#{username} login failure" + return nil + end + else + raise SessionError, 'no login data was found in Login message' + end - end + end - def access_music_session?(music_session_id, user) - music_session = MusicSession.find_by_id(music_session_id) + def access_music_session(music_session_id, user) + music_session = MusicSession.find_by_id(music_session_id) - if music_session.nil? - raise SessionError, 'specified session not found' - end + if music_session.nil? + raise SessionError, 'specified session not found' + end - if !music_session.access? user - raise SessionError, 'not allowed to join the specified session' - end + if !music_session.access? user + raise SessionError, 'not allowed to join the specified session' + end - return music_session - end + return music_session + end - def handle_session_directed(session_id, client_msg, client) + # client_id = the id of the client being accessed + # client = the current client + def access_p2p(client_id, user, msg) - context = @clients[client] + # ping_request and ping_ack messages are special in that they are simply allowed + if msg.type == ClientMessage::Type::PING_REQUEST || msg.type == ClientMessage::Type::PING_ACK + return nil + end - # by not catching any exception here, this will kill the connection - # if for some reason the client is trying to send to a session that it doesn't - # belong to - session = access_music_session?(session_id, context.user) + music_session_client = MusicSessionClient.find_by_client_id(client_id) - @log.debug "publishing to session #{session} from client_id #{client.client_id}" - # put it on the topic exchange for sessions - @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}", :properties => { :headers => { "client_id" => client.client_id } } ) - end + if music_session_client.nil? + raise PermissionError, 'specified client not found' + end - def handle_user_directed(user, client_msg, client) + if !music_session_client.access_p2p? user - raise SessionError, 'not implemented' - end - end + raise SessionError, 'not allowed to message this client' + + end + end + + def handle_session_directed(session_id, client_msg, client) + + context = @clients[client] + + # by not catching any exception here, this will kill the connection + # if for some reason the client is trying to send to a session that it doesn't + # belong to + session = access_music_session(session_id, context.user) + + @log.debug "publishing to session #{session} from client_id #{client.client_id}" + # put it on the topic exchange for sessions + @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}", :properties => {:headers => {"client_id" => client.client_id}}) + end + + def handle_client_directed(to_client_id, client_msg, client) + context = @clients[client] + + # by not catching any exception here, a PermissionError will be thrown if this isn't valid + # if for some reason the client is trying to send to a client that it doesn't + # belong to + access_p2p(to_client_id, context.user, client_msg) + + # populate routing data + client_msg.from = client.client_id + + @log.debug "publishing to client #{to_client_id} from client_id #{client.client_id}" + + # put it on the topic exchange for clients + @clients_exchange.publish(client_msg.to_s, :routing_key => "client.#{to_client_id}", :properties => {:headers => {"client_id" => client.client_id}}) + end + + def handle_user_directed(user, client_msg, client) + + raise SessionError, 'not implemented' + end + end end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 6736ca0e7..6ec66fe5f 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -2,78 +2,80 @@ require 'spec_helper' require 'thread' LoginClient = Class.new do - attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id + attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id - def initiaize() + def initiaize() - end + end - def onopen(&block) - @onopenblock = block - end + def onopen(&block) + @onopenblock = block + end - def onmessage(&block) - @onmsgblock = block - end + def onmessage(&block) + @onmsgblock = block + end - def close(&block) - @oncloseblock = block - end + def close(&block) + @oncloseblock = block + end - def close_websocket() - - end + def close_websocket() - def send(msg) - puts msg - end + end - def get_peername - return "\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" # 37643, "localhost" - end + def send(msg) + puts msg + end + + def get_peername + return "\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" # 37643, "localhost" + end end # does a login and returns client -def login(router, user, password) +def login(router, user, password, client_id) - message_factory = MessageFactory.new - client = LoginClient.new + message_factory = MessageFactory.new + client = LoginClient.new - login_ack = message_factory.login_ack("127.0.0.1") + login_ack = message_factory.login_ack("127.0.0.1", client_id) - router.should_receive(:send_to_client).with(client, login_ack) - client.should_receive(:onclose) - client.should_receive(:onerror) - client.should_receive(:request).and_return({ "query" => { "pb" => "true" } }) - client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") + router.should_receive(:send_to_client).with(client, login_ack) + client.should_receive(:onclose) + client.should_receive(:onerror) + client.should_receive(:request).and_return({"query" => {"pb" => "true"}}) + client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") - @router.new_client(client) - client.onopenblock.call + @router.new_client(client) + client.onopenblock.call - # create a login message, and pass it into the router via onmsgblock.call - login = message_factory.login_with_user_pass(user.email, password) + # create a login message, and pass it into the router via onmsgblock.call + login = message_factory.login_with_user_pass(user.email, password, :client_id => client_id) - # first log in - client.onmsgblock.call login.to_s + # first log in + client.onmsgblock.call login.to_s - # then join music session - return client + # then join music session + return client end +# currently commented out; we have deprecated logging in for jam sessions via websocket-gateway; +# use rest API instead (or direct db access with factory-girl) def login_music_session(router, client, music_session) - message_factory = MessageFactory.new - login_music_session = message_factory.login_music_session(music_session.id) - login_ack = message_factory.login_music_session_ack(false, nil); - router.should_receive(:send_to_client).with(client, login_ack) - client.onmsgblock.call login_music_session.to_s + #message_factory = MessageFactory.new + #login_music_session = message_factory.login_music_session(music_session.id) + #login_ack = message_factory.login_music_session_ack(false, nil); + #router.should_receive(:send_to_client).with(client, login_ack) + #client.onmsgblock.call login_music_session.to_s end describe Router do - message_factory = MessageFactory.new + message_factory = MessageFactory.new before do @@ -100,31 +102,30 @@ describe Router do client.should_receive(:onerror) client.should_receive(:onmessage) client.should_receive(:encode_json=) - client.should_receive(:client_id=) @router.new_client(client) end end - describe "topic routing helpers" do - it "create and delete user lookup set" do - user = double(User) - user.should_receive(:id).any_number_of_times.and_return("1") - client = double("client") - context = ClientContext.new(user, client) + describe "topic routing helpers" do + it "create and delete user lookup set" do + user = double(User) + user.should_receive(:id).any_number_of_times.and_return("1") + client = double("client") + context = ClientContext.new(user, client) - @router.user_context_lookup.length.should == 0 + @router.user_context_lookup.length.should == 0 - @router.add_user(context) - - @router.user_context_lookup.length.should == 1 + @router.add_user(context) - @router.remove_user(context) + @router.user_context_lookup.length.should == 1 - @router.user_context_lookup.length.should == 0 - end - end + @router.remove_user(context) + + @router.user_context_lookup.length.should == 0 + end + end describe "login" do @@ -145,8 +146,8 @@ describe Router do @onmsgblock = block end - def close_websocket() - end + def close_websocket() + end def close() end @@ -160,7 +161,7 @@ describe Router do client.should_receive(:close_websocket) client.should_receive(:onclose) client.should_receive(:onerror) - client.should_receive(:request).and_return({ "query" => { "pb" => "true" } }) + client.should_receive(:request).and_return({"query" => {"pb" => "true"}}) @router.new_client(client) @@ -178,7 +179,7 @@ describe Router do :password => "foobar", :password_confirmation => "foobar") @user.save - client1 = login(@router, @user, "foobar") + client1 = login(@router, @user, "foobar", "1") end @@ -190,40 +191,79 @@ describe Router do music_session = FactoryGirl.create(:music_session, :creator => user1) - music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session) - music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session) + music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session, :client_id => "1") + music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") # make a music_session and define two members - # create client 1, log him in, and log him in to music session - client1 = login(@router, user1, "foobar") - login_music_session(@router, client1, music_session) + # create client 1, log him in, and log him in to music session + client1 = login(@router, user1, "foobar", "1") + login_music_session(@router, client1, music_session) \ + end - it "should allow two valid subscribers to communicate with session-directed messages", :mq => true do + it "should allow two valid subscribers to communicate with session-directed messages", :mq => true do - EventMachine.run do - user1 = FactoryGirl.create(:user) # in the music session - user2 = FactoryGirl.create(:user) # in the music session + EventMachine.run do + user1 = FactoryGirl.create(:user) # in the music session + user2 = FactoryGirl.create(:user) # in the music session - music_session = FactoryGirl.create(:music_session, :creator => user1) - - music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session) - music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session) - - # make a music_session and define two members + music_session = FactoryGirl.create(:music_session, :creator => user1) - # create client 1, log him in, and log him in to music session - client1 = login(@router, user1, "foobar") - login_music_session(@router, client1, music_session) + # create client 1, log him in, and log him in to music session + client1 = login(@router, user1, "foobar", "1") + login_music_session(@router, client1, music_session) - client2 = login(@router, user2, "foobar") - login_music_session(@router, client2, music_session) - EM.stop - end + client2 = login(@router, user2, "foobar", "2") + login_music_session(@router, client2, music_session) + + # make a music_session and define two members + + music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session, :client_id => "1") + music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") + + + EM.stop + end end + it "should allow two valid subscribers to communicate with p2p messages", :mq => true do + + EventMachine.run do + user1 = FactoryGirl.create(:user) # in the music session + user2 = FactoryGirl.create(:user) # in the music session + + music_session = FactoryGirl.create(:music_session, :creator => user1) + + # create client 1, log him in, and log him in to music session + client1 = login(@router, user1, "foobar", "1") + #login_music_session(@router, client1, music_session) + + client2 = login(@router, user2, "foobar", "2") + #login_music_session(@router, client2, music_session) + + # by creating + music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session, :client_id => "1") + + # now attempt to message p2p! + + # first test: user 2 should be able to send a ping message to user 1, even though he isn't in the same session yet + + # create a login message, and pass it into the router via onmsgblock.call + ping = message_factory.ping_request("1", "2") + + #@router.should_receive(:send_to_client) #.with(client1, ping) + + ## send ping to client 2 + #client2.onmsgblock.call ping.to_s + + #music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") + + + EM.stop + end + end end end From cb9ec9c6367545b528809de62ca9ef883a3838f2 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 15 Oct 2012 06:38:33 -0500 Subject: [PATCH 25/98] * usitng pb_msg and not msg to extract message --- lib/jam_websockets/router.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 67c59e696..7114d5e3a 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -340,6 +340,7 @@ module JamWebsockets # TODO: set a max message size before we put it through PB? # TODO: rate limit? + pb_msg = nil begin if client.encode_json @@ -366,7 +367,7 @@ module JamWebsockets @log.info e # wrap the message up and send it down - error_msg = @message_factory.server_permission_error(msg.message_id, e.to_s) + error_msg = @message_factory.server_permission_error(pb_msg.message_id, e.to_s) send_to_client(client, error_msg) rescue => e @log.error "ending client session due to server programming or runtime error. reason=#{e.to_s}" From 0e4d5a13f8ed4dec54211d68cb77ed73f330f0fa Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 16 Oct 2012 22:50:28 -0500 Subject: [PATCH 26/98] * removing session topic concept; instead sending to a session causes a message to be created for each client --- lib/jam_websockets/router.rb | 282 ++++++----------------------------- 1 file changed, 46 insertions(+), 236 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 7114d5e3a..82fa6f2b2 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -22,16 +22,14 @@ module JamWebsockets class Router - attr_accessor :user_context_lookup, :session_context_lookup + attr_accessor :user_context_lookup def initialize(options={}) @log = Logging.logger[self] @pending_clients = Set.new # clients that have connected to server, but not logged in. @clients = {} # clients that have logged in @user_context_lookup = {} # lookup a set of client_contexts by user_id - @session_context_lookup = {} # lookup a set of client_contexts by session_id @client_lookup = {} # lookup a client by client_id - @sessions_exchange = nil @connection = nil @channel = nil @users_exchange = nil @@ -39,8 +37,6 @@ module JamWebsockets @semaphore = Mutex.new @user_topic = nil @user_subscription = nil - @session_topic = nil - @session_subscription = nil @client_topic = nil @client_subscription = nil @thread_pool = nil @@ -103,45 +99,21 @@ module JamWebsockets user_contexts.add(context) end - def remove_user(context) - user_contexts = @user_context_lookup[context.user.id] + def remove_user(client_context) + user_contexts = @user_context_lookup[client_context.user.id] + if user_contexts.nil? - @log.warn "user can not be removed #{context}" + @log.warn "user can not be removed #{client_context}" else # delete the context from set of user contexts - user_contexts.delete(context) + user_contexts.delete(client_context) # if last user context, delete entire set (memory leak concern) if user_contexts.length == 0 - @user_context_lookup.delete(context.user.id) - end - end - end - - def add_session(context) - session_contexts = @session_context_lookup[context.session.id] - if session_contexts.nil? - session_contexts = Set.new - @session_context_lookup[context.session.id] = session_contexts - end - - session_contexts.add(context) - end - - def remove_session(context) - session_contexts = @session_context_lookup[context.session.id] - if session_contexts.nil? - @log.warn "session can not be removed #{context}" - else - # delete the context from set of session contexts - session_contexts.delete(context) - - # if last session context, delete entire set (memory leak concern) - if session_contexts.length == 0 - @session_context_lookup.delete(context.session.id) + @user_context_lookup.delete(client_context.user.id) end - context.session = nil + client_context.user = nil end end @@ -192,63 +164,6 @@ module JamWebsockets end end - ######################## SESSION MESSAGING ########################### - - # create session exchange - @sessions_exchange = @channel.exchange('sessions', :type => :topic) - - # create session messaging topic - @session_topic = @channel.queue("", :auto_delete => true) - @session_topic.bind(@sessions_exchange, :routing_key => "session.#") - @session_topic.purge - - # subscribe for any messages to session - @session_subscription = @session_topic.subscribe(:ack => false) - - # this code serves as a callback that dequeues messages and processes them - @session_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| - begin - routing_key = headers.envelope.routing_key - session_id = routing_key["session.".length..-1] - @semaphore.synchronize do - contexts = @session_context_lookup[session_id] - - unless contexts.nil? - - @log.debug "received session-directed message for session: #{session_id}" - - - # ok, its very odd to have your own message that you sent bounce back to you. - # In one small favor to the client, we purposefully disallow messages a client - # sent from bouncing back to itself. - properties = headers.properties unless headers.nil? - inner_headers = properties.headers unless properties.nil? - origin_client_id = inner_headers["client_id"] - - # counter-intuitively, even though a string is passed in when you send the header, an (apparently) auto-generated class is sent back which, if you to_s, returns the original value - origin_client_id = origin_client_id.to_s unless origin_client_id.nil? - - - @log.debug "session message received from client #{origin_client_id}" - - msg = Jampb::ClientMessage.parse(msg) - contexts.each do |context| - if context.client.client_id != origin_client_id - EM.schedule do - @log.debug "sending session message to #{context}" - send_to_client(context.client, msg) - end - end - end - end - end - - rescue => e - @log.error "unhandled error in messaging to client" - @log.error e - end - end - ############## CLIENT MESSAGING ################### @clients_exchange = @channel.exchange('clients', :type => :topic) @@ -266,14 +181,11 @@ module JamWebsockets @semaphore.synchronize do client = @client_lookup[client_id] - properties = headers.properties unless headers.nil? - inner_headers = properties.headers unless properties.nil? - origin_client_id = inner_headers["client_id"] + msg = Jampb::ClientMessage.parse(msg) - @log.debug "p2p message received from client #{origin_client_id} to client #{client_id}" + @log.debug "p2p message received from #{msg.from} to client #{client_id}" unless client.nil? - msg = Jampb::ClientMessage.parse(msg) EM.schedule do @log.debug "sending p2p message to #{client_id}" @@ -282,10 +194,7 @@ module JamWebsockets else @log.debug "p2p message unroutable to disconnected client #{client_id}" end - end - - rescue => e @log.error "unhandled error in messaging to client" @log.error e @@ -294,7 +203,6 @@ module JamWebsockets end - def new_client(client) @semaphore.synchronize do @@ -385,63 +293,6 @@ module JamWebsockets } end - def add_user(client_context) - user_contexts = @user_context_lookup[client_context.user.id] - - if user_contexts.nil? - user_contexts = Set.new - @user_context_lookup[client_context.user.id] = user_contexts - end - - user_contexts.add(client_context) - end - - def remove_user(client_context) - user_contexts = @user_context_lookup[client_context.user.id] - - if user_contexts.nil? - @log.warn "user can not be removed #{client_context}" - else - # delete the context from set of user contexts - user_contexts.delete(client_context) - - # if last user context, delete entire set (memory leak concern) - if user_contexts.length == 0 - @user_context_lookup.delete(client_context.user.id) - end - - client_context.user = nil - end - end - - def add_session(client_context) - session_contexts = @session_context_lookup[client_context.session.id] - - if session_contexts.nil? - session_contexts = Set.new - @session_context_lookup[client_context.session.id] = session_contexts - end - - session_contexts.add(client_context) - end - - def remove_session(client_context) - session_contexts = @session_context_lookup[client_context.session.id] - - if session_contexts.nil? - @log.warn "session can not be removed #{client_context}" - else - # delete the context from set of session contexts - session_contexts.delete(client_context) - - # if last session context, delete entire set (memory leak concern) - if session_contexts.length == 0 - @session_context_lookup.delete(client_context.session.id) - end - - client_context.session = nil - end - end def send_to_client(client, msg) @log.debug "SEND TO CLIENT START" @@ -463,12 +314,6 @@ module JamWebsockets @user_subscription.shutdown! end - if !@session_subscription.nil? && @session_subscription.active? - @log.debug "cleaning up session subscription" - @session_subscription.cancel - @session_subscription.shutdown! - end - if !@client_subscription.nil? && @client_subscription.active? @log.debug "cleaning up client subscription" @client_subscription.cancel @@ -526,10 +371,6 @@ module JamWebsockets send_friend_update(context.user, false, context.client) remove_user(context) - - if !context.session.nil? - remove_session(context) - end else @log.debug "skipping duplicate cleanup attempt of logged-in client" end @@ -587,14 +428,6 @@ module JamWebsockets handle_heartbeat(client_msg.heartbeat, client) - elsif client_msg.type == ClientMessage::Type::LOGIN_MUSIC_SESSION - - handle_join_music_session(client_msg.login_music_session, client) - - elsif client_msg.type == ClientMessage::Type::LEAVE_MUSIC_SESSION - - handle_leave_music_session(client_msg.leave_music_session, client) - else raise SessionError, "unknown message type '#{client_msg.type}' for #{client_msg.route_to}-directed message" end @@ -680,50 +513,6 @@ module JamWebsockets end end - def handle_join_music_session(join_music_session, client) - # verify that the current user has the rights to actually join the music session - context = @clients[client] - - session_id = join_music_session.music_session - - begin - session = access_music_session(session_id, context.user) - - @log.debug "user #{context} joining new session #{session}" - @semaphore.synchronize do - old_session = context.session - if !old_session.nil? - @log.debug "#{context} is already in session. auto-logging out to join new session." - remove_session(context) - end - context.session = session - add_session(context) - end - rescue => e - # send back a failure ack and bail - @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" - login_music_session = @message_factory.login_music_session_ack(true, e.to_s) - send_to_client(client, login_music_session) - return - end - - # respond with LOGIN_MUSIC_SESSION_ACK to let client know it was successful - login_music_session = @message_factory.login_music_session_ack(false, nil) - send_to_client(client, login_music_session) - - # send 'new client' message to other members in the session - handle_session_directed(session_id, - @message_factory.user_joined_music_session(context.user.id, context.user.name), - client) - end - - def handle_leave_music_session(leave_music_session, client) - - context = @clients[client] - - raise SessionError, "unsupported" - end - def valid_login(username, password, token, client_id) if !token.nil? && token != '' @@ -787,25 +576,10 @@ module JamWebsockets end if !music_session_client.access_p2p? user - raise SessionError, 'not allowed to message this client' - end end - def handle_session_directed(session_id, client_msg, client) - - context = @clients[client] - - # by not catching any exception here, this will kill the connection - # if for some reason the client is trying to send to a session that it doesn't - # belong to - session = access_music_session(session_id, context.user) - - @log.debug "publishing to session #{session} from client_id #{client.client_id}" - # put it on the topic exchange for sessions - @sessions_exchange.publish(client_msg.to_s, :routing_key => "session.#{session_id}", :properties => {:headers => {"client_id" => client.client_id}}) - end def handle_client_directed(to_client_id, client_msg, client) context = @clients[client] @@ -831,5 +605,41 @@ module JamWebsockets # put it on the topic exchange for users @users_exchange.publish(client_msg.to_s, :routing_key => "user.#{user_id}") end + + def handle_session_directed(session_id, client_msg, client) + context = @clients[client] + + user_publish_to_session(session_id, context.user, client_msg, :client_id => client.client_id) + end + + # sends a message to a session on behalf of a user + # if this is originating in the context of a client, it should be specified as :client_id => "value" + # client_msg should be a well-structure message (jam-pb message) + def user_publish_to_session(music_session_id, user, client_msg, sender = {:client_id => ""}) + music_session = access_music_session(music_session_id, user) + + # gather up client_ids in the session + client_ids = music_session.music_session_clients.map { |client| client.client_id }.reject { |client_id| client_id == sender[:client_id] } + + publish_to_session(music_session.id, client_ids, client_msg.to_s, sender) + end + + + # sends a message to a session with no checking of permissions + # this method deliberately has no database interactivity/active_record objects + def publish_to_session(music_session_id, client_ids, client_msg, sender = {:client_id => ""}) + + EM.schedule do + sender_client_id = sender[:client_id] + + # iterate over each person in the session, and send a p2p message + client_ids.each do |client_id| + + @@log.debug "publishing to session:#{music_session_id} client:#{client_id} from client:#{sender_client_id}" + # put it on the topic exchange3 for clients + self.class.client_exchange.publish(client_msg, :routing_key => "client.#{music_session_id}") + end + end + end end end From 4323c4f5738c4cf700e41fd1e9637a2879921dc4 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 22 Oct 2012 19:58:57 -0500 Subject: [PATCH 27/98] * midway through jruby>ruby port for VRFS-18 --- .rvmrc | 3 +-- Gemfile | 7 +----- config/database.yml | 4 +-- lib/jam_websockets/router.rb | 48 +++++++++--------------------------- spec/factories.rb | 2 +- spec/spec_db.rb | 22 +++-------------- spec/spec_helper.rb | 2 +- 7 files changed, 21 insertions(+), 67 deletions(-) diff --git a/.rvmrc b/.rvmrc index a5e31abc5..435cd9286 100644 --- a/.rvmrc +++ b/.rvmrc @@ -1,2 +1 @@ -rvm use jruby-head@websockets --create -#rvm use ruby-1.9.3@websockets --create +rvm use ruby-1.9.3@websockets --create diff --git a/Gemfile b/Gemfile index 682b2ce94..9ceeece71 100644 --- a/Gemfile +++ b/Gemfile @@ -4,17 +4,13 @@ source 'https://rubygems.org' workspace = ENV["WORKSPACE"] || "~/workspace" gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' -gem 'jruby-openssl' gem 'ruby-protocol-buffers', '1.2.2' gem 'jam_ruby', :path => "#{workspace}/jam-ruby" gem 'jampb', :path => "#{workspace}/jam-pb/target/ruby/jampb" gem 'em-websocket'#, :path=> "#{workspace}/em-websocket-jam" -gem 'hot_bunnies', '1.3.8' +gem 'amqp' gem 'activerecord', '3.2.7' gem 'logging' -#gem 'em-http-request' -gem 'activerecord-jdbc-adapter' -gem 'activerecord-jdbcpostgresql-adapter' group :development do gem 'pry' @@ -30,5 +26,4 @@ group :test do gem 'guard', '>= 0.10.0' gem 'guard-rspec', '>= 0.7.3' gem 'pg_migrate','0.1.5' #:path => "#{workspace}/pg_migrate_ruby" - gem 'guard-jruby-rspec' end diff --git a/config/database.yml b/config/database.yml index 518037a56..c1ad9e417 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,5 +1,5 @@ test: - adapter: jdbcpostgresql + adapter: postgresql database: jam_websockets_test host: localhost port: 5432 @@ -10,7 +10,7 @@ test: encoding: unicode development: - adapter: jdbcpostgresql + adapter: postgresql database: jam host: localhost port: 5432 diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 82fa6f2b2..55fcdd099 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -1,12 +1,10 @@ require 'pry' require 'set' -require 'hot_bunnies' +require 'amqp' require 'thread' require 'json' require 'eventmachine' -import java.util.concurrent.Executors - include Jampb # add new field to client connection @@ -24,7 +22,7 @@ module JamWebsockets attr_accessor :user_context_lookup - def initialize(options={}) + def initialize() @log = Logging.logger[self] @pending_clients = Set.new # clients that have connected to server, but not logged in. @clients = {} # clients that have logged in @@ -36,21 +34,19 @@ module JamWebsockets @message_factory = JamRuby::MessageFactory.new @semaphore = Mutex.new @user_topic = nil - @user_subscription = nil @client_topic = nil - @client_subscription = nil @thread_pool = nil end - def start(options = {}) + def start(options={:host => "localhost", :port => 5432}) @log.info "startup" begin - @thread_pool = Executors.new_fixed_thread_pool(8) - @connection = HotBunnies.connect(:host => options[:host], :port => options[:port]) - @channel = @connection.create_channel - @channel.prefetch = 10 + + @connection = AMQP.connect(:host => options[:host]) + @channel = AMQP::Channel.new(@connection) + #@channel.prefetch = 10 register_topics rescue => e @@ -123,17 +119,14 @@ module JamWebsockets ######################## USER MESSAGING ########################### # create user exchange - @users_exchange = @channel.exchange('users', :type => :topic) + @users_exchange = @channel.topic('users') # create user messaging topic @user_topic = @channel.queue("", :auto_delete => true) @user_topic.bind(@users_exchange, :routing_key => "user.#") @user_topic.purge # subscribe for any messages to users - @user_subscription = @user_topic.subscribe(:ack => false) - - # this code serves as a callback that dequeues messages and processes them - @user_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| + @user_topic.subscribe(:ack => false) do |headers, msg| begin routing_key = headers.envelope.routing_key user_id = routing_key["user.".length..-1] @@ -166,15 +159,14 @@ module JamWebsockets ############## CLIENT MESSAGING ################### - @clients_exchange = @channel.exchange('clients', :type => :topic) + @clients_exchange = @channel.topic('clients') @client_topic = @channel.queue("", :auto_delete => true) @client_topic.bind(@clients_exchange, :routing_key => "client.#") @client_topic.purge # subscribe for any p2p messages to a client - @client_subscription = @client_topic.subscribe(:ack => false) - @client_subscription.each(:blocking => false, :executor => @threadpool) do |headers, msg| + @client_topic.subscribe(:ack => false) do |headers, msg| begin routing_key = headers.envelope.routing_key client_id = routing_key["client.".length..-1] @@ -307,24 +299,6 @@ module JamWebsockets def cleanup() # shutdown topic listeners and mq connection - begin - if !@user_subscription.nil? && @user_subscription.active? - @log.debug "cleaning up user subscription" - @user_subscription.cancel - @user_subscription.shutdown! - end - - if !@client_subscription.nil? && @client_subscription.active? - @log.debug "cleaning up client subscription" - @client_subscription.cancel - @client_subscription.shutdown! - end - - rescue => e - @log.debug "unable to cancel subscription on cleanup: #{e}" - end - - @thread_pool.shutdown if !@channel.nil? @channel.close diff --git a/spec/factories.rb b/spec/factories.rb index a5e6f4c1e..2618d0bc0 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -14,7 +14,7 @@ FactoryGirl.define do sequence(:description) { |n| "Jam Session #{n}" } end - factory :music_session_client, :class => JamRuby::MusicSessionClient do + factory :connection, :class => JamRuby::Connection do ip_address "1.1.1.1" end end diff --git a/spec/spec_db.rb b/spec/spec_db.rb index 7ac708075..1f2eb905d 100644 --- a/spec/spec_db.rb +++ b/spec/spec_db.rb @@ -2,25 +2,11 @@ class SpecDb TEST_DB_NAME="jam_websockets_test" - def self.recreate_database(db_config) - recreate_database_jdbc(db_config) - end - - def self.recreate_database_jdbc(db_config) - original = db_config["database"] - db_config["database"] = "postgres" - ActiveRecord::Base.establish_connection(db_config) - ActiveRecord::Base.connection.execute("DROP DATABASE IF EXISTS #{TEST_DB_NAME}") - ActiveRecord::Base.connection.execute("CREATE DATABASE #{TEST_DB_NAME}") - JamDb::Migrator.new.migrate(:dbname => TEST_DB_NAME) - db_config["database"] = original - end - - def self.recreate_database_pg - - conn = PG::Connection.open("dbname=postgres") + def self.recreate_database + conn = PG::Connection.open("dbname=postgres user=postgres password=postgres host=localhost") conn.exec("DROP DATABASE IF EXISTS #{TEST_DB_NAME}") conn.exec("CREATE DATABASE #{TEST_DB_NAME}") - JamDb::Migrator.new.migrate(:dbname => TEST_DB_NAME) + JamDb::Migrator.new.migrate(:dbname => TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost") end + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d1c8af6a1..203534124 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -23,7 +23,7 @@ Logging.logger.root.appenders = Logging.appenders.stdout # recreate test database and migrate it db_config = YAML::load(File.open('config/database.yml'))["test"] -SpecDb::recreate_database(db_config) +SpecDb::recreate_database() # initialize ActiveRecord's db connection ActiveRecord::Base.establish_connection(db_config) From 1660b2b01a04652e0bbeb3d52cc3e165318099e0 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 22 Oct 2012 22:16:14 -0500 Subject: [PATCH 28/98] * VRFS-18 syncing back to other comp --- Gemfile | 1 + bin/websocket_gateway | 2 +- lib/jam_websockets/router.rb | 7 +- lib/jam_websockets/server.rb | 18 ++-- spec/jam_websockets/router_spec.rb | 167 +++++++++++++++-------------- spec/spec_helper.rb | 3 +- 6 files changed, 103 insertions(+), 95 deletions(-) diff --git a/Gemfile b/Gemfile index 9ceeece71..449911a01 100644 --- a/Gemfile +++ b/Gemfile @@ -26,4 +26,5 @@ group :test do gem 'guard', '>= 0.10.0' gem 'guard-rspec', '>= 0.7.3' gem 'pg_migrate','0.1.5' #:path => "#{workspace}/pg_migrate_ruby" + gem 'amqp-spec' end diff --git a/bin/websocket_gateway b/bin/websocket_gateway index b82d6e035..480ca4f1a 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -1,4 +1,4 @@ -#!/usr/bin/env jruby +#!/usr/bin/env ruby require 'jam_websockets' diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 55fcdd099..5b1a6b708 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -38,22 +38,21 @@ module JamWebsockets @thread_pool = nil end - def start(options={:host => "localhost", :port => 5432}) + def start(options={:host => "localhost", :port => 5672}) @log.info "startup" begin - - @connection = AMQP.connect(:host => options[:host]) + @connection = AMQP.connect(:host => options[:host], :port => options[:port]) @channel = AMQP::Channel.new(@connection) #@channel.prefetch = 10 - register_topics rescue => e cleanup raise e end + @log.info "started" end def add_client(client_id, client) diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index dcc5a37e3..d3b37c8be 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -7,7 +7,7 @@ module JamWebsockets def initialize(options={}) @log = Logging.logger[self] @count=0 - @router = Router.new :transport => self + @router = Router.new end def run(options={}) @@ -17,20 +17,20 @@ module JamWebsockets @log.info "starting server #{host}:#{port}" - @router.start + EventMachine.run do + @router.start - # if you don't do this, the app won't exit unless you kill -9 - at_exit do - @log.info "cleaning up server" - @router.cleanup - end + # if you don't do this, the app won't exit unless you kill -9 + at_exit do + @log.info "cleaning up server" + @router.cleanup + end - EventMachine.run { EventMachine::WebSocket.start(:host => "0.0.0.0", :port => options[:port], :debug => options[:emwebsocket_debug]) do |ws| @log.info "new client #{ws}" @router.new_client(ws) end - } + end end end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 0dc7349fe..84940dd0d 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -74,137 +74,149 @@ def login_music_session(router, client, music_session) end -describe Router do +describe Router do + include AMQP::Spec message_factory = MessageFactory.new - before do - + amqp_before do @router = Router.new() @router.start() - end subject { @router } - after do + amqp_after do @router.stop end describe "servicability" do it "should start and stop", :mq => true do + em do + done + end end it "should register for client events", :mq => true do - client = double("client") - client.should_receive(:onopen) - client.should_receive(:onclose) - client.should_receive(:onerror) - client.should_receive(:onmessage) - client.should_receive(:encode_json=) - - @router.new_client(client) + em do + client = double("client") + client.should_receive(:onopen) + client.should_receive(:onclose) + client.should_receive(:onerror) + client.should_receive(:onmessage) + client.should_receive(:encode_json=) + @router.new_client(client) + done + end end end describe "topic routing helpers" do it "create and delete user lookup set" do - user = double(User) - user.should_receive(:id).any_number_of_times.and_return("1") - client = double("client") - context = ClientContext.new(user, client) + em do + user = double(User) + user.should_receive(:id).any_number_of_times.and_return("1") + client = double("client") + context = ClientContext.new(user, client) - @router.user_context_lookup.length.should == 0 + @router.user_context_lookup.length.should == 0 - @router.add_user(context) + @router.add_user(context) - @router.user_context_lookup.length.should == 1 + @router.user_context_lookup.length.should == 1 - @router.remove_user(context) + @router.remove_user(context) - @router.user_context_lookup.length.should == 0 + @router.user_context_lookup.length.should == 0 + done + end end end describe "login" do it "should not allow login of bogus user", :mq => true do - TestClient = Class.new do + em do + TestClient = Class.new do - attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id + attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id - def initialize() + def initialize() + end + + def onopen(&block) + @onopenblock = block + end + + def onmessage(&block) + @onmsgblock = block + end + + def close_websocket() + end + + def close() + end end - def onopen(&block) - @onopenblock = block - end + client = TestClient.new - def onmessage(&block) - @onmsgblock = block - end + error_msg = message_factory.server_rejection_error("invalid login") - def close_websocket() - end + @router.should_receive(:send_to_client).with(client, error_msg) + client.should_receive(:close_websocket) + client.should_receive(:onclose) + client.should_receive(:onerror) + client.should_receive(:request).and_return({"query" => {"pb" => "true"}}) - def close() - end + + @router.new_client(client) + client.onopenblock.call + + # create a login message, and pass it into the router via onmsgblock.call + login = message_factory.login_with_user_pass("baduser@example.com", "foobar") + + client.onmsgblock.call login.to_s + done end - - client = TestClient.new - - error_msg = message_factory.server_rejection_error("invalid login") - - @router.should_receive(:send_to_client).with(client, error_msg) - client.should_receive(:close_websocket) - client.should_receive(:onclose) - client.should_receive(:onerror) - client.should_receive(:request).and_return({"query" => {"pb" => "true"}}) - - - @router.new_client(client) - client.onopenblock.call - - # create a login message, and pass it into the router via onmsgblock.call - login = message_factory.login_with_user_pass("baduser@example.com", "foobar") - - client.onmsgblock.call login.to_s - end it "should allow login of valid user", :mq => true do - @user = User.new(:name => "Example User", :email => "user@example.com", - :password => "foobar", :password_confirmation => "foobar") - @user.save + em do + @user = User.new(:name => "Example User", :email => "user@example.com", + :password => "foobar", :password_confirmation => "foobar") + @user.save - client1 = login(@router, @user, "foobar", "1") + client1 = login(@router, @user, "foobar", "1") + done + end end it "should allow music_session_join of valid user", :mq => true do + em do + user1 = FactoryGirl.create(:user) # in the music session + user2 = FactoryGirl.create(:user) # in the music session + user3 = FactoryGirl.create(:user) # not in the music session - user1 = FactoryGirl.create(:user) # in the music session - user2 = FactoryGirl.create(:user) # in the music session - user3 = FactoryGirl.create(:user) # not in the music session + music_session = FactoryGirl.create(:music_session, :creator => user1) - music_session = FactoryGirl.create(:music_session, :creator => user1) + music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session, :client_id => "1") + music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") - music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session, :client_id => "1") - music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") - - # make a music_session and define two members - - # create client 1, log him in, and log him in to music session - client1 = login(@router, user1, "foobar", "1") - login_music_session(@router, client1, music_session) \ + # make a music_session and define two members + # create client 1, log him in, and log him in to music session + client1 = login(@router, user1, "foobar", "1") + login_music_session(@router, client1, music_session) \ + done + end end it "should allow two valid subscribers to communicate with session-directed messages", :mq => true do - - EventMachine.run do + em do user1 = FactoryGirl.create(:user) # in the music session user2 = FactoryGirl.create(:user) # in the music session @@ -223,14 +235,12 @@ describe Router do music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session, :client_id => "1") music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") - - EM.stop + done end end it "should allow two valid subscribers to communicate with p2p messages", :mq => true do - - EventMachine.run do + em do user1 = FactoryGirl.create(:user) # in the music session user2 = FactoryGirl.create(:user) # in the music session @@ -260,8 +270,7 @@ describe Router do #music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") - - EM.stop + done end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 203534124..0e2fb42f1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,9 @@ - require 'active_record' require 'jam_db' require 'spec_db' require 'jam_websockets' require 'timeout' - +require 'amqp-spec/rspec' jamenv = ENV['JAMENV'] jamenv ||= 'development' From 7a3b20814a6cb564bf813a5d81326cfd4e563fff Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 23 Oct 2012 06:42:28 -0500 Subject: [PATCH 29/98] * ConnectionManager integrated * ruby-1.9.3 instead of JRuby * Heartbeat Interval added to LoginAck --- Gemfile | 2 +- bin/websocket_gateway | 2 +- config/application.yml | 3 ++ lib/jam_websockets/router.rb | 37 ++++++++++-------- lib/jam_websockets/server.rb | 36 ++++++++++++++--- spec/jam_websockets/router_spec.rb | 62 +++++++++++++++--------------- spec/spec_helper.rb | 2 +- 7 files changed, 90 insertions(+), 54 deletions(-) diff --git a/Gemfile b/Gemfile index 449911a01..4ae6d0390 100644 --- a/Gemfile +++ b/Gemfile @@ -26,5 +26,5 @@ group :test do gem 'guard', '>= 0.10.0' gem 'guard-rspec', '>= 0.7.3' gem 'pg_migrate','0.1.5' #:path => "#{workspace}/pg_migrate_ruby" - gem 'amqp-spec' + gem 'evented-spec' end diff --git a/bin/websocket_gateway b/bin/websocket_gateway index 480ca4f1a..0b3c71ec7 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -30,4 +30,4 @@ end Logging.logger.root.appenders = Logging.appenders.stdout ActiveRecord::Base.establish_connection(db_config) -Server.new.run :port => config["port"], :emwebsocket_debug => config["emwebsocket_debug"] +Server.new.run :port => config["port"], :emwebsocket_debug => config["emwebsocket_debug"], :max_stale_connection_time => config["max_stale_connection_time"] diff --git a/config/application.yml b/config/application.yml index 34bf0c45d..9d31c4ccc 100644 --- a/config/application.yml +++ b/config/application.yml @@ -2,11 +2,14 @@ development: port: 6767 verbose: true emwebsocket_debug: false + max_stale_connection_time: 30 test: port: 6769 verbose: true + max_stale_connection_time: 30 production: port: 80 verbose: false + max_stale_connection_time: 30 \ No newline at end of file diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 5b1a6b708..03c394aea 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -36,12 +36,16 @@ module JamWebsockets @user_topic = nil @client_topic = nil @thread_pool = nil + @heartbeat_interval = nil + end - def start(options={:host => "localhost", :port => 5672}) + def start(max_stale_connection_time, options={:host => "localhost", :port => 5672}) @log.info "startup" + @heartbeat_interval = max_stale_connection_time / 2 + begin @connection = AMQP.connect(:host => options[:host], :port => options[:port]) @channel = AMQP::Channel.new(@connection) @@ -167,23 +171,23 @@ module JamWebsockets # subscribe for any p2p messages to a client @client_topic.subscribe(:ack => false) do |headers, msg| begin - routing_key = headers.envelope.routing_key + routing_key = headers.routing_key client_id = routing_key["client.".length..-1] @semaphore.synchronize do client = @client_lookup[client_id] msg = Jampb::ClientMessage.parse(msg) - @log.debug "p2p message received from #{msg.from} to client #{client_id}" + @log.debug "client-directed message received from #{msg.from} to client #{client_id}" unless client.nil? EM.schedule do - @log.debug "sending p2p message to #{client_id}" + @log.debug "sending client-directed down websocket to #{client_id}" send_to_client(client, msg) end else - @log.debug "p2p message unroutable to disconnected client #{client_id}" + @log.debug "client-directed message unroutable to disconnected client #{client_id}" end end rescue => e @@ -338,11 +342,11 @@ module JamWebsockets # remove this connection from the database if !context.user.nil? && !context.client.nil? - JamRuby::Connection.delete_all "user_id = '#{context.user.id}' AND client_id = '#{context.client.client_id}'" + ConnectionManager.active_record_transaction do |connection_manager| + connection_manager.delete_connection(client.client_id) + end end - send_friend_update(context.user, false, context.client) - remove_user(context) else @log.debug "skipping duplicate cleanup attempt of logged-in client" @@ -430,8 +434,9 @@ module JamWebsockets # respond with LOGIN_ACK to let client know it was successful #binding.pry - remote_port, remote_ip = Socket.unpack_sockaddr_in(client.get_peername) - login_ack = @message_factory.login_ack(remote_ip, client_id, user.remember_token) + + remote_ip = extract_ip(client) + login_ack = @message_factory.login_ack(remote_ip, client_id, user.remember_token, @heartbeat_interval) send_to_client(client, login_ack) @semaphore.synchronize do @@ -445,11 +450,8 @@ module JamWebsockets add_client(client_id, client) # TODO # log this connection in the database - connection = JamRuby::Connection.new(:user => user, :client_id => client.client_id) - @log.debug "Created connection => #{connection.user}, #{connection.client_id}" - - if connection.save - send_friend_update(user, true, context.client) + ConnectionManager.active_record_transaction do |connection_manager| + connection_manager.create_connection(user.id, client.client_id, extract_ip(client)) end end else @@ -457,6 +459,7 @@ module JamWebsockets end end + # TODO: deprecated; jam_ruby has routine inspired by this def send_friend_update(user, online, client) @log.debug "sending friend update for user #{user} online = #{online}" @@ -614,5 +617,9 @@ module JamWebsockets end end end + + def extract_ip(client) + return Socket.unpack_sockaddr_in(client.get_peername)[1] + end end end diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index d3b37c8be..d4eca90f8 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -14,11 +14,12 @@ module JamWebsockets host = "0.0.0.0" port = options[:port] + max_stale_connection_time = options[:max_stale_connection_time] - @log.info "starting server #{host}:#{port}" + @log.info "starting server #{host}:#{port} with staleness_time=#{max_stale_connection_time}" EventMachine.run do - @router.start + @router.start(max_stale_connection_time) # if you don't do this, the app won't exit unless you kill -9 at_exit do @@ -26,10 +27,33 @@ module JamWebsockets @router.cleanup end - EventMachine::WebSocket.start(:host => "0.0.0.0", :port => options[:port], :debug => options[:emwebsocket_debug]) do |ws| - @log.info "new client #{ws}" - @router.new_client(ws) - end + start_connection_cleaner(max_stale_connection_time) + + start_websocket_listener(host, port, options[:emwebsocket_debug]) + end + end + + def start_websocket_listener(listen_ip, port, emwebsocket_debug) + EventMachine::WebSocket.start(:host => listen_ip, :port => port, :debug => emwebsocket_debug) do |ws| + @log.info "new client #{ws}" + @router.new_client(ws) + end + end + + + def start_connection_cleaner(stale_max_time) + # one cleanup on startup + cleanup_stale_connections(stale_max_time) + + EventMachine::PeriodicTimer.new(15) do + cleanup_stale_connections(stale_max_time) + end + + end + + def cleanup_stale_connections(stale_max_time) + ConnectionManager.active_record_transaction do |connection_manager| + connection_manager.remove_stale_connections(stale_max_time) end end end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 84940dd0d..d7f80e176 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -42,13 +42,15 @@ def login(router, user, password, client_id) message_factory = MessageFactory.new client = LoginClient.new - login_ack = message_factory.login_ack("127.0.0.1", client_id, user.remember_token) + login_ack = message_factory.login_ack("127.0.0.1", client_id, user.remember_token, 15) router.should_receive(:send_to_client).with(client, login_ack) + router.should_receive(:extract_ip).at_least(:once).with(client).and_return("127.0.0.1") client.should_receive(:onclose) client.should_receive(:onerror) client.should_receive(:request).and_return({ "query" => { "pb" => "true" } }) - client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") + + #client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") @router.new_client(client) client.onopenblock.call @@ -75,31 +77,31 @@ end describe Router do - include AMQP::Spec + include EventedSpec::EMSpec message_factory = MessageFactory.new - amqp_before do + em_before do @router = Router.new() - @router.start() + @router.start(30) end subject { @router } - amqp_after do + em_after do @router.stop end - describe "servicability" do + describe "serviceability" do it "should start and stop", :mq => true do - em do + #em do done - end + #end end it "should register for client events", :mq => true do - em do + #em do client = double("client") client.should_receive(:onopen) client.should_receive(:onclose) @@ -109,13 +111,13 @@ describe Router do @router.new_client(client) done - end + #end end end describe "topic routing helpers" do it "create and delete user lookup set" do - em do + #em do user = double(User) user.should_receive(:id).any_number_of_times.and_return("1") client = double("client") @@ -131,14 +133,14 @@ describe Router do @router.user_context_lookup.length.should == 0 done - end + #end end end describe "login" do it "should not allow login of bogus user", :mq => true do - em do + #em do TestClient = Class.new do attr_accessor :onmsgblock, :onopenblock, :encode_json, :client_id @@ -181,42 +183,42 @@ describe Router do client.onmsgblock.call login.to_s done - end + #end end it "should allow login of valid user", :mq => true do - em do + #em do @user = User.new(:name => "Example User", :email => "user@example.com", :password => "foobar", :password_confirmation => "foobar") @user.save client1 = login(@router, @user, "foobar", "1") done - end + #end end it "should allow music_session_join of valid user", :mq => true do - em do + #em do user1 = FactoryGirl.create(:user) # in the music session user2 = FactoryGirl.create(:user) # in the music session user3 = FactoryGirl.create(:user) # not in the music session music_session = FactoryGirl.create(:music_session, :creator => user1) - music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session, :client_id => "1") - music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") + music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :client_id => "4") + music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :client_id => "5") # make a music_session and define two members # create client 1, log him in, and log him in to music session client1 = login(@router, user1, "foobar", "1") - login_music_session(@router, client1, music_session) \ + login_music_session(@router, client1, music_session) done - end + #end end it "should allow two valid subscribers to communicate with session-directed messages", :mq => true do - em do + #em do user1 = FactoryGirl.create(:user) # in the music session user2 = FactoryGirl.create(:user) # in the music session @@ -232,15 +234,15 @@ describe Router do # make a music_session and define two members - music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session, :client_id => "1") - music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") + music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :client_id => "6") + music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :client_id => "7") done - end + #end end it "should allow two valid subscribers to communicate with p2p messages", :mq => true do - em do + #em do user1 = FactoryGirl.create(:user) # in the music session user2 = FactoryGirl.create(:user) # in the music session @@ -254,7 +256,7 @@ describe Router do #login_music_session(@router, client2, music_session) # by creating - music_session_member1 = FactoryGirl.create(:music_session_client, :user => user1, :music_session => music_session, :client_id => "1") + music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :client_id => "8") # now attempt to message p2p! @@ -268,10 +270,10 @@ describe Router do ## send ping to client 2 #client2.onmsgblock.call ping.to_s - #music_session_member2 = FactoryGirl.create(:music_session_client, :user => user2, :music_session => music_session, :client_id => "2") + #music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :client_id => "2") done - end + #end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0e2fb42f1..2706e081b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,7 +3,7 @@ require 'jam_db' require 'spec_db' require 'jam_websockets' require 'timeout' -require 'amqp-spec/rspec' +require 'evented-spec' jamenv = ENV['JAMENV'] jamenv ||= 'development' From 18fee761ae23fe9315fa8f8340dd2fa9dda58913 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 27 Oct 2012 18:47:27 -0500 Subject: [PATCH 30/98] * fixing MusicSesssionClient to Connection --- lib/jam_websockets/router.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 03c394aea..7821dbb0a 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -545,7 +545,7 @@ module JamWebsockets return nil end - music_session_client = MusicSessionClient.find_by_client_id(client_id) + music_session_client = Connection.find_by_client_id(client_id) if music_session_client.nil? raise PermissionError, 'specified client not found' From 640e77e6011a23e72b88af1f61ed81ee0d8ac930 Mon Sep 17 00:00:00 2001 From: tihot_jk Date: Sun, 28 Oct 2012 17:24:16 -0700 Subject: [PATCH 31/98] Fixing a bug after MusicSessionClient was renamed to Connection. --- lib/jam_websockets/router.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 03c394aea..cb46871b3 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -545,13 +545,13 @@ module JamWebsockets return nil end - music_session_client = MusicSessionClient.find_by_client_id(client_id) + client_connection = Connection.find_by_client_id(client_id) - if music_session_client.nil? + if client_connection.nil? raise PermissionError, 'specified client not found' end - if !music_session_client.access_p2p? user + if !client_connection.access_p2p? user raise SessionError, 'not allowed to message this client' end end From eccfc477a31c1e9ba997ab07c512960ee81e4a96 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 5 Nov 2012 19:06:38 -0600 Subject: [PATCH 32/98] * bumping pg_migrate version to 0.1.6 --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 4ae6d0390..d7f2174f3 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,6 @@ group :test do gem 'database_cleaner', '0.7.0' gem 'guard', '>= 0.10.0' gem 'guard-rspec', '>= 0.7.3' - gem 'pg_migrate','0.1.5' #:path => "#{workspace}/pg_migrate_ruby" + gem 'pg_migrate','0.1.6' #:path => "#{workspace}/pg_migrate_ruby" gem 'evented-spec' end From 0674e5e1b294ad5d08052a0555dc86cca0e5f799 Mon Sep 17 00:00:00 2001 From: Mike Slemmer Date: Thu, 8 Nov 2012 20:51:27 -0800 Subject: [PATCH 33/98] added needed gem --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index d7f2174f3..03c68e527 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,7 @@ gem 'em-websocket'#, :path=> "#{workspace}/em-websocket-jam" gem 'amqp' gem 'activerecord', '3.2.7' gem 'logging' +gem 'tire' group :development do gem 'pry' From caaf4d6646928f39ea99e370787785ff5a9bf22f Mon Sep 17 00:00:00 2001 From: Mike Slemmer Date: Thu, 8 Nov 2012 20:55:18 -0800 Subject: [PATCH 34/98] added 2 more needed gems --- Gemfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gemfile b/Gemfile index 03c68e527..5ef34f8eb 100644 --- a/Gemfile +++ b/Gemfile @@ -12,6 +12,8 @@ gem 'amqp' gem 'activerecord', '3.2.7' gem 'logging' gem 'tire' +gem 'will_paginate' +gem 'rb-readline' group :development do gem 'pry' From d2f9a249c7489724f616801f153300bfc0181ff7 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 13 Nov 2012 21:07:28 -0600 Subject: [PATCH 35/98] * adding jam-ruby gems into websocket-gateway gemfile for reasons I don't fully understand --- Gemfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index d7f2174f3..ec37603ab 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,10 @@ gem 'em-websocket'#, :path=> "#{workspace}/em-websocket-jam" gem 'amqp' gem 'activerecord', '3.2.7' gem 'logging' +gem 'tire' +gem 'will_paginate' +gem 'actionmailer' +gem 'sendgrid' group :development do gem 'pry' From 9cee5acccde23e65c67a2822e5246b165b18a41a Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 17 Nov 2012 22:52:13 -0500 Subject: [PATCH 36/98] removed name --- spec/factories.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/factories.rb b/spec/factories.rb index 2618d0bc0..9021a075a 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,6 +1,5 @@ FactoryGirl.define do factory :user, :class => JamRuby::User do - sequence(:name) { |n| "Person #{n}" } sequence(:email) { |n| "person_#{n}@example.com"} password "foobar" password_confirmation "foobar" From 260f6419398b7fa1ec2d9fca73b88a3014ce6172 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 17 Nov 2012 22:47:21 -0600 Subject: [PATCH 37/98] * adding required params to music_session for facotry girl --- spec/factories.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/factories.rb b/spec/factories.rb index 9021a075a..6c5bac31c 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -4,6 +4,7 @@ FactoryGirl.define do password "foobar" password_confirmation "foobar" + factory :admin do admin true end @@ -11,6 +12,10 @@ FactoryGirl.define do factory :music_session, :class => JamRuby::MusicSession do sequence(:description) { |n| "Jam Session #{n}" } + fan_chat true + fan_access true + approval_required false + musician_access true end factory :connection, :class => JamRuby::Connection do From 61a4a10334f30171319e85537cd0e2ac824cbe79 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 18 Nov 2012 02:08:33 -0600 Subject: [PATCH 38/98] * websocket-gateway build added --- Gemfile | 18 +++++++++++++++--- build | 16 ++++++++++++++++ jenkins | 15 +++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100755 build create mode 100755 jenkins diff --git a/Gemfile b/Gemfile index 46aa9d75b..d6d5904ee 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,25 @@ +#ruby=1.9.3 source 'https://rubygems.org' +source 'https://jamjam:blueberryjam@www.jamkazam.com/gems/' # Look for $WORKSPACE, otherwise use "workspace" as dev path. workspace = ENV["WORKSPACE"] || "~/workspace" +devenv = ENV["BUILD_NUMBER"].nil? # Jenkins sets a build number environment variable + +if devenv + gem 'jam_db', :path=> "#{workspace}/jam-db/target/ruby_package" + gem 'jampb', :path => "#{workspace}/jam-pb/target/ruby/jampb" + gem 'jam_ruby', :path => "#{workspace}/jam-ruby" +else + gem 'jam_db' + gem 'jampb' + gem 'jam_ruby' +end + + gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'ruby-protocol-buffers', '1.2.2' -gem 'jam_ruby', :path => "#{workspace}/jam-ruby" -gem 'jampb', :path => "#{workspace}/jam-pb/target/ruby/jampb" gem 'em-websocket'#, :path=> "#{workspace}/em-websocket-jam" gem 'amqp' gem 'activerecord', '3.2.7' @@ -22,7 +35,6 @@ group :development do end group :test do - gem 'jam_db', :path => "#{workspace}/jam-db/target/ruby_package" gem 'cucumber' gem 'rspec' gem 'factory_girl' diff --git a/build b/build new file mode 100755 index 000000000..ec1b1fa31 --- /dev/null +++ b/build @@ -0,0 +1,16 @@ +#!/bin/bash + +echo "updating dependencies" +bundle update +echo "running rspec tests" +bundle exec rspec + +if [ "$?" = "0" ]; then + echo "tests completed" +else + echo "tests failed." + exit 1 +fi + +echo "build complete" + diff --git a/jenkins b/jenkins new file mode 100755 index 000000000..60eddfe68 --- /dev/null +++ b/jenkins @@ -0,0 +1,15 @@ +#!/bin/bash + +echo "starting build..." +./build + +if [ "$?" = "0" ]; then + echo "build succeeded" + + echo "TODO: build debian package" +else + echo "build failed" + exit 1 +fi + + From 9d6098f89f8573f4ebb201f78232b30a98a676c7 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 21 Nov 2012 14:49:45 -0500 Subject: [PATCH 39/98] fixed unit tests for user db changes --- spec/factories.rb | 6 +++++- spec/jam_websockets/router_spec.rb | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index 6c5bac31c..dbd53f241 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,9 +1,13 @@ FactoryGirl.define do factory :user, :class => JamRuby::User do sequence(:email) { |n| "person_#{n}@example.com"} + sequence(:first_name) { |n| "Person" } + sequence(:last_name) { |n| "#{n}" } password "foobar" password_confirmation "foobar" - + city "Apex" + state "NC" + country "USA" factory :admin do admin true diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index d7f80e176..23d7585f5 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -188,8 +188,9 @@ describe Router do it "should allow login of valid user", :mq => true do #em do - @user = User.new(:name => "Example User", :email => "user@example.com", - :password => "foobar", :password_confirmation => "foobar") + @user = User.new(:first_name => "Example", :last_name => "User", :email => "user@example.com", + :password => "foobar", :password_confirmation => "foobar", + :city => "Apex", :state => "NC", :country => "USA") @user.save client1 = login(@router, @user, "foobar", "1") From 989544bb35df9cfff3969b2b9334273e9c4cc9d8 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 3 Dec 2012 22:08:17 -0600 Subject: [PATCH 40/98] * fixing broken test in rspec --- spec/factories.rb | 11 +++++++++-- spec/jam_websockets/router_spec.rb | 5 ++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index 6c5bac31c..bcd30e8c1 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,9 +1,15 @@ FactoryGirl.define do factory :user, :class => JamRuby::User do sequence(:email) { |n| "person_#{n}@example.com"} + sequence(:first_name) { |n| "Person" } + sequence(:last_name) { |n| "#{n}" } password "foobar" password_confirmation "foobar" - + email_confirmed true + musician true + city "Apex" + state "NC" + country "USA" factory :admin do admin true @@ -12,7 +18,7 @@ FactoryGirl.define do factory :music_session, :class => JamRuby::MusicSession do sequence(:description) { |n| "Jam Session #{n}" } - fan_chat true + fan_chat true fan_access true approval_required false musician_access true @@ -20,5 +26,6 @@ FactoryGirl.define do factory :connection, :class => JamRuby::Connection do ip_address "1.1.1.1" + as_musician true end end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index d7f80e176..1cd1b8613 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -188,10 +188,9 @@ describe Router do it "should allow login of valid user", :mq => true do #em do - @user = User.new(:name => "Example User", :email => "user@example.com", + @user = FactoryGirl.create(:user, :password => "foobar", :password_confirmation => "foobar") - @user.save - + client1 = login(@router, @user, "foobar", "1") done #end From f964dfb0051502b2fc769f453222e571805eb9d0 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 6 Dec 2012 06:45:52 -0600 Subject: [PATCH 41/98] * p327 set as defaul --- .rvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rvmrc b/.rvmrc index 435cd9286..c63411491 100644 --- a/.rvmrc +++ b/.rvmrc @@ -1 +1 @@ -rvm use ruby-1.9.3@websockets --create +rvm use ruby-1.9.3-p327@websockets --create From ef2aa9e84796992c03335c887eea562a18d53bda Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 6 Dec 2012 06:46:15 -0600 Subject: [PATCH 42/98] * changing to p327 as defaul --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index d6d5904ee..219bfcc1b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ -#ruby=1.9.3 +#ruby=1.9.3-p327 source 'https://rubygems.org' source 'https://jamjam:blueberryjam@www.jamkazam.com/gems/' From 46789b453e2ed93af4407f8a38d99cbec022ce7d Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 9 Dec 2012 20:59:50 -0600 Subject: [PATCH 43/98] * adding legal_terms boolean to factory for VRFS-37 --- spec/factories.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/factories.rb b/spec/factories.rb index bcd30e8c1..edb12f257 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -22,6 +22,7 @@ FactoryGirl.define do fan_access true approval_required false musician_access true + legal_terms true end factory :connection, :class => JamRuby::Connection do From fb0ffd151f986742d8e2c90a8e9785fb341f582d Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 10 Dec 2012 07:30:45 -0600 Subject: [PATCH 44/98] * VRFS-168; making a gem out of websocket-gateway --- bin/websocket_gateway_win.sh | 0 jenkins | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) mode change 100644 => 100755 bin/websocket_gateway_win.sh diff --git a/bin/websocket_gateway_win.sh b/bin/websocket_gateway_win.sh old mode 100644 new mode 100755 diff --git a/jenkins b/jenkins index 60eddfe68..e43597b2f 100755 --- a/jenkins +++ b/jenkins @@ -5,11 +5,35 @@ echo "starting build..." if [ "$?" = "0" ]; then echo "build succeeded" + + # generate gem version based on jenkins build number + if [ -z $BUILD_NUMBER ]; then + BUILD_NUMBER="1" + fi + VERSION="0.0.${BUILD_NUMBER}" + echo "packaging gem jam_websockets-$VERSION" + cat > lib/jam_websockets/version.rb << EOF +module JamWebsockets + VERSION = "$VERSION" +end +EOF - echo "TODO: build debian package" + gem build jam_websockets.gemspec + + GEMNAME="jam_websockets-${VERSION}.gem" + + echo "publishing gem" + curl -f -T $GEMNAME $GEM_SERVER/$GEMNAME + + if [ "$?" != "0" ]; then + echo "publish failed" + exit 1 + fi + echo "done publishing gems" else echo "build failed" exit 1 fi + From 8e331aaf005c8ec0a74861a5c08f2755c30078e1 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 10 Dec 2012 07:35:08 -0600 Subject: [PATCH 45/98] * forget GEM_SERVER var --- jenkins | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jenkins b/jenkins index e43597b2f..4957148d4 100755 --- a/jenkins +++ b/jenkins @@ -1,5 +1,7 @@ #!/bin/bash +GEM_SERVER=http://localhost:9000/gems + echo "starting build..." ./build From a4882436ed14ac3f466de89eac25dec101c0389f Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 1 Jan 2013 21:48:59 -0600 Subject: [PATCH 46/98] * VRFS-188; make sure pry isn't added to requires because it's making jam-web complain about a lack of a tty --- lib/jam_websockets/router.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index cb46871b3..86d31a958 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -1,4 +1,3 @@ -require 'pry' require 'set' require 'amqp' require 'thread' From 5de82166b8c8381985ecbccee60c04e062a54436 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 3 Jan 2013 01:01:48 -0600 Subject: [PATCH 47/98] VRFS-190; initial attempt for websocket-gateway --- .gitignore | 2 ++ Gemfile | 4 ++++ build | 48 +++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index a35fe92d7..b001fe0df 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ tmp *~ *.swp *.iml +target +vendor diff --git a/Gemfile b/Gemfile index 219bfcc1b..f2897dd50 100644 --- a/Gemfile +++ b/Gemfile @@ -45,3 +45,7 @@ group :test do gem 'pg_migrate','0.1.6' #:path => "#{workspace}/pg_migrate_ruby" gem 'evented-spec' end + +group :package do + gem 'fpm' +end diff --git a/build b/build index ec1b1fa31..6105b8c28 100755 --- a/build +++ b/build @@ -1,16 +1,46 @@ #!/bin/bash -echo "updating dependencies" -bundle update -echo "running rspec tests" -bundle exec rspec +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -if [ "$?" = "0" ]; then - echo "tests completed" -else - echo "tests failed." - exit 1 +# 'target' is the output directory +rm -rf $DIR/target +mkdir $DIR/target +mkdir $DIR/target/deb + +# put all dependencies into vendor/bundle +#rm -rf vendor/bundle +echo "updating dependencies" + +bundle install --path vendor/bundle +bundle update + + +if [ -z $SKIP_TESTS ]; then + echo "running rspec tests" + bundle exec rspec + + if [ "$?" = "0" ]; then + echo "tests completed" + else + echo "tests failed." + exit 1 + fi fi +if [ -n "$PACKAGE" ]; then + if [ -z "$BUILD_NUMBER" ]; then + echo "BUILD NUMBER is not defined" + exit 1 + fi + set -e + # cache all gems local, and tell bundle to use local gems only + bundle install --path vendor/bundle --local + + # create debian using fpm + bundle exec fpm -s dir -t deb -p target/deb/websocket-gateway_0.1.${BUILD_NUMBER}_amd64.deb -n "websocket-gateway" -v "0.1.$BUILD_NUMBER" --prefix /var/lib/websocket-gateway --after-install $DIR/script/package/post-install.sh --before-install $DIR/script/package/pre-install.sh --before-remove $DIR/script/package/pre-uninstall.sh --after-remove $DIR/script/package/post-uninstall.sh --exclude $DIR/.git . + +fi + + echo "build complete" From ad989300d7e06dd15bd0bc565cb0e6c870041c15 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 3 Jan 2013 22:45:47 -0600 Subject: [PATCH 48/98] * adding packaging scripts to websocket-gateway --- script/package/passenger.sh | 6 +++++ script/package/post-install.sh | 13 ++++++++++ script/package/post-uninstall.sh | 27 ++++++++++++++++++++ script/package/pre-install.sh | 36 +++++++++++++++++++++++++++ script/package/pre-uninstall.sh | 0 script/package/upstart-run.sh | 11 ++++++++ script/package/websocket-gateway.conf | 7 ++++++ 7 files changed, 100 insertions(+) create mode 100755 script/package/passenger.sh create mode 100755 script/package/post-install.sh create mode 100755 script/package/post-uninstall.sh create mode 100755 script/package/pre-install.sh create mode 100755 script/package/pre-uninstall.sh create mode 100755 script/package/upstart-run.sh create mode 100755 script/package/websocket-gateway.conf diff --git a/script/package/passenger.sh b/script/package/passenger.sh new file mode 100755 index 000000000..810673e30 --- /dev/null +++ b/script/package/passenger.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +export BUILD_NUMBER=1 + +exec "/usr/local/rbenv/versions/1.9.3-p327/bin/ruby" "$@" + diff --git a/script/package/post-install.sh b/script/package/post-install.sh new file mode 100755 index 000000000..1d7ce14ba --- /dev/null +++ b/script/package/post-install.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -eu + +NAME="websocket-gateway" + +USER="$NAME" +GROUP="$NAME" + +# copy upstart file +cp /var/lib/$NAME/script/package/$NAME.conf /etc/init/$NAME.conf + +chown -R $USER:$GROUP /var/lib/$NAME diff --git a/script/package/post-uninstall.sh b/script/package/post-uninstall.sh new file mode 100755 index 000000000..e46246eae --- /dev/null +++ b/script/package/post-uninstall.sh @@ -0,0 +1,27 @@ +#!/bin/sh + + + +NAME="websocket-gateway" + +set -e +if [ "$1" = "remove" ] +then + set +e + # stop the process, if any is found. we don't want this failing to cause an error, though. + stop websocket-gateway + set -e + + if [ -f /etc/init/websocket-gateway.conf ]; then + rm /etc/init/websocket-gateway.conf + fi +fi + +if [ "$1" = "purge" ] +then + if [ -d /var/lib/$NAME ]; then + rm -rf /var/lib/$NAME + fi + + userdel $NAME +fi diff --git a/script/package/pre-install.sh b/script/package/pre-install.sh new file mode 100755 index 000000000..0cca8a624 --- /dev/null +++ b/script/package/pre-install.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +set -eu + +NAME="websocket-gateway" + +HOME="/var/lib/$NAME" +USER="$NAME" +GROUP="$NAME" + +# if NIS is used, then errors can occur but be non-fatal +if which ypwhich >/dev/null 2>&1 && ypwhich >/dev/null 2>&1 +then + set +e +fi + +if ! getent group "$GROUP" >/dev/null +then + addgroup --system "$GROUP" >/dev/null +fi + +# creating user if it isn't already there +if ! getent passwd "$USER" >/dev/null +then + adduser \ + --system \ + --home $HOME \ + --shell /bin/false \ + --disabled-login \ + --ingroup "$GROUP" \ + --gecos "$USER" server \ + "$USER" >/dev/null +fi + +# NISno longer a possible problem; stop ignoring errors +set -e diff --git a/script/package/pre-uninstall.sh b/script/package/pre-uninstall.sh new file mode 100755 index 000000000..e69de29bb diff --git a/script/package/upstart-run.sh b/script/package/upstart-run.sh new file mode 100755 index 000000000..2837317fb --- /dev/null +++ b/script/package/upstart-run.sh @@ -0,0 +1,11 @@ +#!/bin/bash -l + +# default config values +BUILD_NUMBER=1 + +CONFIG_FILE="/etc/websocket-gateway/upstart.conf" +if [ -e "$CONFIG_FILE" ]; then + . "$CONFIG_FILE" +fi + +BUILD_NUMBER=$BUILD_NUMBER exec bundle exec ruby -Ilib bin/websocket_gateway diff --git a/script/package/websocket-gateway.conf b/script/package/websocket-gateway.conf new file mode 100755 index 000000000..c51febf4b --- /dev/null +++ b/script/package/websocket-gateway.conf @@ -0,0 +1,7 @@ +description "websocket-gateway" + +start on startup +start on runlevel [2345] +stop on runlevel [016] + +exec start-stop-daemon --start --chdir /var/lib/websocket-gateway --exec /var/lib/websocket-gateway/script/package/upstart-run.sh From 0ac75396533a6f53fa98f2124a77499d614d8c16 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 5 Jan 2013 02:34:58 -0600 Subject: [PATCH 49/98] * VRFS-190; get production mode working with a rolling file appender --- bin/websocket_gateway | 10 +++++----- build | 12 +++++++++++- config/application.yml | 4 ++-- config/database.yml | 11 +++++++++++ log/.gitkeep | 0 log/production.1.log | 12 ++++++++++++ log/production.log | 4 ++++ 7 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 log/.gitkeep create mode 100644 log/production.1.log create mode 100644 log/production.log diff --git a/bin/websocket_gateway b/bin/websocket_gateway index 0b3c71ec7..bb1e6297b 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -14,10 +14,6 @@ bin_dir = File.expand_path(File.dirname(__FILE__)) app_config_file = File.join(bin_dir, '..', 'config', 'application.yml') db_config_file = File.join(bin_dir, '..', 'config', 'database.yml') -# limit execution in base dir of project -#app_config_file = File.join('config', 'application.yml') -#db_config_file = File.join('config', 'database.yml') - config = YAML::load(File.open(app_config_file))[jamenv] db_config = YAML::load(File.open(db_config_file))[jamenv] @@ -27,7 +23,11 @@ else Logging.logger.root.level = :info end -Logging.logger.root.appenders = Logging.appenders.stdout +if jamenv == "production" + Logging.logger.root.appenders = Logging.appenders.rolling_file("log/#{jamenv}.log", :truncate=>true) +else + Logging.logger.root.appenders = Logging.appenders.stdout +end ActiveRecord::Base.establish_connection(db_config) Server.new.run :port => config["port"], :emwebsocket_debug => config["emwebsocket_debug"], :max_stale_connection_time => config["max_stale_connection_time"] diff --git a/build b/build index 6105b8c28..65f6880fa 100755 --- a/build +++ b/build @@ -32,12 +32,22 @@ if [ -n "$PACKAGE" ]; then echo "BUILD NUMBER is not defined" exit 1 fi + + type -P dpkg-architecture > /dev/null + + if [ "$?" = "0" ]; then + ARCH=`dpkg-architecture -qDEB_HOST_ARCH` + else + echo "WARN: unable to determine architecture." + ARCH=`all` + fi + set -e # cache all gems local, and tell bundle to use local gems only bundle install --path vendor/bundle --local # create debian using fpm - bundle exec fpm -s dir -t deb -p target/deb/websocket-gateway_0.1.${BUILD_NUMBER}_amd64.deb -n "websocket-gateway" -v "0.1.$BUILD_NUMBER" --prefix /var/lib/websocket-gateway --after-install $DIR/script/package/post-install.sh --before-install $DIR/script/package/pre-install.sh --before-remove $DIR/script/package/pre-uninstall.sh --after-remove $DIR/script/package/post-uninstall.sh --exclude $DIR/.git . + bundle exec fpm -s dir -t deb -p target/deb/websocket-gateway_0.1.${BUILD_NUMBER}_${ARCH}.deb -n "websocket-gateway" -v "0.1.$BUILD_NUMBER" --prefix /var/lib/websocket-gateway --after-install $DIR/script/package/post-install.sh --before-install $DIR/script/package/pre-install.sh --before-remove $DIR/script/package/pre-uninstall.sh --after-remove $DIR/script/package/post-uninstall.sh --exclude $DIR/.git . fi diff --git a/config/application.yml b/config/application.yml index 9d31c4ccc..df89cddc9 100644 --- a/config/application.yml +++ b/config/application.yml @@ -10,6 +10,6 @@ test: max_stale_connection_time: 30 production: - port: 80 + port: 6767 verbose: false - max_stale_connection_time: 30 \ No newline at end of file + max_stale_connection_time: 30 diff --git a/config/database.yml b/config/database.yml index c1ad9e417..435b92b3f 100644 --- a/config/database.yml +++ b/config/database.yml @@ -19,3 +19,14 @@ development: password: postgres timeout: 2000 encoding: unicode + +production: + adapter: postgresql + database: jam + host: localhost + port: 5432 + pool: 3 + username: postgres + password: postgres + timeout: 2000 + encoding: unicode diff --git a/log/.gitkeep b/log/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/log/production.1.log b/log/production.1.log new file mode 100644 index 000000000..779a32c33 --- /dev/null +++ b/log/production.1.log @@ -0,0 +1,12 @@ + INFO JamWebsockets::Server : starting server 0.0.0.0:6767 with staleness_time=30 + INFO JamWebsockets::Router : startup + INFO JamWebsockets::Router : started + INFO JamWebsockets::Server : cleaning up server + INFO JamWebsockets::Server : starting server 0.0.0.0:6767 with staleness_time=30 + INFO JamWebsockets::Router : startup + INFO JamWebsockets::Router : started + INFO JamWebsockets::Server : cleaning up server + INFO JamWebsockets::Server : starting server 0.0.0.0:6767 with staleness_time=30 + INFO JamWebsockets::Router : startup + INFO JamWebsockets::Router : started + INFO JamWebsockets::Server : cleaning up server diff --git a/log/production.log b/log/production.log new file mode 100644 index 000000000..a7bcc7ebd --- /dev/null +++ b/log/production.log @@ -0,0 +1,4 @@ + INFO JamWebsockets::Server : starting server 0.0.0.0:6767 with staleness_time=30 + INFO JamWebsockets::Router : startup + INFO JamWebsockets::Router : started + INFO JamWebsockets::Server : cleaning up server From 26f9c222c44497786e4203829dd993aa99616ae8 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 5 Jan 2013 02:38:42 -0600 Subject: [PATCH 50/98] * VRFS-190 - rolling file in production: keep 20, max file size of 1 meg or day --- bin/websocket_gateway | 3 ++- log/production.1.log | 8 -------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/bin/websocket_gateway b/bin/websocket_gateway index bb1e6297b..f49239554 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -24,7 +24,8 @@ else end if jamenv == "production" - Logging.logger.root.appenders = Logging.appenders.rolling_file("log/#{jamenv}.log", :truncate=>true) + one_meg = 1024 * 1024 + Logging.logger.root.appenders = Logging.appenders.rolling_file("log/#{jamenv}.log", :truncate=>true, :age=>'daily', :size=>one_meg, :keep=>20) else Logging.logger.root.appenders = Logging.appenders.stdout end diff --git a/log/production.1.log b/log/production.1.log index 779a32c33..a7bcc7ebd 100644 --- a/log/production.1.log +++ b/log/production.1.log @@ -2,11 +2,3 @@ INFO JamWebsockets::Router : startup INFO JamWebsockets::Router : started INFO JamWebsockets::Server : cleaning up server - INFO JamWebsockets::Server : starting server 0.0.0.0:6767 with staleness_time=30 - INFO JamWebsockets::Router : startup - INFO JamWebsockets::Router : started - INFO JamWebsockets::Server : cleaning up server - INFO JamWebsockets::Server : starting server 0.0.0.0:6767 with staleness_time=30 - INFO JamWebsockets::Router : startup - INFO JamWebsockets::Router : started - INFO JamWebsockets::Server : cleaning up server From fbfff6ac7f37af10b3c38eb8cdb291f91c426a31 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 5 Jan 2013 02:39:10 -0600 Subject: [PATCH 51/98] * VRFS-190 - rolling file in production: keep 20, max file size of 1 meg or day --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b001fe0df..7540559aa 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ spec/reports test/tmp test/version_tmp tmp - +log/*.log .idea *~ *.swp From 5d2be5eb94f44ed0d4698faeda059ac5c8ef405b Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 5 Jan 2013 02:40:05 -0600 Subject: [PATCH 52/98] * VRFS-190; gitignore/remove accidentally added log files --- .gitignore | 2 +- log/production.1.log | 4 ---- log/production.log | 4 ---- 3 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 log/production.1.log delete mode 100644 log/production.log diff --git a/.gitignore b/.gitignore index 7540559aa..f2cd712b0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ spec/reports test/tmp test/version_tmp tmp -log/*.log +log/* .idea *~ *.swp diff --git a/log/production.1.log b/log/production.1.log deleted file mode 100644 index a7bcc7ebd..000000000 --- a/log/production.1.log +++ /dev/null @@ -1,4 +0,0 @@ - INFO JamWebsockets::Server : starting server 0.0.0.0:6767 with staleness_time=30 - INFO JamWebsockets::Router : startup - INFO JamWebsockets::Router : started - INFO JamWebsockets::Server : cleaning up server diff --git a/log/production.log b/log/production.log deleted file mode 100644 index a7bcc7ebd..000000000 --- a/log/production.log +++ /dev/null @@ -1,4 +0,0 @@ - INFO JamWebsockets::Server : starting server 0.0.0.0:6767 with staleness_time=30 - INFO JamWebsockets::Router : startup - INFO JamWebsockets::Router : started - INFO JamWebsockets::Server : cleaning up server From d505ae77eb8171f595e72df04595b9d573bc4536 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 5 Jan 2013 23:43:23 -0600 Subject: [PATCH 53/98] * when updating staleness of connection, skip validations --- lib/jam_websockets/router.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 86d31a958..f68d0094f 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -479,12 +479,14 @@ module JamWebsockets def handle_heartbeat(heartbeat, client) context = @clients[client] - @log.debug "updating timestamp for user #{context}" connection = Connection.find_by_user_id_and_client_id(context.user.id, context.client.client_id) - unless connection.nil? + if connection.nil? + @log.debug "unable to find connection due to heartbeat from client: #{context}" + else + @log.debug "updating connection freshness due to heartbeat from client: #{context}" connection.updated_at = DateTime.now - connection.save + connection.save(:validate => false) # validates are unneeded, as we are just touching updated_at end end From 22e83eb1d2e8944d1f9a95a6f106c226b51f291b Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 5 Jan 2013 07:56:16 -0600 Subject: [PATCH 54/98] * VRFS-190 done; sudo start websocket-gateway /sudo stop websocket-gateway possible after install of ubuntu package --- build | 2 +- script/package/post-install.sh | 2 ++ script/package/post-uninstall.sh | 2 +- script/package/pre-install.sh | 2 +- script/package/upstart-run.sh | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build b/build index 65f6880fa..240f646fb 100755 --- a/build +++ b/build @@ -47,7 +47,7 @@ if [ -n "$PACKAGE" ]; then bundle install --path vendor/bundle --local # create debian using fpm - bundle exec fpm -s dir -t deb -p target/deb/websocket-gateway_0.1.${BUILD_NUMBER}_${ARCH}.deb -n "websocket-gateway" -v "0.1.$BUILD_NUMBER" --prefix /var/lib/websocket-gateway --after-install $DIR/script/package/post-install.sh --before-install $DIR/script/package/pre-install.sh --before-remove $DIR/script/package/pre-uninstall.sh --after-remove $DIR/script/package/post-uninstall.sh --exclude $DIR/.git . + bundle exec fpm -s dir -t deb -p target/deb/websocket-gateway_0.1.${BUILD_NUMBER}_${ARCH}.deb -n "websocket-gateway" -v "0.1.$BUILD_NUMBER" --prefix /var/lib/websocket-gateway --after-install $DIR/script/package/post-install.sh --before-install $DIR/script/package/pre-install.sh --before-remove $DIR/script/package/pre-uninstall.sh --after-remove $DIR/script/package/post-uninstall.sh Gemfile lib bin vendor .bundle config script fi diff --git a/script/package/post-install.sh b/script/package/post-install.sh index 1d7ce14ba..fb09f8d7f 100755 --- a/script/package/post-install.sh +++ b/script/package/post-install.sh @@ -10,4 +10,6 @@ GROUP="$NAME" # copy upstart file cp /var/lib/$NAME/script/package/$NAME.conf /etc/init/$NAME.conf +mkdir -p /var/lib/$NAME/log + chown -R $USER:$GROUP /var/lib/$NAME diff --git a/script/package/post-uninstall.sh b/script/package/post-uninstall.sh index e46246eae..976157e2e 100755 --- a/script/package/post-uninstall.sh +++ b/script/package/post-uninstall.sh @@ -10,7 +10,7 @@ then set +e # stop the process, if any is found. we don't want this failing to cause an error, though. stop websocket-gateway - set -e + set -e if [ -f /etc/init/websocket-gateway.conf ]; then rm /etc/init/websocket-gateway.conf diff --git a/script/package/pre-install.sh b/script/package/pre-install.sh index 0cca8a624..2c12b2e5c 100755 --- a/script/package/pre-install.sh +++ b/script/package/pre-install.sh @@ -28,7 +28,7 @@ then --shell /bin/false \ --disabled-login \ --ingroup "$GROUP" \ - --gecos "$USER" server \ + --gecos "$USER" \ "$USER" >/dev/null fi diff --git a/script/package/upstart-run.sh b/script/package/upstart-run.sh index 2837317fb..a018db654 100755 --- a/script/package/upstart-run.sh +++ b/script/package/upstart-run.sh @@ -8,4 +8,4 @@ if [ -e "$CONFIG_FILE" ]; then . "$CONFIG_FILE" fi -BUILD_NUMBER=$BUILD_NUMBER exec bundle exec ruby -Ilib bin/websocket_gateway +BUILD_NUMBER=$BUILD_NUMBER JAMENV=production exec bundle exec ruby -Ilib bin/websocket_gateway From 516a3dd6c0d29829bc664965d5dd353415d652f8 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 5 Jan 2013 23:12:23 -0600 Subject: [PATCH 55/98] * trying to publish websocket deb to local jenkins server --- jenkins | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/jenkins b/jenkins index 4957148d4..20cd4c6e1 100755 --- a/jenkins +++ b/jenkins @@ -28,10 +28,27 @@ EOF curl -f -T $GEMNAME $GEM_SERVER/$GEMNAME if [ "$?" != "0" ]; then - echo "publish failed" + echo "gem publish failed" exit 1 fi - echo "done publishing gems" + echo "done publishing gem" + + if [ ! -z "$PACKAGE" ]; then + echo "publishing ubuntu package (.deb)" + DEB_SERVER=http://localhost:9000/apt-i386 + DEBPATH=`find target/deb -name *.deb` + DEBNAME=`basename $DEBPATH` + + curl -f -T $DEBPATH $DEB_SERVER/$DEBNAME + + if [ "$?" != "0" ]; then + echo "deb publish failed" + exit 1 + fi + echo "done publishing deb" + + + fi else echo "build failed" exit 1 From d157a58db787926be16e3c5ec6a47309ec7e9e54 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 5 Jan 2013 23:18:26 -0600 Subject: [PATCH 56/98] * deb server --- jenkins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins b/jenkins index 20cd4c6e1..b5133f94d 100755 --- a/jenkins +++ b/jenkins @@ -1,6 +1,7 @@ #!/bin/bash GEM_SERVER=http://localhost:9000/gems +DEB_SERVER=http://localhost:9010/apt-i386 echo "starting build..." ./build @@ -35,7 +36,6 @@ EOF if [ ! -z "$PACKAGE" ]; then echo "publishing ubuntu package (.deb)" - DEB_SERVER=http://localhost:9000/apt-i386 DEBPATH=`find target/deb -name *.deb` DEBNAME=`basename $DEBPATH` From 3344ffa1868149020eecc7a175555106979fd41f Mon Sep 17 00:00:00 2001 From: jam Date: Thu, 10 Jan 2013 14:07:25 -0600 Subject: [PATCH 57/98] All all messages through to peers irrespective of type --- lib/jam_websockets/router.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index f68d0094f..c416ba35c 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -541,6 +541,7 @@ module JamWebsockets # client = the current client def access_p2p(client_id, user, msg) + return nil # ping_request and ping_ack messages are special in that they are simply allowed if msg.type == ClientMessage::Type::PING_REQUEST || msg.type == ClientMessage::Type::PING_ACK return nil From ef819532bb614c81a0d92e2ad82aae2afbe3eb1b Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 13 Jan 2013 22:51:36 -0600 Subject: [PATCH 58/98] VRFS-217; removing tire reference --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index f2897dd50..3ea9500b7 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,6 @@ gem 'em-websocket'#, :path=> "#{workspace}/em-websocket-jam" gem 'amqp' gem 'activerecord', '3.2.7' gem 'logging' -gem 'tire' gem 'will_paginate' gem 'actionmailer' gem 'sendgrid' From 81d747edd6b5697f4ba63bb9cb1e16de9cf6491a Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 14 Jan 2013 00:03:46 -0600 Subject: [PATCH 59/98] * remove vendor/bundle --- build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build b/build index 240f646fb..0cb842ea4 100755 --- a/build +++ b/build @@ -8,7 +8,7 @@ mkdir $DIR/target mkdir $DIR/target/deb # put all dependencies into vendor/bundle -#rm -rf vendor/bundle +rm -rf vendor/bundle echo "updating dependencies" bundle install --path vendor/bundle From 93bea675e59aa6b1624071b3c9f8ad7658ed87ec Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 16 Jan 2013 22:40:42 -0600 Subject: [PATCH 60/98] * remove Gemfile.lock right before running --- script/package/upstart-run.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/script/package/upstart-run.sh b/script/package/upstart-run.sh index a018db654..95ccbade6 100755 --- a/script/package/upstart-run.sh +++ b/script/package/upstart-run.sh @@ -8,4 +8,10 @@ if [ -e "$CONFIG_FILE" ]; then . "$CONFIG_FILE" fi +# I don't like doing this, but the next command (bundle exec) retouches/generates +# the gemfile. This unfortunately means the next debian update doesn't update this file. +# Ultimately this means an old Gemfile.lock is left behind for a new package, +# and bundle won't run because it thinks it has the wrong versions of gems +rm Gemfile.lock + BUILD_NUMBER=$BUILD_NUMBER JAMENV=production exec bundle exec ruby -Ilib bin/websocket_gateway From b759b2ae01144d720385b221196498429815154a Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 16 Jan 2013 22:51:04 -0600 Subject: [PATCH 61/98] * -f so no error if not present --- script/package/upstart-run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/package/upstart-run.sh b/script/package/upstart-run.sh index 95ccbade6..af5cbee66 100755 --- a/script/package/upstart-run.sh +++ b/script/package/upstart-run.sh @@ -12,6 +12,6 @@ fi # the gemfile. This unfortunately means the next debian update doesn't update this file. # Ultimately this means an old Gemfile.lock is left behind for a new package, # and bundle won't run because it thinks it has the wrong versions of gems -rm Gemfile.lock +rm -f Gemfile.lock BUILD_NUMBER=$BUILD_NUMBER JAMENV=production exec bundle exec ruby -Ilib bin/websocket_gateway From 0a41cc6ee908110943e42d23d6c70fbafc91001f Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 26 Jan 2013 19:55:09 -0600 Subject: [PATCH 62/98] * pinning to 0.3.8 because client.request is no longer a property in 0.4.0 of em-websokce --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 3ea9500b7..16dc06cc6 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ end gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'ruby-protocol-buffers', '1.2.2' -gem 'em-websocket'#, :path=> "#{workspace}/em-websocket-jam" +gem 'em-websocket', '0.3.8'#, :path=> "#{workspace}/em-websocket-jam" gem 'amqp' gem 'activerecord', '3.2.7' gem 'logging' From f38cf2a1f6b8925b3686af516e491a4d61bf4b46 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 26 Jan 2013 20:15:03 -0600 Subject: [PATCH 63/98] * fixe for em-websocket 0.4.0 --- Gemfile | 2 +- lib/jam_websockets/router.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 16dc06cc6..59599d31c 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ end gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'ruby-protocol-buffers', '1.2.2' -gem 'em-websocket', '0.3.8'#, :path=> "#{workspace}/em-websocket-jam" +gem 'em-websocket' #, :path=> "#{workspace}/em-websocket-jam" gem 'amqp' gem 'activerecord', '3.2.7' gem 'logging' diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index c416ba35c..c8bda4bea 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -206,12 +206,12 @@ module JamWebsockets # default to using json instead of pb client.encode_json = true - client.onopen { + client.onopen { |handshake| #binding.pry @log.debug "client connected #{client}" # check for '?pb' or '?pb=true' in url query parameters - query_pb = client.request["query"]["pb"] + query_pb = handshake.query["pb"] if !query_pb.nil? && (query_pb == "" || query_pb == "true") client.encode_json = false From 5557edadff6b01d2b97e08a379a4f52263f774c8 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 26 Jan 2013 20:15:58 -0600 Subject: [PATCH 64/98] * making sure version is greater than 0.4.0 --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 59599d31c..5396d4423 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ end gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'ruby-protocol-buffers', '1.2.2' -gem 'em-websocket' #, :path=> "#{workspace}/em-websocket-jam" +gem 'em-websocket', '>=0.4.0' #, :path=> "#{workspace}/em-websocket-jam" gem 'amqp' gem 'activerecord', '3.2.7' gem 'logging' From 12822972ab78f0d1deb0c27397ea77b90a74c588 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 26 Jan 2013 20:29:17 -0600 Subject: [PATCH 65/98] * fix broken tests --- spec/jam_websockets/router_spec.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index c684b429a..4141e6974 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -48,12 +48,13 @@ def login(router, user, password, client_id) router.should_receive(:extract_ip).at_least(:once).with(client).and_return("127.0.0.1") client.should_receive(:onclose) client.should_receive(:onerror) - client.should_receive(:request).and_return({ "query" => { "pb" => "true" } }) #client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") @router.new_client(client) - client.onopenblock.call + handshake = double("handshake") + handshake.should_receive(:query).and_return({ "pb" => "true" }) + client.onopenblock.call handshake # create a login message, and pass it into the router via onmsgblock.call login = message_factory.login_with_user_pass(user.email, password, :client_id => client_id) @@ -172,11 +173,12 @@ describe Router do client.should_receive(:close_websocket) client.should_receive(:onclose) client.should_receive(:onerror) - client.should_receive(:request).and_return({"query" => {"pb" => "true"}}) @router.new_client(client) - client.onopenblock.call + handshake = double("handshake") + handshake.should_receive(:query).and_return({"pb" => "true"}) + client.onopenblock.call handshake # create a login message, and pass it into the router via onmsgblock.call login = message_factory.login_with_user_pass("baduser@example.com", "foobar") From 2140eff3bdfc6c6b370174774f7d6da34801621c Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Wed, 6 Feb 2013 07:43:26 -0600 Subject: [PATCH 66/98] vrfs192: added aasm gem; added max_reconnect_time parameter --- Gemfile | 1 + bin/websocket_gateway | 5 ++++- config/application.yml | 10 +++++++--- lib/jam_websockets/router.rb | 18 ++++++++++++++---- lib/jam_websockets/server.rb | 20 +++++++++++++++++++- spec/jam_websockets/router_spec.rb | 2 +- spec/spec_helper.rb | 6 +++++- 7 files changed, 51 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 5396d4423..5bc617a9c 100644 --- a/Gemfile +++ b/Gemfile @@ -28,6 +28,7 @@ gem 'will_paginate' gem 'actionmailer' gem 'sendgrid' gem 'rb-readline' +gem 'aasm', '3.0.16' group :development do gem 'pry' diff --git a/bin/websocket_gateway b/bin/websocket_gateway index f49239554..8260d500b 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -31,4 +31,7 @@ else end ActiveRecord::Base.establish_connection(db_config) -Server.new.run :port => config["port"], :emwebsocket_debug => config["emwebsocket_debug"], :max_stale_connection_time => config["max_stale_connection_time"] +Server.new.run(:port => config["port"], + :emwebsocket_debug => config["emwebsocket_debug"], + :max_stale_connection_time => config["max_stale_connection_time"], + :max_reconnect_time => config["max_reconnect_time"]) diff --git a/config/application.yml b/config/application.yml index df89cddc9..ef2c4e214 100644 --- a/config/application.yml +++ b/config/application.yml @@ -1,15 +1,19 @@ +Defaults: &defaults + max_stale_connection_time: 30 + max_reconnect_time: 180 + development: port: 6767 verbose: true emwebsocket_debug: false - max_stale_connection_time: 30 + <<: *defaults test: port: 6769 verbose: true - max_stale_connection_time: 30 + <<: *defaults production: port: 6767 verbose: false - max_stale_connection_time: 30 + <<: *defaults diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index c8bda4bea..b83e513e1 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -433,9 +433,15 @@ module JamWebsockets # respond with LOGIN_ACK to let client know it was successful #binding.pry + + connection = JamRuby::Connection.find_by_client_id(client_id) remote_ip = extract_ip(client) - login_ack = @message_factory.login_ack(remote_ip, client_id, user.remember_token, @heartbeat_interval) + login_ack = @message_factory.login_ack(remote_ip, + client_id, + user.remember_token, + @heartbeat_interval, + connection.try(:music_session_id)) send_to_client(client, login_ack) @semaphore.synchronize do @@ -448,9 +454,13 @@ module JamWebsockets add_user(context) add_client(client_id, client) # TODO - # log this connection in the database - ConnectionManager.active_record_transaction do |connection_manager| - connection_manager.create_connection(user.id, client.client_id, extract_ip(client)) + if connection + connection.connect! + else + # log this connection in the database + ConnectionManager.active_record_transaction do |connection_manager| + connection_manager.create_connection(user.id, client.client_id, extract_ip(client)) + end end end else diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index d4eca90f8..be738bba1 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -15,6 +15,7 @@ module JamWebsockets host = "0.0.0.0" port = options[:port] max_stale_connection_time = options[:max_stale_connection_time] + max_reconnect_time = options[:max_reconnect_time] @log.info "starting server #{host}:#{port} with staleness_time=#{max_stale_connection_time}" @@ -27,7 +28,8 @@ module JamWebsockets @router.cleanup end - start_connection_cleaner(max_stale_connection_time) + start_connection_flagger(max_stale_connection_time) + start_connection_cleaner(max_reconnect_time) start_websocket_listener(host, port, options[:emwebsocket_debug]) end @@ -56,6 +58,22 @@ module JamWebsockets connection_manager.remove_stale_connections(stale_max_time) end end + + def start_connection_flagger(flag_max_time) + # one cleanup on startup + flag_stale_connections(flag_max_time) + + EventMachine::PeriodicTimer.new(15) do + flag_stale_connections(flag_max_time) + end + end + + def flag_stale_connections(flag_max_time) + ConnectionManager.active_record_transaction do |connection_manager| + connection_manager.flag_stale_connections(flag_max_time) + end + end + end end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 4141e6974..919d39f87 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -42,7 +42,7 @@ def login(router, user, password, client_id) message_factory = MessageFactory.new client = LoginClient.new - login_ack = message_factory.login_ack("127.0.0.1", client_id, user.remember_token, 15) + login_ack = message_factory.login_ack("127.0.0.1", client_id, user.remember_token, 15, nil) router.should_receive(:send_to_client).with(client, login_ack) router.should_receive(:extract_ip).at_least(:once).with(client).and_return("127.0.0.1") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2706e081b..e4c25691f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,8 +8,12 @@ require 'evented-spec' jamenv = ENV['JAMENV'] jamenv ||= 'development' -config = YAML::load(File.open('config/application.yml'))[jamenv] +fn = "#{File.dirname(__FILE__)}/../config/application.yml" +puts "*** spec_helper.rb: fn=#{fn}; #{File.exists?(fn)}; #{jamenv}" +ff = File.open("#{File.dirname(__FILE__)}/../config/application.yml",'r') +config = YAML::load(ff)[jamenv] +puts "*** spec_helper.rb: jamenv=#{jamenv}; config = #{config}" if config["verbose"] Logging.logger.root.level = :debug From 1c1949010eb32360dde663bd46a960e2dfdd44b6 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 11 Feb 2013 00:04:41 -0600 Subject: [PATCH 67/98] renamed connection timers to correspond to state names --- bin/websocket_gateway | 4 ++-- config/application.yml | 4 ++-- lib/jam_websockets/router.rb | 4 ++-- lib/jam_websockets/server.rb | 34 ++++++++++++++++++++-------------- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/bin/websocket_gateway b/bin/websocket_gateway index 8260d500b..ead97d4ac 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -33,5 +33,5 @@ end ActiveRecord::Base.establish_connection(db_config) Server.new.run(:port => config["port"], :emwebsocket_debug => config["emwebsocket_debug"], - :max_stale_connection_time => config["max_stale_connection_time"], - :max_reconnect_time => config["max_reconnect_time"]) + :connect_time_stale => config["connect_time_stale"], + :connect_time_expire => config["connect_time_expire"]) diff --git a/config/application.yml b/config/application.yml index ef2c4e214..f0576f674 100644 --- a/config/application.yml +++ b/config/application.yml @@ -1,6 +1,6 @@ Defaults: &defaults - max_stale_connection_time: 30 - max_reconnect_time: 180 + connect_time_stale: 30 + connect_time_expire: 180 development: port: 6767 diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index b83e513e1..0309f4373 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -39,11 +39,11 @@ module JamWebsockets end - def start(max_stale_connection_time, options={:host => "localhost", :port => 5672}) + def start(connect_time_stale, options={:host => "localhost", :port => 5672}) @log.info "startup" - @heartbeat_interval = max_stale_connection_time / 2 + @heartbeat_interval = connect_time_stale / 2 begin @connection = AMQP.connect(:host => options[:host], :port => options[:port]) diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index be738bba1..158047619 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -14,13 +14,13 @@ module JamWebsockets host = "0.0.0.0" port = options[:port] - max_stale_connection_time = options[:max_stale_connection_time] - max_reconnect_time = options[:max_reconnect_time] + connect_time_stale = options[:connect_time_stale].to_i + connect_time_expire = options[:connect_time_expire].to_i - @log.info "starting server #{host}:#{port} with staleness_time=#{max_stale_connection_time}" + @log.info "starting server #{host}:#{port} with staleness_time=#{connect_time_stale}; reconnect time = #{connect_time_expire}" EventMachine.run do - @router.start(max_stale_connection_time) + @router.start(connect_time_stale) # if you don't do this, the app won't exit unless you kill -9 at_exit do @@ -28,8 +28,8 @@ module JamWebsockets @router.cleanup end - start_connection_flagger(max_stale_connection_time) - start_connection_cleaner(max_reconnect_time) + start_connection_expiration(connect_time_expire) + start_connection_flagger(connect_time_stale) start_websocket_listener(host, port, options[:emwebsocket_debug]) end @@ -42,34 +42,40 @@ module JamWebsockets end end - - def start_connection_cleaner(stale_max_time) + def start_connection_expiration(stale_max_time) # one cleanup on startup - cleanup_stale_connections(stale_max_time) + @log.info("*** start_connection_expiration: #{stale_max_time}") - EventMachine::PeriodicTimer.new(15) do - cleanup_stale_connections(stale_max_time) + expire_stale_connections(stale_max_time) + + EventMachine::PeriodicTimer.new(stale_max_time) do + @log.info("*** start_connection_expiration: TIMER #{stale_max_time}") + expire_stale_connections(stale_max_time) end end - def cleanup_stale_connections(stale_max_time) + def expire_stale_connections(stale_max_time) ConnectionManager.active_record_transaction do |connection_manager| - connection_manager.remove_stale_connections(stale_max_time) + connection_manager.expire_stale_connections(stale_max_time) end end def start_connection_flagger(flag_max_time) + @log.info("*** start_connection_flagger: #{flag_max_time}") + # one cleanup on startup flag_stale_connections(flag_max_time) - EventMachine::PeriodicTimer.new(15) do + EventMachine::PeriodicTimer.new(flag_max_time) do + @log.info("*** start_connection_flagger: TIMER #{flag_max_time}") flag_stale_connections(flag_max_time) end end def flag_stale_connections(flag_max_time) ConnectionManager.active_record_transaction do |connection_manager| + @log.info("*** flag_stale_connections: TIMER #{flag_max_time}") connection_manager.flag_stale_connections(flag_max_time) end end From 08976ec71a3b35ba979f3b1367b8e4b5d76d3d81 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 11 Feb 2013 00:53:00 -0600 Subject: [PATCH 68/98] updated logging stmts for connection state mgmt --- lib/jam_websockets/server.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index 158047619..5f38e4a29 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -44,38 +44,33 @@ module JamWebsockets def start_connection_expiration(stale_max_time) # one cleanup on startup - @log.info("*** start_connection_expiration: #{stale_max_time}") - expire_stale_connections(stale_max_time) EventMachine::PeriodicTimer.new(stale_max_time) do - @log.info("*** start_connection_expiration: TIMER #{stale_max_time}") expire_stale_connections(stale_max_time) end end def expire_stale_connections(stale_max_time) + @log.debug("*** expire_stale_connections: fires each #{stale_max_time} seconds") ConnectionManager.active_record_transaction do |connection_manager| connection_manager.expire_stale_connections(stale_max_time) end end def start_connection_flagger(flag_max_time) - @log.info("*** start_connection_flagger: #{flag_max_time}") - # one cleanup on startup flag_stale_connections(flag_max_time) EventMachine::PeriodicTimer.new(flag_max_time) do - @log.info("*** start_connection_flagger: TIMER #{flag_max_time}") flag_stale_connections(flag_max_time) end end def flag_stale_connections(flag_max_time) + @log.debug("*** flag_stale_connections: fires each #{flag_max_time} seconds") ConnectionManager.active_record_transaction do |connection_manager| - @log.info("*** flag_stale_connections: TIMER #{flag_max_time}") connection_manager.flag_stale_connections(flag_max_time) end end From 245ab0583b81c47b47e0361d6c328513ef7fc3b7 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 12 Feb 2013 22:57:04 -0600 Subject: [PATCH 69/98] adding carrierwave to gemfile because jamruby has it as a dependency and local gems seem to not pull in transitive dependencies --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 5bc617a9c..dfc9595c7 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,7 @@ gem 'actionmailer' gem 'sendgrid' gem 'rb-readline' gem 'aasm', '3.0.16' +gem 'carrierwave' group :development do gem 'pry' From 71cea79f36b540e968d818137082eb2484164687 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 21 Feb 2013 11:49:32 -0600 Subject: [PATCH 70/98] added method stale_client, cleanup_clients_with_ids; in handle_heartbeat added updating of connection state --- lib/jam_websockets/router.rb | 50 +++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 0309f4373..f58cdc7ee 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -198,7 +198,6 @@ module JamWebsockets def new_client(client) - @semaphore.synchronize do @pending_clients.add(client) end @@ -221,8 +220,7 @@ module JamWebsockets client.onclose { @log.debug "Connection closed" - - cleanup_client(client) + stale_client(client) } client.onerror { |error| @@ -321,10 +319,38 @@ module JamWebsockets cleanup end + # caused a client connection to be marked stale + def stale_client(client) + if cid = client.client_id + ConnectionManager.active_record_transaction do |connection_manager| + connection_manager.flag_connection_stale_with_client_id(cid) + end + end + end + + def cleanup_clients_with_ids(client_ids) + @log.debug("*** cleanup_clients_with_ids: client_ids = #{client_ids.inspect}") + client_ids.each do |cid| + if 0 < (ws_clients = @clients.keys).length + ws_clients.each do |client| + if cid == client.client_id + self.cleanup_client(client) + break + else + @log.debug("*** cleanup_clients: deleting connection = #{cid}") + ConnectionManager.active_record_transaction { |mgr| mgr.delete_connection(cid) } + end + end + else + ConnectionManager.active_record_transaction { |mgr| mgr.delete_connection(cid) } + end + end + end + # removes all resources associated with a client def cleanup_client(client) - @semaphore.synchronize do + # @log.debug("*** cleanup_clients: client = #{client}") pending = @pending_clients.delete?(client) if !pending.nil? @@ -395,6 +421,7 @@ module JamWebsockets end def handle_server_directed(client_msg, client) + # @log.info("*** handle_server_directed(#{client_msg}, #{client})") if client_msg.type == ClientMessage::Type::LOGIN @@ -411,6 +438,8 @@ module JamWebsockets def handle_login(login, client) + @log.info("*** handle_login: login=#{login}; client=#{client}") + username = login.username if login.value_for_tag(1) password = login.password if login.value_for_tag(2) token = login.token if login.value_for_tag(3) @@ -429,7 +458,7 @@ module JamWebsockets if !user.nil? - @log.debug "user #{user.email} logged in" + @log.debug "user #{user} logged in" # respond with LOGIN_ACK to let client know it was successful #binding.pry @@ -495,8 +524,11 @@ module JamWebsockets @log.debug "unable to find connection due to heartbeat from client: #{context}" else @log.debug "updating connection freshness due to heartbeat from client: #{context}" - connection.updated_at = DateTime.now - connection.save(:validate => false) # validates are unneeded, as we are just touching updated_at + if connection.stale? + connection.connect! + else + connection.touch + end end end @@ -505,10 +537,10 @@ module JamWebsockets if !token.nil? && token != '' @log.debug "logging in via token" # attempt login with token - user = User.find_by_remember_token(token) + user = JamRuby::User.find_by_remember_token(token) if user.nil? - @log.debug "no user found with token" + @log.debug "no user found with token #{token}" return false else @log.debug "#{user} login via token" From d794482ab04d9a3a7f45d093fbb699478504265a Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 21 Feb 2013 11:53:10 -0600 Subject: [PATCH 71/98] updated the expire duration by subtracting the stale time; replaced ConnectionManager.expire_stale_connections with stale_connection_client_ids and call to @router.cleanup_clients_with_ids --- lib/jam_websockets/server.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index 5f38e4a29..5d9c735cc 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -27,8 +27,11 @@ module JamWebsockets @log.info "cleaning up server" @router.cleanup end - - start_connection_expiration(connect_time_expire) + + # take stale off the expire limit because the call to stale will + # touch the updated_at column, adding an extra stale limit to the expire time limit + expire_time = connect_time_expire > connect_time_stale ? connect_time_expire - connect_time_stale : connect_time_expire + start_connection_expiration(expire_time) start_connection_flagger(connect_time_stale) start_websocket_listener(host, port, options[:emwebsocket_debug]) @@ -53,10 +56,12 @@ module JamWebsockets end def expire_stale_connections(stale_max_time) - @log.debug("*** expire_stale_connections: fires each #{stale_max_time} seconds") + client_ids = [] ConnectionManager.active_record_transaction do |connection_manager| - connection_manager.expire_stale_connections(stale_max_time) + client_ids = connection_manager.stale_connection_client_ids(stale_max_time) end + @log.debug("*** expire_stale_connections(#{stale_max_time}): client_ids = #{client_ids.inspect}") + @router.cleanup_clients_with_ids(client_ids) end def start_connection_flagger(flag_max_time) From 991312f6ad1227d8a4e7252774c96eebcc3da781 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 21 Feb 2013 13:38:18 -0600 Subject: [PATCH 72/98] removed debug stmts --- lib/jam_websockets/server.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index 5d9c735cc..3000c36af 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -60,7 +60,7 @@ module JamWebsockets ConnectionManager.active_record_transaction do |connection_manager| client_ids = connection_manager.stale_connection_client_ids(stale_max_time) end - @log.debug("*** expire_stale_connections(#{stale_max_time}): client_ids = #{client_ids.inspect}") + # @log.debug("*** expire_stale_connections(#{stale_max_time}): client_ids = #{client_ids.inspect}") @router.cleanup_clients_with_ids(client_ids) end @@ -74,7 +74,7 @@ module JamWebsockets end def flag_stale_connections(flag_max_time) - @log.debug("*** flag_stale_connections: fires each #{flag_max_time} seconds") + # @log.debug("*** flag_stale_connections: fires each #{flag_max_time} seconds") ConnectionManager.active_record_transaction do |connection_manager| connection_manager.flag_stale_connections(flag_max_time) end From c1dc7d99678b8303b627ffb7b76394b226c64e20 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 21 Feb 2013 14:06:35 -0600 Subject: [PATCH 73/98] added msg arg to debug for send_to_client --- lib/jam_websockets/router.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index f58cdc7ee..10014e686 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -287,7 +287,7 @@ module JamWebsockets def send_to_client(client, msg) - @log.debug "SEND TO CLIENT START" + @log.debug "SEND TO CLIENT START (#{msg})" if client.encode_json client.send(msg.to_json.to_s) else From 876e897ac5dc1e6a67c1ac4608cfa85a4ceaac4e Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 21 Feb 2013 14:18:17 -0600 Subject: [PATCH 74/98] removed debug stmt --- lib/jam_websockets/router.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 10014e686..28e4324f8 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -329,7 +329,7 @@ module JamWebsockets end def cleanup_clients_with_ids(client_ids) - @log.debug("*** cleanup_clients_with_ids: client_ids = #{client_ids.inspect}") + # @log.debug("*** cleanup_clients_with_ids: client_ids = #{client_ids.inspect}") client_ids.each do |cid| if 0 < (ws_clients = @clients.keys).length ws_clients.each do |client| From a9f815ee526c74b27698ba51935fb3f22ce6791d Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 21 Feb 2013 19:41:09 -0600 Subject: [PATCH 75/98] from add_client, removed warning when connection still exists; in handle_login retrieve connection earlier and create new client_id if no connection object; in handle_heartbeat, reconnect the connection and touch updated_at inside connection_manager --- lib/jam_websockets/router.rb | 53 ++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 28e4324f8..f24794d7d 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -59,12 +59,6 @@ module JamWebsockets end def add_client(client_id, client) - - # should never occur - if @client_lookup.has_key?(client_id) - @log.warn "client_id #{client_id} connected while the old connection has not yet been terminated" - end - @client_lookup[client_id] = client end @@ -337,7 +331,7 @@ module JamWebsockets self.cleanup_client(client) break else - @log.debug("*** cleanup_clients: deleting connection = #{cid}") + # @log.debug("*** cleanup_clients: deleting connection = #{cid}") ConnectionManager.active_record_transaction { |mgr| mgr.delete_connection(cid) } end end @@ -421,7 +415,7 @@ module JamWebsockets end def handle_server_directed(client_msg, client) - # @log.info("*** handle_server_directed(#{client_msg}, #{client})") + # @log.info("*** handle_server_directed(#{client_msg.inspect}, #{client})") if client_msg.type == ClientMessage::Type::LOGIN @@ -437,19 +431,30 @@ module JamWebsockets end def handle_login(login, client) - - @log.info("*** handle_login: login=#{login}; client=#{client}") - username = login.username if login.value_for_tag(1) password = login.password if login.value_for_tag(2) token = login.token if login.value_for_tag(3) client_id = login.client_id if login.value_for_tag(4) + @log.info("*** handle_login: token=#{token}; client_id=#{client_id}") + connection = nil + # you don't have to supply client_id in login--if you don't, we'll generate one if client_id.nil? || client_id.empty? # give a unique ID to this client. This is used to prevent session messages # from echoing back to the sender, for instance. client_id = UUIDTools::UUID.random_create.to_s + else + # check if there's a connection for the client... if it's stale, reconnect it + if connection = JamRuby::Connection.find_by_client_id(client_id) + # FIXME: I think connection table needs to updated within connection_manager + # otherwise this would be 1 line of code (connection.connect!) + ConnectionManager.active_record_transaction do |connection_manager| + connection_manager.reconnect(connection) + end if connection.stale? + end + # if there's a client_id but no connection object, create new client_id + client_id = UUIDTools::UUID.random_create.to_s if !connection end client.client_id = client_id @@ -457,14 +462,11 @@ module JamWebsockets user = valid_login(username, password, token, client_id) if !user.nil? - @log.debug "user #{user} logged in" # respond with LOGIN_ACK to let client know it was successful #binding.pry - connection = JamRuby::Connection.find_by_client_id(client_id) - remote_ip = extract_ip(client) login_ack = @message_factory.login_ack(remote_ip, client_id, @@ -483,9 +485,7 @@ module JamWebsockets add_user(context) add_client(client_id, client) # TODO - if connection - connection.connect! - else + unless connection # log this connection in the database ConnectionManager.active_record_transaction do |connection_manager| connection_manager.create_connection(user.id, client.client_id, extract_ip(client)) @@ -517,17 +517,18 @@ module JamWebsockets end def handle_heartbeat(heartbeat, client) - context = @clients[client] - connection = Connection.find_by_user_id_and_client_id(context.user.id, context.client.client_id) - - if connection.nil? - @log.debug "unable to find connection due to heartbeat from client: #{context}" + unless context = @clients[client] + @log.warn "*** WARNING: unable to find context due to heartbeat from client: #{client.client_id}; calling cleanup" + cleanup_client(client) else - @log.debug "updating connection freshness due to heartbeat from client: #{context}" - if connection.stale? - connection.connect! + connection = Connection.find_by_user_id_and_client_id(context.user.id, context.client.client_id) + if connection.nil? + @log.warn "*** WARNING: unable to find connection due to heartbeat from client: #{context}; calling cleanup_client" + cleanup_client(client) else - connection.touch + ConnectionManager.active_record_transaction do |connection_manager| + connection_manager.reconnect(connection) + end if connection.stale? end end end From 984fad86e03d2d0a7a3f79627ac1981125704a44 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 28 Feb 2013 23:17:03 -0600 Subject: [PATCH 76/98] fixed failing tests on send_to_client by inspecting args (rather than trying to match different login_ack objects) --- spec/jam_websockets/router_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 919d39f87..5fc26c8d7 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -44,7 +44,11 @@ def login(router, user, password, client_id) login_ack = message_factory.login_ack("127.0.0.1", client_id, user.remember_token, 15, nil) - router.should_receive(:send_to_client).with(client, login_ack) + router.should_receive(:send_to_client) do |*args| + args.count.should == 2 + args[0].should == client + args[1].is_a?(Jampb::ClientMessage).should be_true + end router.should_receive(:extract_ip).at_least(:once).with(client).and_return("127.0.0.1") client.should_receive(:onclose) client.should_receive(:onerror) From 9e4468d1af419508b05f949ec054cdb95a67a475 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 14 Mar 2013 23:24:25 -0500 Subject: [PATCH 77/98] * VRFS-259: beta signup --- Gemfile | 1 + bin/websocket_gateway | 22 ++++++++++++++-------- lib/jam_websockets/router.rb | 2 +- spec/factories.rb | 16 ++++++++++++++++ spec/spec_helper.rb | 35 +++++++++++++++++++++-------------- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/Gemfile b/Gemfile index dfc9595c7..3cbd82156 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,7 @@ gem 'sendgrid' gem 'rb-readline' gem 'aasm', '3.0.16' gem 'carrierwave' +gem 'devise' group :development do gem 'pry' diff --git a/bin/websocket_gateway b/bin/websocket_gateway index ead97d4ac..6b186d8d7 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -1,5 +1,19 @@ #!/usr/bin/env ruby +# establish database connection before including JamRuby +require 'active_record' +bin_dir = File.expand_path(File.dirname(__FILE__)) + +app_config_file = File.join(bin_dir, '..', 'config', 'application.yml') +db_config_file = File.join(bin_dir, '..', 'config', 'database.yml') + +config = YAML::load(File.open(app_config_file))[jamenv] +db_config = YAML::load(File.open(db_config_file))[jamenv] + +ActiveRecord::Base.establish_connection(db_config) + + +# now bring in the Jam code require 'jam_websockets' include JamWebsockets @@ -9,13 +23,6 @@ include JamWebsockets jamenv = ENV['JAMENV'] jamenv ||= 'development' -bin_dir = File.expand_path(File.dirname(__FILE__)) - -app_config_file = File.join(bin_dir, '..', 'config', 'application.yml') -db_config_file = File.join(bin_dir, '..', 'config', 'database.yml') - -config = YAML::load(File.open(app_config_file))[jamenv] -db_config = YAML::load(File.open(db_config_file))[jamenv] if config["verbose"] Logging.logger.root.level = :debug @@ -30,7 +37,6 @@ else Logging.logger.root.appenders = Logging.appenders.stdout end -ActiveRecord::Base.establish_connection(db_config) Server.new.run(:port => config["port"], :emwebsocket_debug => config["emwebsocket_debug"], :connect_time_stale => config["connect_time_stale"], diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index f24794d7d..4911df028 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -554,7 +554,7 @@ module JamWebsockets # attempt login with username and password user = User.find_by_email(username) - if !user.nil? && user.authenticate(password) + if !user.nil? && user.valid_password?(password) @log.debug "#{user} login via password" return user else diff --git a/spec/factories.rb b/spec/factories.rb index edb12f257..3522a5e5f 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -10,10 +10,16 @@ FactoryGirl.define do city "Apex" state "NC" country "USA" + terms_of_service true + factory :admin do admin true end + + before(:create) do |user| + user.musician_instruments << FactoryGirl.build(:musician_instrument, user: user) + end end factory :music_session, :class => JamRuby::MusicSession do @@ -29,4 +35,14 @@ FactoryGirl.define do ip_address "1.1.1.1" as_musician true end + + factory :instrument, :class => JamRuby::Instrument do + description { |n| "Instrument #{n}" } + end + + factory :musician_instrument, :class=> JamRuby::MusicianInstrument do + instrument { Instrument.find('electric guitar') } + proficiency_level 1 + priority 0 + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e4c25691f..cf25b462a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,15 @@ require 'active_record' require 'jam_db' require 'spec_db' + +# recreate test database and migrate it +db_config = YAML::load(File.open('config/database.yml'))["test"] + +SpecDb::recreate_database() +# initialize ActiveRecord's db connection +ActiveRecord::Base.establish_connection(db_config) + + require 'jam_websockets' require 'timeout' require 'evented-spec' @@ -23,12 +32,6 @@ end Logging.logger.root.appenders = Logging.appenders.stdout -# recreate test database and migrate it -db_config = YAML::load(File.open('config/database.yml'))["test"] - -SpecDb::recreate_database() -# initialize ActiveRecord's db connection -ActiveRecord::Base.establish_connection(db_config) require 'jam_ruby' require 'jampb' @@ -67,20 +70,24 @@ include Jampb config.run_all_when_everything_filtered = true config.filter_run :focus - config.before(:suite) do - # DatabaseCleaner.strategy = :transaction - # DatabaseCleaner.clean_with(:truncation) - end - config.before(:each) do - # DatabaseCleaner.start + DatabaseCleaner.start end config.after(:each) do - ActiveRecord::Base.connection.execute('select truncate_tables()') - # DatabaseCleaner.clean + DatabaseCleaner.clean end + config.before(:suite) do + DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] } + DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres] }) + end + + #config.after(:each) do + # ActiveRecord::Base.connection.execute('select truncate_tables()') + # # DatabaseCleaner.clean + # end + # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. From 5ca044120798a3691a248f6b3394ef661bd9fb32 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 14 Mar 2013 23:54:20 -0500 Subject: [PATCH 78/98] * fixing startup of websocket-gatceway --- bin/websocket_gateway | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bin/websocket_gateway b/bin/websocket_gateway index 6b186d8d7..19cf17391 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -6,7 +6,8 @@ bin_dir = File.expand_path(File.dirname(__FILE__)) app_config_file = File.join(bin_dir, '..', 'config', 'application.yml') db_config_file = File.join(bin_dir, '..', 'config', 'database.yml') - +jamenv = ENV['JAMENV'] +jamenv ||= 'development' config = YAML::load(File.open(app_config_file))[jamenv] db_config = YAML::load(File.open(db_config_file))[jamenv] @@ -20,9 +21,6 @@ include JamWebsockets # run some method -jamenv = ENV['JAMENV'] -jamenv ||= 'development' - if config["verbose"] Logging.logger.root.level = :debug From 13215ce0d3c116668ad706ff7d6c182df1af4123 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 27 Mar 2013 16:02:00 -0500 Subject: [PATCH 79/98] * fixing one egregrous error --- lib/jam_websockets/router.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 4911df028..491064a03 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -82,6 +82,7 @@ module JamWebsockets end def add_user(context) + puts "adding user lookup entry for context.user.id #{context.user.id}" user_contexts = @user_context_lookup[context.user.id] if user_contexts.nil? user_contexts = Set.new @@ -89,6 +90,7 @@ module JamWebsockets end user_contexts.add(context) + puts "dumping user_contexts: count #{@user_context_lookup}" end def remove_user(client_context) @@ -124,7 +126,7 @@ module JamWebsockets # subscribe for any messages to users @user_topic.subscribe(:ack => false) do |headers, msg| begin - routing_key = headers.envelope.routing_key + routing_key = headers.routing_key user_id = routing_key["user.".length..-1] @semaphore.synchronize do @@ -143,7 +145,7 @@ module JamWebsockets end end else - @log.debug "Context is null" + @log.debug "Can't route message: no user connected with id #{user_id}" end end From daff290db57f209f445bc13e248a36341ee6036e Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 27 Mar 2013 16:02:33 -0500 Subject: [PATCH 80/98] * taking out spurious puts --- lib/jam_websockets/router.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 491064a03..f3f4968bd 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -82,7 +82,6 @@ module JamWebsockets end def add_user(context) - puts "adding user lookup entry for context.user.id #{context.user.id}" user_contexts = @user_context_lookup[context.user.id] if user_contexts.nil? user_contexts = Set.new @@ -90,7 +89,6 @@ module JamWebsockets end user_contexts.add(context) - puts "dumping user_contexts: count #{@user_context_lookup}" end def remove_user(client_context) From 11458f0d39d6f7d9e210018131234c1d6e842a2b Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 18 May 2013 06:50:39 -0500 Subject: [PATCH 81/98] * updating pg_migrate to 0.1.7 to match the rest of the site --- Gemfile | 2 +- lib/jam_websockets/router.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 3cbd82156..aaa36e7e9 100644 --- a/Gemfile +++ b/Gemfile @@ -44,7 +44,7 @@ group :test do gem 'database_cleaner', '0.7.0' gem 'guard', '>= 0.10.0' gem 'guard-rspec', '>= 0.7.3' - gem 'pg_migrate','0.1.6' #:path => "#{workspace}/pg_migrate_ruby" + gem 'pg_migrate','0.1.7' #:path => "#{workspace}/pg_migrate_ruby" gem 'evented-spec' end diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index f3f4968bd..5f3e07913 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -542,7 +542,7 @@ module JamWebsockets if user.nil? @log.debug "no user found with token #{token}" - return false + return nil else @log.debug "#{user} login via token" return user From 512731f0174c45b03f41ecdabaea758c73e012ad Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 18 May 2013 07:01:10 -0500 Subject: [PATCH 82/98] * adding postgres-copy --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index aaa36e7e9..6ed3c0165 100644 --- a/Gemfile +++ b/Gemfile @@ -31,6 +31,7 @@ gem 'rb-readline' gem 'aasm', '3.0.16' gem 'carrierwave' gem 'devise' +gem 'postgres-copy' group :development do gem 'pry' From 1a475a4238d4dc26a32afcc32bb40f0ba6107e58 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 23 May 2013 18:16:14 -0500 Subject: [PATCH 83/98] * adding new gem --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 6ed3c0165..0a38b9796 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'aasm', '3.0.16' gem 'carrierwave' gem 'devise' gem 'postgres-copy' +gem 'aws-sdk' group :development do gem 'pry' From 808eb6f280aecf57d13f99ee7ace6fa207472463 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 30 May 2013 21:00:12 -0500 Subject: [PATCH 84/98] * websocket-gateway to use ruby 2.0.0 --- .ruby-gemset | 1 + .ruby-version | 1 + .rvmrc | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .ruby-gemset create mode 100644 .ruby-version delete mode 100644 .rvmrc diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 000000000..14774b465 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +websockets diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000..95a5ad2d5 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.0.0-p195 diff --git a/.rvmrc b/.rvmrc deleted file mode 100644 index c63411491..000000000 --- a/.rvmrc +++ /dev/null @@ -1 +0,0 @@ -rvm use ruby-1.9.3-p327@websockets --create From 3507bf5b790b2ca2e42747316bba11ca24add02d Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 5 Jul 2013 14:00:51 -0500 Subject: [PATCH 85/98] * updating pg_migrate --- Gemfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 0a38b9796..d906a4c5f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,8 @@ #ruby=1.9.3-p327 source 'https://rubygems.org' -source 'https://jamjam:blueberryjam@www.jamkazam.com/gems/' +unless ENV['LOCAL_DEV'] == '1' + source 'https://jamjam:blueberryjam@www.jamkazam.com/gems/' +end # Look for $WORKSPACE, otherwise use "workspace" as dev path. workspace = ENV["WORKSPACE"] || "~/workspace" @@ -46,7 +48,7 @@ group :test do gem 'database_cleaner', '0.7.0' gem 'guard', '>= 0.10.0' gem 'guard-rspec', '>= 0.7.3' - gem 'pg_migrate','0.1.7' #:path => "#{workspace}/pg_migrate_ruby" + gem 'pg_migrate','0.1.11' #:path => "#{workspace}/pg_migrate_ruby" gem 'evented-spec' end From 404c07db1b6d2477d47c617a9bc4cd684381cc37 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 14 Jul 2013 21:50:34 -0500 Subject: [PATCH 86/98] * VRFS-196, VRFS-195, VRFS-197 - integrate amqp_connection_manager into websocket-gateway, and send server up/down messages in response to pings --- lib/jam_websockets/client_context.rb | 3 +- lib/jam_websockets/router.rb | 47 +++++++++++++++++----------- spec/jam_websockets/router_spec.rb | 3 +- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/jam_websockets/client_context.rb b/lib/jam_websockets/client_context.rb index 46cb87a92..316057c56 100644 --- a/lib/jam_websockets/client_context.rb +++ b/lib/jam_websockets/client_context.rb @@ -1,13 +1,14 @@ module JamWebsockets class ClientContext - attr_accessor :user, :client, :msg_count, :session + attr_accessor :user, :client, :msg_count, :session, :sent_bad_state_previously def initialize(user, client) @user = user @client = client @msg_count = 0 @session = nil + @sent_bad_state_previously = false end def to_s diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 5f3e07913..209baff4b 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -27,8 +27,7 @@ module JamWebsockets @clients = {} # clients that have logged in @user_context_lookup = {} # lookup a set of client_contexts by user_id @client_lookup = {} # lookup a client by client_id - @connection = nil - @channel = nil + @amqp_connection_manager = nil @users_exchange = nil @message_factory = JamRuby::MessageFactory.new @semaphore = Mutex.new @@ -46,11 +45,13 @@ module JamWebsockets @heartbeat_interval = connect_time_stale / 2 begin - @connection = AMQP.connect(:host => options[:host], :port => options[:port]) - @channel = AMQP::Channel.new(@connection) - #@channel.prefetch = 10 - register_topics + @amqp_connection_manager = AmqpConnectionManager.new(true, 4, :host => options[:host], :port => options[:port]) + @amqp_connection_manager.connect do |channel| + register_topics(channel) + end + rescue => e + @log.error "unable to initialize #{e.to_s}" cleanup raise e end @@ -110,14 +111,14 @@ module JamWebsockets end # register topic for user messages and session messages - def register_topics + def register_topics(channel) ######################## USER MESSAGING ########################### # create user exchange - @users_exchange = @channel.topic('users') + @users_exchange = channel.topic('users') # create user messaging topic - @user_topic = @channel.queue("", :auto_delete => true) + @user_topic = channel.queue("", :auto_delete => true) @user_topic.bind(@users_exchange, :routing_key => "user.#") @user_topic.purge @@ -155,9 +156,9 @@ module JamWebsockets ############## CLIENT MESSAGING ################### - @clients_exchange = @channel.topic('clients') + @clients_exchange = channel.topic('clients') - @client_topic = @channel.queue("", :auto_delete => true) + @client_topic = channel.queue("", :auto_delete => true) @client_topic.bind(@clients_exchange, :routing_key => "client.#") @client_topic.purge @@ -294,12 +295,8 @@ module JamWebsockets def cleanup() # shutdown topic listeners and mq connection - if !@channel.nil? - @channel.close - end - - if !@connection.nil? - @connection.close + unless @amqp_connection_manager.nil? + @amqp_connection_manager.disconnect end # tear down each individual client @@ -423,7 +420,7 @@ module JamWebsockets elsif client_msg.type == ClientMessage::Type::HEARTBEAT - handle_heartbeat(client_msg.heartbeat, client) + handle_heartbeat(client_msg.heartbeat, client_msg.message_id, client) else raise SessionError, "unknown message type '#{client_msg.type}' for #{client_msg.route_to}-directed message" @@ -516,7 +513,7 @@ module JamWebsockets end end - def handle_heartbeat(heartbeat, client) + def handle_heartbeat(heartbeat, heartbeat_message_id, client) unless context = @clients[client] @log.warn "*** WARNING: unable to find context due to heartbeat from client: #{client.client_id}; calling cleanup" cleanup_client(client) @@ -530,6 +527,18 @@ module JamWebsockets connection_manager.reconnect(connection) end if connection.stale? end + + # send errors to clients in response to heartbeats if + if !@amqp_connection_manager.connected? + error_msg = @message_factory.server_bad_state_error(heartbeat_message_id, "messaging system down") + context.sent_bad_state_previously = true + send_to_client(client, error_msg) + return + elsif context.sent_bad_state_previously + context.sent_bad_state_previously = false + recovery_msg = @message_factory.server_bad_state_recovered(heartbeat_message_id) + send_to_client(client, recovery_msg) + end end end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 5fc26c8d7..070fbbeaf 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -88,13 +88,12 @@ describe Router do em_before do @router = Router.new() - @router.start(30) end subject { @router } em_after do - @router.stop + end From f5e4bc3ce92c9c179be234e9b6f8cc7ec906b100 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 19 Jul 2013 16:39:29 -0500 Subject: [PATCH 87/98] * VRFS-424 pointing to int.jamkazam.com for gem server --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index d906a4c5f..75ebedf18 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ #ruby=1.9.3-p327 source 'https://rubygems.org' unless ENV['LOCAL_DEV'] == '1' - source 'https://jamjam:blueberryjam@www.jamkazam.com/gems/' + source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/' end # Look for $WORKSPACE, otherwise use "workspace" as dev path. From b25156518ff4e9b2e6ac3184a047d54de9bd434a Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 19 Jul 2013 20:45:20 -0500 Subject: [PATCH 88/98] * fixing to make arch sniff --- jenkins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins b/jenkins index b5133f94d..5c2c9f6dc 100755 --- a/jenkins +++ b/jenkins @@ -1,7 +1,7 @@ #!/bin/bash GEM_SERVER=http://localhost:9000/gems -DEB_SERVER=http://localhost:9010/apt-i386 +DEB_SERVER=http://localhost:9010/`uname -p` echo "starting build..." ./build From 32c3824c498b6bbc43056f92ab938f5822c1d067 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 19 Jul 2013 20:45:59 -0500 Subject: [PATCH 89/98] * fixing to make arch sniff --- jenkins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins b/jenkins index 5c2c9f6dc..5dc8f4c18 100755 --- a/jenkins +++ b/jenkins @@ -1,7 +1,7 @@ #!/bin/bash GEM_SERVER=http://localhost:9000/gems -DEB_SERVER=http://localhost:9010/`uname -p` +DEB_SERVER=http://localhost:9010/apt-`uname -p` echo "starting build..." ./build From e8fbd2e2638db61e6419196753719dd2331a23c4 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 22 Jul 2013 20:00:42 -0500 Subject: [PATCH 90/98] * VRFS-430: setting up MQRouter.client_exchange and .user_exchange on initialization so hat online messages work --- lib/jam_websockets/router.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 209baff4b..94fd6e581 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -154,6 +154,8 @@ module JamWebsockets end end + MQRouter.user_exchange = @users_exchange + ############## CLIENT MESSAGING ################### @clients_exchange = channel.topic('clients') @@ -189,6 +191,8 @@ module JamWebsockets @log.error e end end + + MQRouter.client_exchange = @clients_exchange end From 52287d054fcb744ea239963e36296301a905c818 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 23 Jul 2013 22:01:51 -0500 Subject: [PATCH 91/98] * VRFS-430; found another edge case on initialization of websocket-gateway requiring rework of startup code --- lib/jam_websockets/router.rb | 3 ++- lib/jam_websockets/server.rb | 17 +++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 94fd6e581..f602f577d 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -38,7 +38,7 @@ module JamWebsockets end - def start(connect_time_stale, options={:host => "localhost", :port => 5672}) + def start(connect_time_stale, options={:host => "localhost", :port => 5672}, &block) @log.info "startup" @@ -48,6 +48,7 @@ module JamWebsockets @amqp_connection_manager = AmqpConnectionManager.new(true, 4, :host => options[:host], :port => options[:port]) @amqp_connection_manager.connect do |channel| register_topics(channel) + block.call end rescue => e diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index 3000c36af..7607175c2 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -20,21 +20,22 @@ module JamWebsockets @log.info "starting server #{host}:#{port} with staleness_time=#{connect_time_stale}; reconnect time = #{connect_time_expire}" EventMachine.run do - @router.start(connect_time_stale) + @router.start(connect_time_stale) do + # take stale off the expire limit because the call to stale will + # touch the updated_at column, adding an extra stale limit to the expire time limit + expire_time = connect_time_expire > connect_time_stale ? connect_time_expire - connect_time_stale : connect_time_expire + start_connection_expiration(expire_time) + start_connection_flagger(connect_time_stale) + + start_websocket_listener(host, port, options[:emwebsocket_debug]) + end # if you don't do this, the app won't exit unless you kill -9 at_exit do @log.info "cleaning up server" @router.cleanup end - - # take stale off the expire limit because the call to stale will - # touch the updated_at column, adding an extra stale limit to the expire time limit - expire_time = connect_time_expire > connect_time_stale ? connect_time_expire - connect_time_stale : connect_time_expire - start_connection_expiration(expire_time) - start_connection_flagger(connect_time_stale) - start_websocket_listener(host, port, options[:emwebsocket_debug]) end end From 97a56fd8e087b9fd4aa3654a2a9eb6a5fca1aa25 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 7 Aug 2013 10:38:35 -0500 Subject: [PATCH 92/98] * increased messaging for leave/join situations and reconnect support for VRFS-484 --- config/application.yml | 2 +- lib/jam_websockets/client_context.rb | 9 ++ lib/jam_websockets/router.rb | 100 ++++++++++++--------- lib/jam_websockets/server.rb | 5 +- spec/jam_websockets/client_context_spec.rb | 14 +++ spec/jam_websockets/router_spec.rb | 2 +- 6 files changed, 87 insertions(+), 45 deletions(-) create mode 100644 spec/jam_websockets/client_context_spec.rb diff --git a/config/application.yml b/config/application.yml index f0576f674..038b59167 100644 --- a/config/application.yml +++ b/config/application.yml @@ -1,6 +1,6 @@ Defaults: &defaults connect_time_stale: 30 - connect_time_expire: 180 + connect_time_expire: 60 development: port: 6767 diff --git a/lib/jam_websockets/client_context.rb b/lib/jam_websockets/client_context.rb index 316057c56..c3f302778 100644 --- a/lib/jam_websockets/client_context.rb +++ b/lib/jam_websockets/client_context.rb @@ -15,5 +15,14 @@ return "Client[user:#{@user} client:#{@client} msgs:#{@msg_count} session:#{@session}]" end + def hash + @client.hash + end + + def ==(o) + o.class == self.class && o.client == @client + end + alias_method :eql?, :== + end end diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index f602f577d..61da6549d 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -60,8 +60,8 @@ module JamWebsockets @log.info "started" end - def add_client(client_id, client) - @client_lookup[client_id] = client + def add_client(client_id, client_context) + @client_lookup[client_id] = client_context end def remove_client(client_id, client) @@ -69,13 +69,13 @@ module JamWebsockets if deleted.nil? @log.warn "unable to delete #{client_id} from client_lookup" - elsif deleted != client + elsif deleted.client != client # put it back--this is only possible if add_client hit the 'old connection' path # so in other words if this happens: # add_client(1, clientX) # add_client(1, clientY) # but clientX is essentially defunct - this could happen due to a bug in client, or EM doesn't notify always of connection close in time # remove_client(1, clientX) -- this check maintains that clientY stays as the current client in the hash - @client_lookup[client_id] = client + @client_lookup[client_id] = deleted @log.debug "putting back client into @client_lookup for #{client_id} #{client.inspect}" else @log.debug "cleaned up @client_lookup for #{client_id}" @@ -86,11 +86,11 @@ module JamWebsockets def add_user(context) user_contexts = @user_context_lookup[context.user.id] if user_contexts.nil? - user_contexts = Set.new + user_contexts = Hash.new @user_context_lookup[context.user.id] = user_contexts end - user_contexts.add(context) + user_contexts[context.client] = context end def remove_user(client_context) @@ -100,7 +100,7 @@ module JamWebsockets @log.warn "user can not be removed #{client_context}" else # delete the context from set of user contexts - user_contexts.delete(client_context) + user_contexts.delete(client_context.client) # if last user context, delete entire set (memory leak concern) if user_contexts.length == 0 @@ -138,7 +138,7 @@ module JamWebsockets msg = Jampb::ClientMessage.parse(msg) - contexts.each do |context| + contexts.each do |client_id, context| EM.schedule do @log.debug "sending user message to #{context}" send_to_client(context.client, msg) @@ -171,7 +171,8 @@ module JamWebsockets routing_key = headers.routing_key client_id = routing_key["client.".length..-1] @semaphore.synchronize do - client = @client_lookup[client_id] + client_context = @client_lookup[client_id] + client = client_context.client msg = Jampb::ClientMessage.parse(msg) @@ -319,7 +320,11 @@ module JamWebsockets def stale_client(client) if cid = client.client_id ConnectionManager.active_record_transaction do |connection_manager| - connection_manager.flag_connection_stale_with_client_id(cid) + music_session_id = connection_manager.flag_connection_stale_with_client_id(cid) + # update the session members, letting them know this client went stale + context = @client_lookup[client.client_id] + music_session = MusicSession.find_by_id(music_session_id) unless music_session_id.nil? + Notification.send_musician_session_stale(music_session, client.client_id, context.user) unless music_session.nil? end end end @@ -327,19 +332,8 @@ module JamWebsockets def cleanup_clients_with_ids(client_ids) # @log.debug("*** cleanup_clients_with_ids: client_ids = #{client_ids.inspect}") client_ids.each do |cid| - if 0 < (ws_clients = @clients.keys).length - ws_clients.each do |client| - if cid == client.client_id - self.cleanup_client(client) - break - else - # @log.debug("*** cleanup_clients: deleting connection = #{cid}") - ConnectionManager.active_record_transaction { |mgr| mgr.delete_connection(cid) } - end - end - else - ConnectionManager.active_record_transaction { |mgr| mgr.delete_connection(cid) } - end + client_context = @client_lookup[cid] + self.cleanup_client(client_context.client) unless client_context.nil? end end @@ -350,7 +344,7 @@ module JamWebsockets pending = @pending_clients.delete?(client) if !pending.nil? - @log.debug "cleaning up pending client #{client}" + @log.debug "cleaning up not-logged-in client #{client}" else @log.debug "cleanup up logged-in client #{client}" @@ -360,11 +354,14 @@ module JamWebsockets context = @clients.delete(client) if !context.nil? - # remove this connection from the database if !context.user.nil? && !context.client.nil? - ConnectionManager.active_record_transaction do |connection_manager| - connection_manager.delete_connection(client.client_id) + ConnectionManager.active_record_transaction do |mgr| + mgr.delete_connection(client.client_id) { |conn, count, music_session_id| + Notification.send_friend_update(context.user.id, false, conn) if count == 0 + music_session = MusicSession.find_by_id(music_session_id) unless music_session_id.nil? + Notification.send_musician_session_depart(music_session, client.client_id, context.user) unless music_session.nil? + } end end @@ -437,9 +434,11 @@ module JamWebsockets password = login.password if login.value_for_tag(2) token = login.token if login.value_for_tag(3) client_id = login.client_id if login.value_for_tag(4) + reconnect_music_session_id = login.client_id if login.value_for_tag(5) @log.info("*** handle_login: token=#{token}; client_id=#{client_id}") connection = nil + reconnected = false # you don't have to supply client_id in login--if you don't, we'll generate one if client_id.nil? || client_id.empty? @@ -451,8 +450,21 @@ module JamWebsockets if connection = JamRuby::Connection.find_by_client_id(client_id) # FIXME: I think connection table needs to updated within connection_manager # otherwise this would be 1 line of code (connection.connect!) + + music_session_upon_reentry = connection.music_session + ConnectionManager.active_record_transaction do |connection_manager| - connection_manager.reconnect(connection) + music_session_id, reconnected = connection_manager.reconnect(connection, reconnect_music_session_id) + context = @client_lookup[client_id] + if music_session_id.nil? + # if this is a reclaim of a connection, but music_session_id comes back null, then we need to check if this connection was IN a music session before. + # if so, then we need to tell the others in the session that this user is now departed + Notification.send_musician_session_depart(music_session_upon_reentry, client.client_id, context.user) unless context.nil? || music_session_upon_reentry.nil? || music_session_upon_reentry.destroyed? + else + music_session = MusicSession.find_by_id(music_session_id) + Notification.send_musician_session_fresh(music_session, client.client_id, context.user) unless context.nil? + end + end if connection.stale? end # if there's a client_id but no connection object, create new client_id @@ -467,15 +479,8 @@ module JamWebsockets @log.debug "user #{user} logged in" # respond with LOGIN_ACK to let client know it was successful - #binding.pry - remote_ip = extract_ip(client) - login_ack = @message_factory.login_ack(remote_ip, - client_id, - user.remember_token, - @heartbeat_interval, - connection.try(:music_session_id)) - send_to_client(client, login_ack) + @semaphore.synchronize do # remove from pending_queue @@ -485,14 +490,25 @@ module JamWebsockets context = ClientContext.new(user, client) @clients[client] = context add_user(context) - add_client(client_id, client) # TODO + add_client(client_id, context) - unless connection + unless connection # log this connection in the database ConnectionManager.active_record_transaction do |connection_manager| - connection_manager.create_connection(user.id, client.client_id, extract_ip(client)) + connection_manager.create_connection(user.id, client.client_id, remote_ip) do |conn, count| + if count == 1 + Notification.send_friend_update(user.id, true, conn) + end + end end end + login_ack = @message_factory.login_ack(remote_ip, + client_id, + user.remember_token, + @heartbeat_interval, + connection.try(:music_session_id), + reconnected) + send_to_client(client, login_ack) end else raise SessionError, 'invalid login' @@ -528,12 +544,14 @@ module JamWebsockets @log.warn "*** WARNING: unable to find connection due to heartbeat from client: #{context}; calling cleanup_client" cleanup_client(client) else + connection.touch + ConnectionManager.active_record_transaction do |connection_manager| - connection_manager.reconnect(connection) + connection_manager.reconnect(connection, connection.music_session.id) end if connection.stale? end - # send errors to clients in response to heartbeats if + # send errors to clients in response to heartbeats if rabbitmq is down if !@amqp_connection_manager.connected? error_msg = @message_factory.server_bad_state_error(heartbeat_message_id, "messaging system down") context.sent_bad_state_previously = true diff --git a/lib/jam_websockets/server.rb b/lib/jam_websockets/server.rb index 7607175c2..795e8455d 100644 --- a/lib/jam_websockets/server.rb +++ b/lib/jam_websockets/server.rb @@ -23,7 +23,8 @@ module JamWebsockets @router.start(connect_time_stale) do # take stale off the expire limit because the call to stale will # touch the updated_at column, adding an extra stale limit to the expire time limit - expire_time = connect_time_expire > connect_time_stale ? connect_time_expire - connect_time_stale : connect_time_expire + # expire_time = connect_time_expire > connect_time_stale ? connect_time_expire - connect_time_stale : connect_time_expire + expire_time = connect_time_expire start_connection_expiration(expire_time) start_connection_flagger(connect_time_stale) @@ -69,7 +70,7 @@ module JamWebsockets # one cleanup on startup flag_stale_connections(flag_max_time) - EventMachine::PeriodicTimer.new(flag_max_time) do + EventMachine::PeriodicTimer.new(flag_max_time/2) do flag_stale_connections(flag_max_time) end end diff --git a/spec/jam_websockets/client_context_spec.rb b/spec/jam_websockets/client_context_spec.rb new file mode 100644 index 000000000..522e77b10 --- /dev/null +++ b/spec/jam_websockets/client_context_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe ClientContext do + + let(:context) {ClientContext.new({}, "client1")} + + describe 'hashing' do + it "hash correctly" do + set = Set.new + set.add?(context).should eql(set) + set.add?(context).should be_nil + end + end +end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index 070fbbeaf..61193a26f 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -42,7 +42,7 @@ def login(router, user, password, client_id) message_factory = MessageFactory.new client = LoginClient.new - login_ack = message_factory.login_ack("127.0.0.1", client_id, user.remember_token, 15, nil) + login_ack = message_factory.login_ack("127.0.0.1", client_id, user.remember_token, 15, nil, false) router.should_receive(:send_to_client) do |*args| args.count.should == 2 From 6e431f3dec2d6deed4c2787dc642b12dfcc0bd7b Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 7 Aug 2013 12:36:47 -0500 Subject: [PATCH 93/98] * null music session bug messing up production --- lib/jam_websockets/router.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 61da6549d..c4d731f6d 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -547,7 +547,7 @@ module JamWebsockets connection.touch ConnectionManager.active_record_transaction do |connection_manager| - connection_manager.reconnect(connection, connection.music_session.id) + connection_manager.reconnect(connection, connection.music_session_id) end if connection.stale? end From 6f3eb7bf7789ea0c60f367fa167344c60884ffca Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 18 Aug 2013 02:57:25 +0000 Subject: [PATCH 94/98] * updating ruby version --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 95a5ad2d5..abf2ccea0 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-2.0.0-p195 +ruby-2.0.0-p247 From ba7354861680d81975a4a750d56aafc932fd5a8c Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 31 Aug 2013 19:55:44 +0000 Subject: [PATCH 95/98] * VRFS-608 - move database deletion out of cleanup method which is only for cleaning up websocket gateway 'connection' state --- lib/jam_websockets/router.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index c4d731f6d..3faa89faa 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -332,8 +332,19 @@ module JamWebsockets def cleanup_clients_with_ids(client_ids) # @log.debug("*** cleanup_clients_with_ids: client_ids = #{client_ids.inspect}") client_ids.each do |cid| + client_context = @client_lookup[cid] self.cleanup_client(client_context.client) unless client_context.nil? + + # remove this connection from the database + ConnectionManager.active_record_transaction do |mgr| + mgr.delete_connection(cid) { |conn, count, music_session_id, user_id| + Notification.send_friend_update(user_id, false, conn) if count == 0 + music_session = MusicSession.find_by_id(music_session_id) unless music_session_id.nil? + user = User.find_by_id(user_id) unless user_id.nil? + Notification.send_musician_session_depart(music_session, cid, user) unless music_session.nil? || user.nil? + } + end end end @@ -354,17 +365,6 @@ module JamWebsockets context = @clients.delete(client) if !context.nil? - # remove this connection from the database - if !context.user.nil? && !context.client.nil? - ConnectionManager.active_record_transaction do |mgr| - mgr.delete_connection(client.client_id) { |conn, count, music_session_id| - Notification.send_friend_update(context.user.id, false, conn) if count == 0 - music_session = MusicSession.find_by_id(music_session_id) unless music_session_id.nil? - Notification.send_musician_session_depart(music_session, client.client_id, context.user) unless music_session.nil? - } - end - end - remove_user(context) else @log.debug "skipping duplicate cleanup attempt of logged-in client" From 062a5299b8b3b9cf7da64a7ce0cb2e0edd227e3c Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 1 Sep 2013 02:17:38 +0000 Subject: [PATCH 96/98] * VRFS-594 - sending heartbeat ack down in response to heartbeat --- lib/jam_websockets/router.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 3faa89faa..48ad33022 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -551,6 +551,10 @@ module JamWebsockets end if connection.stale? end + heartbeat_ack = @message_factory.heartbeat_ack() + + send_to_client(client, heartbeat_ack) + # send errors to clients in response to heartbeats if rabbitmq is down if !@amqp_connection_manager.connected? error_msg = @message_factory.server_bad_state_error(heartbeat_message_id, "messaging system down") From 5f7f73d06e15a002bb9adaa6575174bb5029c338 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 5 Sep 2013 07:33:19 +0000 Subject: [PATCH 97/98] eliminate BEL which makes terminal bounce --- lib/jam_websockets/router.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 48ad33022..4100f4116 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -288,7 +288,7 @@ module JamWebsockets def send_to_client(client, msg) - @log.debug "SEND TO CLIENT START (#{msg})" + @log.debug "SEND TO CLIENT START" # (#{msg})" if client.encode_json client.send(msg.to_json.to_s) else From e886b21139f6d39b70797581f804274486dd9152 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 14 Sep 2013 19:57:20 +0000 Subject: [PATCH 98/98] * printing type --- lib/jam_websockets/router.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 48ad33022..7c2681498 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -288,14 +288,13 @@ module JamWebsockets def send_to_client(client, msg) - @log.debug "SEND TO CLIENT START (#{msg})" + @log.debug "SEND TO CLIENT (#{@message_factory.get_message_type(msg)})" if client.encode_json client.send(msg.to_json.to_s) else # this is so odd that this is necessary from an API perspective. but searching through the source code... it's all I could find in em-websocket for allowing a binary message to be sent client.instance_variable_get(:@handler).send_frame(:binary, msg.to_s) end - @log.debug "SEND TO CLIENT STOP" end def cleanup()