palava_machine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ OGNiZjliNTM4ZmUyY2E0MGQwNjRjMGQ3OGQ3MDliZWRjMmI0MTQ3OQ==
5
+ data.tar.gz: !binary |-
6
+ MDkzM2Q5MWZiZWU3YWIzZjM1NjcwNDVmYzc2M2QxNzM0ZmMxZWEzYQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YTVkMmM3ZDUzN2UyMGQ0ZTg0NDg4YzBmNjY3ZjFmNmE1OTRkOWJjYjAzZjhh
10
+ YzE1ZDBhNzgwNGM3ZTRjYmNiZDgzMTA5M2NiZTc4YWUyNDU5ODc2OGEzYTg2
11
+ ZDJlNzM1ZDBlNjZlMDNmYTlkYjQ3NjZjNjYxYmJjYjc4MDY4YjA=
12
+ data.tar.gz: !binary |-
13
+ NjBlY2JlZDI5N2ZlYTg0NjFjNTZiNjAzZTY5YmEwMzM5YmYyZGEyZGM3MzQ5
14
+ MzZkZGRmMjQ5MGYyNGNlZjMyM2IwZGU3ODE0MjdhYzcwZDI4MmYxZTRkMjY3
15
+ MGEyNmEzMDE1MmFiNTJkMjNiOGY4ZTg0YjlhOTA4M2M1ZmJiMzU=
data/ChangeLog.md ADDED
@@ -0,0 +1,2 @@
1
+ ## 1.0.0
2
+ - First Open Source Release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ # ruby '2.0.0'
3
+ ruby '1.9.3'
4
+ gemspec
@@ -0,0 +1,2 @@
1
+ ## 1.0.0
2
+ - First Open Source Release
data/Rakefile ADDED
@@ -0,0 +1,163 @@
1
+ require 'fileutils'
2
+ require 'bundler/setup'
3
+ require 'yaml'
4
+ require 'resque/tasks'
5
+ require 'resque_scheduler/tasks'
6
+
7
+ ROOT_PATH = File.dirname(__FILE__) + '/../'
8
+
9
+ # Start a worker with proper env vars and output redirection
10
+ def run_worker(queue, count = 1)
11
+ puts "Starting #{count} worker(s) with QUEUE: #{queue}"
12
+ ops = {:pgroup => true, :err => [(ROOT_PATH + "log/resque.workers.error.log").to_s, "a"],
13
+ :out => [(ROOT_PATH + "log/resque.workers.log").to_s, "a"]}
14
+ env_vars = {"QUEUE" => queue.to_s}
15
+ count.times {
16
+ ## Using Kernel.spawn and Process.detach because regular system() call would
17
+ ## cause the processes to quit when capistrano finishes
18
+ pid = spawn(env_vars, "rake resque:work", ops)
19
+ Process.detach(pid)
20
+ }
21
+ end
22
+
23
+ # Start a scheduler, requires resque_scheduler >= 2.0.0.f
24
+ def run_scheduler
25
+ puts "Starting resque scheduler"
26
+ env_vars = {
27
+ "BACKGROUND" => "1",
28
+ "PIDFILE" => (ROOT_PATH + "/pid/resque-scheduler.pid").to_s,
29
+ "VERBOSE" => "1"
30
+ }
31
+ ops = {:pgroup => true, :err => [(ROOT_PATH + "log/resque.scheduler.error.log").to_s, "a"],
32
+ :out => [(ROOT_PATH + "log/resque.scheduler.log").to_s, "a"]}
33
+ pid = spawn(env_vars, "rake resque:scheduler", ops)
34
+ Process.detach(pid)
35
+ end
36
+
37
+
38
+ namespace :deploy do
39
+ # desc 'whiskey_disk deploy hook'
40
+ # task :post_deploy => %w[
41
+ # resque:restart_workers
42
+ # resque:restart_scheduler
43
+ # ]
44
+ end
45
+
46
+
47
+ namespace :resque do
48
+ task :environment do
49
+ require 'resque'
50
+ require 'resque_scheduler'
51
+ require 'resque/scheduler'
52
+
53
+ Resque.redis = 'localhost:6379'
54
+ Resque.schedule = YAML.load_file('config/schedule.yml')
55
+ require_relative 'jobs'
56
+ end
57
+
58
+ task :setup => :environment
59
+
60
+ desc "Restart running workers"
61
+ task :restart_workers => :environment do
62
+ Rake::Task['resque:stop_workers'].invoke
63
+ Rake::Task['resque:start_workers'].invoke
64
+ end
65
+
66
+ desc "Quit running workers"
67
+ task :stop_workers => :environment do
68
+ pids = Array.new
69
+ Resque.workers.each do |worker|
70
+ pids.concat(worker.worker_pids)
71
+ end
72
+ if pids.empty?
73
+ puts "No workers to kill"
74
+ else
75
+ syscmd = "kill -s QUIT #{pids.join(' ')}"
76
+ puts "Running syscmd: #{syscmd}"
77
+ system(syscmd)
78
+ end
79
+ end
80
+
81
+ desc "Start workers"
82
+ task :start_workers => :environment do
83
+ run_worker("*", 2)
84
+ run_worker("high", 1)
85
+ end
86
+
87
+ desc "Restart scheduler"
88
+ task :restart_scheduler => :environment do
89
+ Rake::Task['resque:stop_scheduler'].invoke
90
+ Rake::Task['resque:start_scheduler'].invoke
91
+ end
92
+
93
+ desc "Quit scheduler"
94
+ task :stop_scheduler => :environment do
95
+ pidfile = ROOT_PATH + "pid/resque-scheduler.pid"
96
+ if !File.exists?(pidfile)
97
+ puts "Scheduler not running"
98
+ else
99
+ pid = File.read(pidfile).to_i
100
+ syscmd = "kill -s QUIT #{pid}"
101
+ puts "Running syscmd: #{syscmd}"
102
+ system(syscmd)
103
+ FileUtils.rm_f(pidfile)
104
+ end
105
+ end
106
+
107
+ desc "Start scheduler"
108
+ task :start_scheduler => :environment do
109
+ run_scheduler
110
+ end
111
+
112
+ desc "Reload schedule"
113
+ task :reload_schedule => :environment do
114
+ pidfile = ROOT_PATH + "pid/resque-scheduler.pid"
115
+
116
+ if !File.exists?(pidfile)
117
+ puts "Scheduler not running"
118
+ else
119
+ pid = File.read(pidfile).to_i
120
+ syscmd = "kill -s USR2 #{pid}"
121
+ puts "Running syscmd: #{syscmd}"
122
+ system(syscmd)
123
+ end
124
+ end
125
+ end
126
+
127
+ # # #
128
+
129
+ def gemspec
130
+ name = Dir['*.gemspec'].first
131
+ @gemspec ||= eval(File.read(name), binding, name)
132
+ end
133
+
134
+ desc "Build the gem"
135
+ task :gem => :gemspec do
136
+ sh "gem build #{gemspec.name}.gemspec"
137
+ FileUtils.mkdir_p 'pkg'
138
+ FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg'
139
+ end
140
+
141
+ desc "Install the gem locally"
142
+ task :install => :gem do
143
+ sh %{gem install pkg/#{gemspec.name}-#{gemspec.version}.gem --no-rdoc --no-ri}
144
+ end
145
+
146
+ desc "Generate the gemspec"
147
+ task :generate do
148
+ puts gemspec.to_ruby
149
+ end
150
+
151
+ desc "Validate the gemspec"
152
+ task :gemspec do
153
+ gemspec.validate
154
+ end
155
+
156
+ desc 'rspec specs'
157
+ task :spec do
158
+ sh %[rspec spec]
159
+ end
160
+
161
+ task :default => :spec
162
+ task :test => :spec
163
+
data/ReadMe.md ADDED
@@ -0,0 +1,56 @@
1
+ # PalavaMachine
2
+
3
+ ## Description
4
+
5
+ PalavaMachine is a WebRTC signaling server. Signaling describes the process of finding other peers and exchange information about how to establish a media connection.
6
+
7
+ The server is implemented in [EventMachine](http://rubyeventmachine.com/) and [Redis PubSub](http://redis.io/topics/pubsub) and communication to the clients is done via WebSockets. See it in action at [palava.tv.](https://palava.tv)
8
+
9
+ ## What can I do with it?
10
+
11
+ *This is a pre-release for interested Ruby/JS/WebRTC developers*. If you are unsure, what to use this gem for, you'll just need to wait. We'll soon put a more detailed instructions on our [blog](https://blog.palava.tv).
12
+
13
+ ## Installation & Usage
14
+
15
+ Make sure you have redis(http://redis.io/download) installed, then clone this repository and run
16
+
17
+ $ bundle install
18
+
19
+ Start the server with
20
+
21
+ $ bin/palava-machine
22
+
23
+ Alternatively, download the [palava_machine gem](http://rubygems.org/gems/palava_machine) from rubygems.org:
24
+
25
+ $ gem install palava_machine
26
+
27
+ And run:
28
+
29
+ $ palava-machine
30
+
31
+ ### Deamonized Version
32
+
33
+ The PalavaMachine can be started as a daemon process for production usage:
34
+
35
+ $ palava-machine-daemon start
36
+
37
+ Stop it with
38
+
39
+ $ palava-machine-daemon stop
40
+
41
+ ### Specs
42
+
43
+ To run the test suite use
44
+
45
+ $ rspec
46
+
47
+ ## Credits
48
+
49
+ Open Source License information following soon!
50
+
51
+ (c) 2013 Jan Lelis, jan@signaling.io
52
+ (c) 2013 Marius Melzer, marius@signaling.io
53
+ (c) 2013 Stephan Thamm, thammi@chaossource.net
54
+ (c) 2013 Kilian Ulbrich, kilian@innovailable.eu
55
+
56
+ Part of the [palava project](https://blog.palava.tv)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/palava_machine/runner'
4
+ PalavaMachine::Runner.run(PalavaMachine::Runner.parse_cli_options)
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ require 'daemons'
3
+
4
+ Daemons.run("#{File.dirname(__FILE__)}/palava-machine",
5
+ multiple: true,
6
+ ARGV: ARGV + ['--', '4240', '5']
7
+ )
@@ -0,0 +1,13 @@
1
+ require_relative 'palava_machine/version' unless defined? PalavaMachine::VERSION
2
+
3
+ module PalavaMachine
4
+ class MessageParsingError < StandardError; end
5
+
6
+ class MessageError < StandardError
7
+ attr_reader :ws
8
+
9
+ def initialize(ws)
10
+ @ws = ws
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'version'
2
+
3
+ require 'json'
4
+
5
+ module PalavaMachine
6
+ class ClientMessage
7
+ RULES = {
8
+ info: [],
9
+ join_room: [:room_id, :status],
10
+ leave_room: [],
11
+ send_to_peer: [:peer_id, :data],
12
+ update_status: [:status],
13
+ }
14
+
15
+ def initialize(message, connection_id = nil)
16
+ begin
17
+ @_data = JSON.parse(message)
18
+ rescue # TODO find exact json error to catch
19
+ raise MessageParsingError, 'invalid message'
20
+ end
21
+
22
+ raise MessageParsingError, 'invalid message: not a hash' unless @_data.instance_of?(Hash)
23
+ @connection_id = connection_id
24
+ end
25
+
26
+ def [](w)
27
+ @_data[w]
28
+ end
29
+
30
+ def valid?
31
+ RULES.keys.include?(name) or raise MessageParsingError, 'unknown event'
32
+ end
33
+
34
+ def name
35
+ @name ||= @_data['event'] && @_data['event'].to_sym or raise(MessageParsingError, 'no event given')
36
+ end
37
+
38
+ def connection_id
39
+ @connection_id or raise MessageParsingError, 'connection id used but not set'
40
+ end
41
+
42
+ def arguments
43
+ valid? && RULES[name].map{ |data_key| @_data[data_key.to_s] }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.dirname(__FILE__) + '/jobs/*'].each { |filename|
2
+ require_relative "jobs/#{ File.basename(filename) }"
3
+ }
@@ -0,0 +1,59 @@
1
+ require 'set'
2
+ require 'redis'
3
+ require 'mongo'
4
+
5
+ class ExportStatsJob
6
+ class StatsExporter
7
+ STATS_NAMESPACE = "store:stats"
8
+
9
+ def initialize(redis_address, mongo_address)
10
+ @redis = Redis.new(host: 'localhost', port: 6379)
11
+ @mongo = Mongo::MongoClient.new#(mongo_address)
12
+ @times = Set.new
13
+ end
14
+
15
+ def import_timestamps!(ns)
16
+ redis_pattern = "#{STATS_NAMESPACE}:#{ns}:*"
17
+ offset = redis_pattern.size - 1
18
+ @times.merge @redis.keys(redis_pattern).map{ |key|
19
+ key[offset..-1].to_i
20
+ }
21
+ end
22
+
23
+ # remove timestamps which are not closed (+ grace time)
24
+ def prune_timestamps!
25
+ limit = Time.now.utc.to_i - 3660
26
+ @times.select!{ |time| time < limit }
27
+ puts "Transfering #{@times.length} timespans"
28
+ end
29
+
30
+ def store_in_mongo!
31
+ collection = @mongo.db("plv_stats").collection("rtc")
32
+
33
+ @times.each { |time|
34
+ collection.insert(
35
+ "c_at" => time,
36
+ "connection_time" => get_and_delete_from_redis("connection_time", time),
37
+ "room_peaks" => get_and_delete_from_redis("room_peaks", time),
38
+ )
39
+ }
40
+ end
41
+
42
+ def get_and_delete_from_redis(ns, time)
43
+ key = "#{STATS_NAMESPACE}:#{ns}:#{time}"
44
+ data = @redis.hgetall(key) || {}
45
+ @redis.del(key)
46
+ data
47
+ end
48
+ end
49
+
50
+ class << self
51
+ def perform(redis_address = 'localhost:6379', mongo_address = 'localhost:27017')
52
+ se = StatsExporter.new(redis_address, mongo_address)
53
+ se.import_timestamps! "room_peaks"
54
+ se.import_timestamps! "connection_time"
55
+ se.prune_timestamps!
56
+ se.store_in_mongo!
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,311 @@
1
+ require_relative 'version'
2
+ require_relative 'socket_store'
3
+
4
+ require 'em-hiredis'
5
+ require 'json'
6
+ require 'digest/sha2'
7
+ require 'time'
8
+ require 'logger'
9
+ require 'logger/colors'
10
+ require 'forwardable'
11
+
12
+
13
+ module PalavaMachine
14
+ class Manager
15
+ extend Forwardable
16
+
17
+
18
+ attr_reader :connections
19
+
20
+
21
+ def_delegators :@log, :debug, :info, :warn, :error, :fatal
22
+
23
+
24
+ PAYLOAD_NEW_PEER = lambda { |connection_id, status = nil|
25
+ payload = { event: 'new_peer', peer_id: connection_id }
26
+ payload[:status] = status if status
27
+ payload.to_json
28
+ }
29
+
30
+ PAYLOAD_PEER_LEFT = lambda { |connection_id| {
31
+ event: 'peer_left',
32
+ sender_id: connection_id,
33
+ }.to_json }
34
+
35
+
36
+ SCRIPT_JOIN_ROOM = <<-LUA
37
+ local members = redis.call('smembers', KEYS[1])
38
+ local count = 0
39
+ for _, peer_id in pairs(members) do
40
+ redis.call('publish', "ps:connection:" .. peer_id, ARGV[2])
41
+ count = count + 1
42
+ end
43
+ redis.call('sadd', KEYS[1], ARGV[1])
44
+ if count == 0 or tonumber(redis.call('get', KEYS[2])) <= count then
45
+ redis.call('set', KEYS[2], count + 1)
46
+ end
47
+ redis.call('set', KEYS[3], ARGV[3])
48
+ redis.call('set', KEYS[4], ARGV[4])
49
+ return members
50
+ LUA
51
+
52
+ SCRIPT_LEAVE_ROOM = <<-LUA
53
+ redis.call('hincrby', KEYS[7], (ARGV[3] - tonumber(redis.call('get', KEYS[3]))) / 60, 1) --stats
54
+ redis.call('srem', KEYS[1], ARGV[1])
55
+ redis.call('del', KEYS[3])
56
+ redis.call('del', KEYS[4])
57
+ redis.call('del', KEYS[5])
58
+
59
+ if redis.call('scard', KEYS[1]) == 0 then -- also delete room if it is empty
60
+ redis.call('hincrby', KEYS[6], redis.call('get', KEYS[2]), 1) --stats
61
+ redis.call('del', KEYS[1])
62
+ redis.call('del', KEYS[2])
63
+ else -- tell others in room
64
+ for _, peer_id in pairs(redis.call('smembers', KEYS[1])) do
65
+ redis.call('publish', "ps:connection:" .. peer_id, ARGV[2])
66
+ end
67
+ end
68
+ LUA
69
+
70
+
71
+ def initialize(options = {})
72
+ @redis_address = 'localhost:6379'
73
+ @redis_db = options[:db] || 0
74
+ @connections = SocketStore.new
75
+ @log = Logger.new(STDOUT)
76
+ @log.level = Logger::DEBUG
77
+ @log.formatter = proc{ |level, datetime, _, msg|
78
+ "#{datetime.strftime '%F %T'} | #{msg}\n"
79
+ }
80
+ end
81
+
82
+ def initialize_in_em
83
+ @redis = EM::Hiredis.connect "redis://#{@redis_address}/#{@redis_db}"
84
+ @publisher = @redis.pubsub
85
+ @subscriber = EM::Hiredis.connect("redis://#{@redis_address}/#{@redis_db}").pubsub # You need an extra connection for subs
86
+ @redis.on :failed do
87
+ @log.error 'Could not connect to Redis server'
88
+ end
89
+ end
90
+
91
+ def announce_connection(ws)
92
+ connection_id = @connections.register_connection(ws)
93
+ info "#{connection_id} <open>"
94
+
95
+ @subscriber.subscribe "ps:connection:#{connection_id}" do |payload|
96
+ # debug "SUB payload #{payload} for <#{connection_id}>"
97
+ ws.send_text(payload)
98
+ end
99
+ end
100
+
101
+ def return_error(connection_id, message)
102
+ raise MessageError.new(@connections[connection_id]), message
103
+ end
104
+
105
+ def unannounce_connection(ws, close_ws = false)
106
+ if connection_id = @connections.unregister_connection(ws)
107
+ info "#{connection_id} <close>"
108
+ leave_room(connection_id)
109
+ @subscriber.unsubscribe "ps:connection:#{connection_id}"
110
+ if close_ws && ws.state != :closed # currently not used FIXME
111
+ ws.close
112
+ end
113
+ end
114
+ end
115
+
116
+ def join_room(connection_id, room_id, status)
117
+ return_error connection_id, 'no room id given' if !room_id || room_id.empty?
118
+ return_error connection_id, 'room id too long' if room_id.size > 50
119
+
120
+ @redis.get "store:connection:room:#{connection_id}" do |res|
121
+ return_error connection_id, 'already joined another room' if res
122
+ room_id = Digest::SHA512.hexdigest(room_id)
123
+ info "#{connection_id} joins ##{room_id[0..10]}... #{status}"
124
+
125
+ script_join_room(connection_id, room_id, status){ |members|
126
+ return_error connection_id, 'room is full' unless members
127
+
128
+ update_status_without_notifying_peers(connection_id, status){
129
+ if members.empty?
130
+ send_joined_room(connection_id, [])
131
+ else
132
+ get_statuses_for_members(members) do |members_with_statuses|
133
+ send_joined_room(connection_id, members_with_statuses)
134
+ end
135
+ end
136
+ }
137
+ }
138
+ end
139
+ end
140
+
141
+ def script_join_room(connection_id, room_id, status, &block)
142
+ @redis.eval \
143
+ SCRIPT_JOIN_ROOM,
144
+ 4,
145
+ "store:room:members:#{room_id}",
146
+ "store:room:peak_members:#{room_id}",
147
+ "store:connection:joined:#{connection_id}",
148
+ "store:connection:room:#{connection_id}",
149
+ connection_id,
150
+ PAYLOAD_NEW_PEER[connection_id, status],
151
+ Time.now.getutc.to_i,
152
+ room_id,
153
+ &block
154
+ end
155
+ private :script_join_room
156
+
157
+ def get_statuses_for_members(members)
158
+ member_count = members.size
159
+ members_with_statuses = []
160
+ members.each { |peer_id|
161
+ @redis.hgetall("store:connection:status:#{peer_id}") do |status_array|
162
+ members_with_statuses << { peer_id: peer_id, status: Hash[status_array.each_slice(2).to_a] }
163
+ yield members_with_statuses if members_with_statuses.size == member_count
164
+ end
165
+ }
166
+ end
167
+ private :get_statuses_for_members
168
+
169
+ def send_joined_room(connection_id, members_with_statuses)
170
+ @connections[connection_id].send_text({
171
+ event: 'joined_room',
172
+ own_id: connection_id,
173
+ peers: members_with_statuses,
174
+ }.to_json)
175
+ end
176
+ private :send_joined_room
177
+
178
+ def leave_room(connection_id)
179
+ @redis.get("store:connection:room:#{connection_id}") do |room_id|
180
+ next unless room_id # return_error connection_id, 'currently not in any room'
181
+
182
+ info "#{connection_id} leaves ##{room_id[0..10]}..."
183
+ script_leave_room(connection_id, room_id)
184
+ end
185
+ end
186
+
187
+ def script_leave_room(connection_id, room_id, &block)
188
+ now = Time.now.getutc.to_i
189
+ hour = now - now % (60 * 60)
190
+
191
+ @redis.eval \
192
+ SCRIPT_LEAVE_ROOM,
193
+ 7,
194
+ "store:room:members:#{room_id}",
195
+ "store:room:peak_members:#{room_id}",
196
+ "store:connection:joined:#{connection_id}",
197
+ "store:connection:room:#{connection_id}",
198
+ "store:connection:status:#{connection_id}",
199
+ "store:stats:room_peaks:#{hour}",
200
+ "store:stats:connection_time:#{hour}",
201
+ connection_id,
202
+ PAYLOAD_PEER_LEFT[connection_id],
203
+ now,
204
+ &block
205
+ end
206
+ private :script_leave_room
207
+
208
+ def update_status(connection_id, input_status)
209
+ @redis.get("store:connection:room:#{connection_id}") do |room_id|
210
+ return_error connection_id, 'currently not in any room' unless room_id
211
+
212
+ update_status_without_notifying_peers(connection_id, input_status){
213
+ @redis.smembers("store:room:members:#{room_id}") do |members|
214
+ members.each { |peer_id|
215
+ @publisher.publish "ps:connection:#{peer_id}", {
216
+ event: 'peer_updated_status',
217
+ status: input_status,
218
+ sender_id: connection_id,
219
+ }.to_json
220
+ }
221
+ end
222
+ }
223
+ end
224
+ end
225
+
226
+ def send_to_peer(connection_id, peer_id, data)
227
+ unless data.instance_of? Hash
228
+ return_error connection_id, "cannot send raw data"
229
+ end
230
+
231
+ @redis.get("store:connection:room:#{connection_id}") do |room_id|
232
+ return_error connection_id, 'currently not in any room' unless room_id
233
+
234
+ @redis.sismember("store:room:members:#{room_id}", peer_id) do |is_member|
235
+ return_error connection_id, 'unknown peer' if is_member.nil? || is_member.zero?
236
+
237
+ unless %w[offer answer ice_candidate].include? data['event']
238
+ return_error connection_id, 'event not allowed'
239
+ end
240
+
241
+ @publisher.publish "ps:connection:#{peer_id}", (data || {}).merge("sender_id" => connection_id).to_json
242
+ end
243
+ end
244
+ end
245
+
246
+ def announce_shutdown(seconds = 0)
247
+ warn "Announcing shutdown in #{seconds} seconds"
248
+ @connections.sockets.each { |ws|
249
+ ws.send_text({
250
+ event: 'shutdown',
251
+ seconds: seconds,
252
+ }.to_json)
253
+ }
254
+ end
255
+
256
+ def shutdown!(seconds = 0)
257
+ sleep(seconds)
258
+ @connections.dup.sockets.each{ |ws| ws.close(4200) } # TODO double check this one
259
+ end
260
+
261
+
262
+ private
263
+
264
+
265
+ # TODO shorten
266
+ def update_status_without_notifying_peers(connection_id, input_status, &block)
267
+ if !input_status
268
+ block.call
269
+ return false
270
+ end
271
+
272
+ status = {}
273
+
274
+ if input_status['name']
275
+ if !input_status['name'] || input_status['name'] =~ /\A\s*\z/
276
+ return_error connection_id, 'blank name not allowed'
277
+ end
278
+
279
+ if input_status['name'].size > 50
280
+ return_error connection_id, 'name too long'
281
+ end
282
+
283
+ begin
284
+ valid_encoding = input_status['name'] =~ /\A\p{ASCII}+\z/
285
+ rescue Encoding::CompatibilityError
286
+ valid_encoding = false
287
+ end
288
+
289
+ if !valid_encoding
290
+ input_status['name'] = '*' * input_status['name'].size
291
+ end
292
+
293
+ status['name'] = input_status['name']
294
+ end
295
+
296
+ if input_status['user_agent']
297
+ unless %w[firefox chrome unknown].include? input_status['user_agent']
298
+ return_error connection_id, 'unknown user agent'
299
+ end
300
+
301
+ status['user_agent'] = input_status['user_agent']
302
+ end
303
+
304
+ unless status.empty?
305
+ @redis.hmset "store:connection:status:#{connection_id}", *status.to_a.flatten, &block
306
+ true
307
+ end
308
+ end
309
+
310
+ end
311
+ end