tamashii 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +31 -0
- data/.rubocop.yml +11 -0
- data/.rubocop_todo.yml +91 -0
- data/Gemfile +2 -0
- data/Guardfile +2 -0
- data/Rakefile +2 -0
- data/lib/tamashii.rb +5 -2
- data/lib/tamashii/server.rb +27 -0
- data/lib/tamashii/server/base.rb +31 -0
- data/lib/tamashii/server/client.rb +23 -0
- data/lib/tamashii/server/connection.rb +12 -0
- data/lib/tamashii/server/connection/client_socket.rb +127 -0
- data/lib/tamashii/server/connection/stream.rb +116 -0
- data/lib/tamashii/server/connection/stream_event_loop.rb +124 -0
- data/lib/tamashii/server/rack.rb +31 -0
- data/lib/tamashii/server/response.rb +25 -0
- data/lib/tamashii/server/subscription.rb +10 -0
- data/lib/tamashii/server/subscription/redis.rb +80 -0
- data/lib/tamashii/version.rb +3 -1
- data/tamashii.gemspec +2 -0
- metadata +44 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 971b0a636366074f7e051af483166d7056753fcf
|
4
|
+
data.tar.gz: c75d1df8a9e5c487b1c989170e7aaf09594e315b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '08bd487845177364886ecf70e5cb745446a1801cc6264b3dc4c477cc717ddfa172e00ba4b4edc97a0aba8054d2f151599a2f58e79c6965bda2a7e0f2ba28c7d3'
|
7
|
+
data.tar.gz: 3098ada65a89ee03fcce8ae3c1a99572e20fc70fcd51f282ddb233df58ec7f4ea658c935cf612e56f39e340bc3e73692fb522428d8f967f29f938beed870f9ad
|
data/.gitlab-ci.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# base image
|
2
|
+
image: "ruby:2.4.1"
|
3
|
+
|
4
|
+
# build stages
|
5
|
+
stages:
|
6
|
+
- test
|
7
|
+
|
8
|
+
# cache gems in between builds
|
9
|
+
cache:
|
10
|
+
paths:
|
11
|
+
- vendor/ruby
|
12
|
+
|
13
|
+
# this is a basic example for a gem or script which doesn't use
|
14
|
+
# services such as redis or postgres
|
15
|
+
before_script:
|
16
|
+
- gem install bundler -v 1.13.7 --no-ri --no-rdoc # bundler is not installed with the image
|
17
|
+
- bundle install -j $(nproc) --path vendor # install dependencies into ./vendor/ruby
|
18
|
+
|
19
|
+
# jobs
|
20
|
+
rspec:
|
21
|
+
stage: test
|
22
|
+
script:
|
23
|
+
- bundle exec rspec -p
|
24
|
+
|
25
|
+
rubocop:
|
26
|
+
stage: test
|
27
|
+
services: []
|
28
|
+
before_script:
|
29
|
+
- gem install rubocop
|
30
|
+
script:
|
31
|
+
- rubocop
|
data/.rubocop.yml
ADDED
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2017-05-03 14:59:53 +0800 using RuboCop version 0.48.1.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 3
|
10
|
+
# Configuration parameters: CountComments, ExcludedMethods.
|
11
|
+
Metrics/BlockLength:
|
12
|
+
Max: 36
|
13
|
+
|
14
|
+
# Offense count: 8
|
15
|
+
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
16
|
+
# URISchemes: http, https
|
17
|
+
Metrics/LineLength:
|
18
|
+
Max: 99
|
19
|
+
|
20
|
+
# Offense count: 1
|
21
|
+
# Cop supports --auto-correct.
|
22
|
+
# Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods.
|
23
|
+
# SupportedStyles: line_count_based, semantic, braces_for_chaining
|
24
|
+
# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
|
25
|
+
# FunctionalMethods: let, let!, subject, watch
|
26
|
+
# IgnoredMethods: lambda, proc, it
|
27
|
+
Style/BlockDelimiters:
|
28
|
+
Exclude:
|
29
|
+
- 'spec/tamashii/server_spec.rb'
|
30
|
+
|
31
|
+
# Offense count: 1
|
32
|
+
# Cop supports --auto-correct.
|
33
|
+
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
|
34
|
+
Style/ExtraSpacing:
|
35
|
+
Exclude:
|
36
|
+
- 'tamashii.gemspec'
|
37
|
+
|
38
|
+
# Offense count: 1
|
39
|
+
# Cop supports --auto-correct.
|
40
|
+
# Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
|
41
|
+
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
|
42
|
+
Style/HashSyntax:
|
43
|
+
Exclude:
|
44
|
+
- 'Rakefile'
|
45
|
+
|
46
|
+
# Offense count: 1
|
47
|
+
# Cop supports --auto-correct.
|
48
|
+
Style/MutableConstant:
|
49
|
+
Exclude:
|
50
|
+
- 'lib/tamashii/version.rb'
|
51
|
+
|
52
|
+
# Offense count: 3
|
53
|
+
# Cop supports --auto-correct.
|
54
|
+
# Configuration parameters: PreferredDelimiters.
|
55
|
+
Style/PercentLiteralDelimiters:
|
56
|
+
Exclude:
|
57
|
+
- 'Guardfile'
|
58
|
+
- 'tamashii.gemspec'
|
59
|
+
|
60
|
+
# Offense count: 1
|
61
|
+
# Cop supports --auto-correct.
|
62
|
+
# Configuration parameters: AllowForAlignment.
|
63
|
+
Style/SpaceAroundOperators:
|
64
|
+
Exclude:
|
65
|
+
- 'tamashii.gemspec'
|
66
|
+
|
67
|
+
# Offense count: 18
|
68
|
+
# Cop supports --auto-correct.
|
69
|
+
# Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline.
|
70
|
+
# SupportedStyles: single_quotes, double_quotes
|
71
|
+
Style/StringLiterals:
|
72
|
+
Exclude:
|
73
|
+
- 'Guardfile'
|
74
|
+
- 'Rakefile'
|
75
|
+
- 'bin/console'
|
76
|
+
- 'lib/tamashii/version.rb'
|
77
|
+
- 'tamashii.gemspec'
|
78
|
+
|
79
|
+
# Offense count: 1
|
80
|
+
# Cop supports --auto-correct.
|
81
|
+
# Configuration parameters: EnforcedStyleForMultiline, SupportedStylesForMultiline.
|
82
|
+
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
83
|
+
Style/TrailingCommaInLiteral:
|
84
|
+
Exclude:
|
85
|
+
- 'spec/tamashii/server_spec.rb'
|
86
|
+
|
87
|
+
# Offense count: 2
|
88
|
+
# Cop supports --auto-correct.
|
89
|
+
Style/UnneededPercentQ:
|
90
|
+
Exclude:
|
91
|
+
- 'tamashii.gemspec'
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
data/Rakefile
CHANGED
data/lib/tamashii.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'logger'
|
5
|
+
require 'logger/colors'
|
6
|
+
require 'websocket/driver'
|
7
|
+
require 'rack'
|
8
|
+
require 'nio'
|
9
|
+
require 'thread'
|
10
|
+
require 'redis'
|
11
|
+
|
12
|
+
module Tamashii
|
13
|
+
# :nodoc:
|
14
|
+
module Server
|
15
|
+
autoload :Rack, 'tamashii/server/rack'
|
16
|
+
autoload :Base, 'tamashii/server/base'
|
17
|
+
autoload :Response, 'tamashii/server/response'
|
18
|
+
autoload :Client, 'tamashii/server/client'
|
19
|
+
autoload :Connection, 'tamashii/server/connection'
|
20
|
+
autoload :Subscription, 'tamashii/server/subscription'
|
21
|
+
|
22
|
+
def self.logger
|
23
|
+
# TODO: Add config to set logger path
|
24
|
+
@logger ||= ::Logger.new(STDOUT)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Server
|
5
|
+
# :nodoc:
|
6
|
+
class Base
|
7
|
+
attr_reader :mutex
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@mutex = Monitor.new
|
11
|
+
mutex.synchronize { @instance ||= Rack.new(self, event_loop) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
@instance.call(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
def pubsub
|
19
|
+
@pubsub || mutex.synchronize do
|
20
|
+
@pubsub ||= Subscription::Redis.new(self)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def event_loop
|
25
|
+
@event_loop || mutex.synchronize do
|
26
|
+
@event_loop ||= Connection::StreamEventLoop.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Server
|
5
|
+
# :nodoc:
|
6
|
+
class Client
|
7
|
+
class << self
|
8
|
+
def register(socket)
|
9
|
+
return false unless socket.is_a?(Connection::ClientSocket)
|
10
|
+
clients.add(socket)
|
11
|
+
end
|
12
|
+
|
13
|
+
def unregister(socket)
|
14
|
+
clients.delete(socket)
|
15
|
+
end
|
16
|
+
|
17
|
+
def clients
|
18
|
+
@clients ||= Set.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Server
|
5
|
+
# :nodoc:
|
6
|
+
module Connection
|
7
|
+
autoload :ClientSocket, 'tamashii/server/connection/client_socket'
|
8
|
+
autoload :StreamEventLoop, 'tamashii/server/connection/stream_event_loop'
|
9
|
+
autoload :Stream, 'tamashii/server/connection/stream'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Server
|
5
|
+
module Connection
|
6
|
+
# :nodoc:
|
7
|
+
class ClientSocket
|
8
|
+
def self.determine_url(env)
|
9
|
+
scheme = secure_request?(env) ? 'wss:' : 'ws:'
|
10
|
+
"#{scheme}//#{env['HTTP_HOST']}#{env['REQUEST_URI']}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.secure_request?(env)
|
14
|
+
return true if env['HTTPS'] == 'on'
|
15
|
+
return true if env['HTTP_X_FORWARDED_SSL'] == 'on'
|
16
|
+
return true if env['HTTP_X_FORWARDED_SCHEME'] == 'https'
|
17
|
+
return true if env['HTTP_X_FORWARDED_PROTO'] == 'https'
|
18
|
+
return true if env['rack.url_scheme'] == 'https'
|
19
|
+
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :env, :url
|
24
|
+
attr_accessor :id
|
25
|
+
|
26
|
+
# TODO: Support define protocols
|
27
|
+
def initialize(server, env, event_loop)
|
28
|
+
@server = server
|
29
|
+
@env = env
|
30
|
+
@event_loop = event_loop
|
31
|
+
|
32
|
+
@id ||= env['REMOTE_ADDR']
|
33
|
+
|
34
|
+
@url = ClientSocket.determine_url(@env)
|
35
|
+
@driver = setup_driver
|
36
|
+
|
37
|
+
@stream = Stream.new(@event_loop, self)
|
38
|
+
end
|
39
|
+
|
40
|
+
def start_driver
|
41
|
+
return if @driver.nil?
|
42
|
+
@stream.hijack_rack_socket
|
43
|
+
|
44
|
+
callback = @env['async.callback']
|
45
|
+
callback&.call([101, {}, @stream])
|
46
|
+
|
47
|
+
@driver.start
|
48
|
+
end
|
49
|
+
|
50
|
+
def rack_response
|
51
|
+
start_driver
|
52
|
+
Server.logger.info("Accept new websocket connection from #{env['REMOTE_ADDR']}")
|
53
|
+
Server::Response.new(message: 'WebSocket Connected')
|
54
|
+
end
|
55
|
+
|
56
|
+
def write(data)
|
57
|
+
@stream.write(data)
|
58
|
+
rescue => e
|
59
|
+
emit_error e.message
|
60
|
+
end
|
61
|
+
|
62
|
+
def transmit(message)
|
63
|
+
Server.logger.debug("Send to #{id} with data #{message}")
|
64
|
+
case message
|
65
|
+
when Numeric then @driver.text(message.to_s)
|
66
|
+
when String then @driver.text(message)
|
67
|
+
else
|
68
|
+
@driver.binary(message)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def close
|
73
|
+
# TODO: Define close reason / code
|
74
|
+
@driver.close('', 1000)
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse(data)
|
78
|
+
@driver.parse(data)
|
79
|
+
end
|
80
|
+
|
81
|
+
def client_gone
|
82
|
+
finialize_close
|
83
|
+
end
|
84
|
+
|
85
|
+
def protocol
|
86
|
+
@driver.protocol
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def setup_driver
|
92
|
+
driver = ::WebSocket::Driver.rack(self)
|
93
|
+
|
94
|
+
driver.on(:open) { |_| open }
|
95
|
+
driver.on(:message) { |e| receive_message(e.data) }
|
96
|
+
driver.on(:close) { |e| begin_close(e.reason, e.code) }
|
97
|
+
driver.on(:error) { |e| emit_error(e.message) }
|
98
|
+
|
99
|
+
driver
|
100
|
+
end
|
101
|
+
|
102
|
+
def open
|
103
|
+
Server::Client.register(self)
|
104
|
+
end
|
105
|
+
|
106
|
+
def receive_message(data)
|
107
|
+
@server.pubsub.broadcast(data)
|
108
|
+
end
|
109
|
+
|
110
|
+
def emit_error(message)
|
111
|
+
Server.logger.error("Client #{id} has some error: #{message}")
|
112
|
+
end
|
113
|
+
|
114
|
+
def begin_close(_reason, _code)
|
115
|
+
Server.logger.info("Close connection to #{id}")
|
116
|
+
Client.unregister(self)
|
117
|
+
finialize_close
|
118
|
+
end
|
119
|
+
|
120
|
+
def finialize_close
|
121
|
+
# TODO: Processing close
|
122
|
+
@stream.close
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Server
|
5
|
+
module Connection
|
6
|
+
# :nodoc:
|
7
|
+
class Stream
|
8
|
+
attr_reader :event_loop
|
9
|
+
|
10
|
+
def initialize(event_loop, socket)
|
11
|
+
@event_loop = event_loop
|
12
|
+
@socket = socket
|
13
|
+
@stream_send = socket.env['stream.send']
|
14
|
+
|
15
|
+
@rack_hijack_io = nil
|
16
|
+
@write_lock = Mutex.new
|
17
|
+
|
18
|
+
@write_head = nil
|
19
|
+
@write_buffer = Queue.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def each(&callback)
|
23
|
+
@stream_send ||= callback
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
shutdown
|
28
|
+
@socket.client_gone
|
29
|
+
end
|
30
|
+
|
31
|
+
def shutdown
|
32
|
+
clean_rack_hijack
|
33
|
+
end
|
34
|
+
|
35
|
+
def write(data)
|
36
|
+
return @stream_send.call(data) if @stream_send
|
37
|
+
|
38
|
+
return write_safe(data) if @write_lock.try_lock
|
39
|
+
write_buffer(data)
|
40
|
+
data.bytesize
|
41
|
+
rescue EOFError, Errno::ECONNRESET
|
42
|
+
@socket.client_gone
|
43
|
+
end
|
44
|
+
|
45
|
+
def flush_write_buffer
|
46
|
+
@write_lock.synchronize do
|
47
|
+
loop do
|
48
|
+
return true if @write_buffer.empty? && @write_head.nil?
|
49
|
+
@write_head = @write_buffer.pop if @write_head.nil?
|
50
|
+
|
51
|
+
return unless process_flush
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def receive(data)
|
57
|
+
@socket.parse(data)
|
58
|
+
end
|
59
|
+
|
60
|
+
def hijack_rack_socket
|
61
|
+
return unless @socket.env['rack.hijack']
|
62
|
+
|
63
|
+
@socket.env['rack.hijack'].call
|
64
|
+
@rack_hijack_io = @socket.env['rack.hijack_io']
|
65
|
+
|
66
|
+
@event_loop.attach(@rack_hijack_io, self)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def write_safe(data)
|
72
|
+
return unless @write_head.nil? && @write_buffer.empty?
|
73
|
+
written = @rack_hijack_io.write_nonblock(data, exception: false)
|
74
|
+
|
75
|
+
case written
|
76
|
+
when :wait_writable then write_buffer(data)
|
77
|
+
when data.bytesize then data.bytesize
|
78
|
+
else
|
79
|
+
write_head data.byteslice(written, data.bytesize)
|
80
|
+
end
|
81
|
+
ensure
|
82
|
+
@write_lock.unlock
|
83
|
+
end
|
84
|
+
|
85
|
+
def write_buffer(data)
|
86
|
+
@write_buffer << data
|
87
|
+
@event_loop.writes_pending @rack_hijack_io
|
88
|
+
end
|
89
|
+
|
90
|
+
def write_head(head)
|
91
|
+
@write_head = head
|
92
|
+
@event_loop.writes_pending @rack_hijack_io
|
93
|
+
end
|
94
|
+
|
95
|
+
def process_flush
|
96
|
+
written = @rack_hijack_io.write_nonblock(@write_head, exception: false)
|
97
|
+
case written
|
98
|
+
when :wait_writable then return false
|
99
|
+
when @write_head.bytesize
|
100
|
+
@write_head = nil
|
101
|
+
return true
|
102
|
+
else
|
103
|
+
@write_head = @write_head.byteslice(written, @write_head.bytesize)
|
104
|
+
return false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def clean_rack_hijack
|
109
|
+
return unless @rack_hijack_io
|
110
|
+
@event_loop.detach(@rack_hijack_io, self)
|
111
|
+
@rack_hijack_io = nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Thread.abort_on_exception = true
|
4
|
+
|
5
|
+
module Tamashii
|
6
|
+
module Server
|
7
|
+
module Connection
|
8
|
+
# :nodoc:
|
9
|
+
class StreamEventLoop
|
10
|
+
def initialize
|
11
|
+
@nio = @thread = nil
|
12
|
+
@stopping = false
|
13
|
+
|
14
|
+
@streams = {}
|
15
|
+
|
16
|
+
@todo = Queue.new
|
17
|
+
|
18
|
+
@spawn_mutex = Mutex.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def attach(io, stream)
|
22
|
+
@todo << lambda do
|
23
|
+
@streams[io] = @nio.register(io, :r)
|
24
|
+
@streams[io].value = stream
|
25
|
+
end
|
26
|
+
wakeup
|
27
|
+
end
|
28
|
+
|
29
|
+
def detach(io, _)
|
30
|
+
@todo << lambda do
|
31
|
+
@nio.deregister io
|
32
|
+
@streams.delete io
|
33
|
+
io.close
|
34
|
+
end
|
35
|
+
wakeup
|
36
|
+
end
|
37
|
+
|
38
|
+
def writes_pending(io)
|
39
|
+
@todo << lambda do
|
40
|
+
monitor = @streams[io]
|
41
|
+
monitor&.interests = :rw
|
42
|
+
end
|
43
|
+
wakeup
|
44
|
+
end
|
45
|
+
|
46
|
+
def stop
|
47
|
+
@stopping = true
|
48
|
+
wakeup if @nio
|
49
|
+
end
|
50
|
+
|
51
|
+
def stopped?
|
52
|
+
@stopping
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def spawn
|
58
|
+
return if @thread && @thread.status
|
59
|
+
|
60
|
+
@spawn_mutex.synchronize do
|
61
|
+
return if @thread && @thread.status
|
62
|
+
|
63
|
+
@nio ||= NIO::Selector.new
|
64
|
+
@thread = Thread.new { run }
|
65
|
+
|
66
|
+
return true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def wakeup
|
71
|
+
spawn || @nio.wakeup
|
72
|
+
end
|
73
|
+
|
74
|
+
def run
|
75
|
+
loop do
|
76
|
+
if stopped?
|
77
|
+
@nio.close
|
78
|
+
break
|
79
|
+
end
|
80
|
+
|
81
|
+
@todo.pop(true).call until @todo.empty?
|
82
|
+
|
83
|
+
monitors = @nio.select
|
84
|
+
next unless monitors
|
85
|
+
process(monitors)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def process(monitors)
|
90
|
+
monitors.each do |monitor|
|
91
|
+
io = monitor.io
|
92
|
+
stream = monitor.value
|
93
|
+
|
94
|
+
if monitor.writable?
|
95
|
+
monitor.interests = :r if stream.flush_write_buffer
|
96
|
+
next unless monitor.readable?
|
97
|
+
end
|
98
|
+
|
99
|
+
next unless read(io, stream)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def read(io, stream)
|
104
|
+
incoming = io.read_nonblock(4096, exception: false)
|
105
|
+
case incoming
|
106
|
+
when :wait_readable then false
|
107
|
+
when nil then stream.close
|
108
|
+
else
|
109
|
+
stream.receive incoming
|
110
|
+
end
|
111
|
+
rescue
|
112
|
+
try_close(io, stream)
|
113
|
+
end
|
114
|
+
|
115
|
+
def try_close(io, stream)
|
116
|
+
stream.close
|
117
|
+
rescue
|
118
|
+
@nio.deregister io
|
119
|
+
@streams.delete io
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Server
|
5
|
+
# :nodoc:
|
6
|
+
class Rack
|
7
|
+
def initialize(server, event_loop)
|
8
|
+
@server = server
|
9
|
+
@event_loop = event_loop
|
10
|
+
|
11
|
+
@server.pubsub.subscribe
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
return start_websocket(env) if ::WebSocket::Driver.websocket?(env)
|
16
|
+
start_http(env)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def start_websocket(env)
|
22
|
+
Connection::ClientSocket.new(@server, env, @event_loop).rack_response
|
23
|
+
end
|
24
|
+
|
25
|
+
def start_http(_)
|
26
|
+
# TODO: Supply API for query WebSocket status
|
27
|
+
Server::Response.new(message: 'Invalid protocol or api endpoint')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Server
|
5
|
+
# :nodoc:
|
6
|
+
class Response < ::Rack::Response
|
7
|
+
def initialize(body = {}, status = 200, header = {}, &block)
|
8
|
+
body = process_body(body)
|
9
|
+
header = process_header(header, body.first)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_body(body)
|
14
|
+
[body.merge(version: Tamashii::VERSION).to_json]
|
15
|
+
end
|
16
|
+
|
17
|
+
def process_header(header, body)
|
18
|
+
header.merge(
|
19
|
+
'Content-Type' => 'application/json',
|
20
|
+
'Content-Length' => body.bytesize
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Server
|
5
|
+
module Subscription
|
6
|
+
# :nodoc:
|
7
|
+
class Redis
|
8
|
+
def initialize(server)
|
9
|
+
@server = server
|
10
|
+
end
|
11
|
+
|
12
|
+
def broadcast(payload)
|
13
|
+
Server.logger.info("Broadcasting: #{payload}")
|
14
|
+
broadcast_conn.publish('_tamashii_internal', pack(payload))
|
15
|
+
end
|
16
|
+
|
17
|
+
def subscribe
|
18
|
+
ensure_listener_running
|
19
|
+
end
|
20
|
+
|
21
|
+
def shutdown
|
22
|
+
subscription_conn.unsubscribe
|
23
|
+
end
|
24
|
+
|
25
|
+
def prepare
|
26
|
+
ensure_listener_running
|
27
|
+
end
|
28
|
+
|
29
|
+
def pack(data)
|
30
|
+
case data
|
31
|
+
when Numeric then "N:#{data}"
|
32
|
+
when String then "S:#{data}"
|
33
|
+
else
|
34
|
+
"B:#{data.pack('C*')}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def unpack(data)
|
39
|
+
case data[0..1]
|
40
|
+
when 'N:' then data[2..-1].to_i
|
41
|
+
when 'S:' then data[2..-1]
|
42
|
+
else
|
43
|
+
data[2..-1].unpack('C*')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
def broadcast_conn
|
50
|
+
# TODO: Add config to support set server
|
51
|
+
@conn || @server.mutex.synchronize do
|
52
|
+
@conn ||= ::Redis.new
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def subscription_conn
|
57
|
+
@subscription_conn ||= ::Redis.new
|
58
|
+
end
|
59
|
+
|
60
|
+
def listen
|
61
|
+
Server.logger.info('Starting subscribe redis #_tamashii_internal channel')
|
62
|
+
subscription_conn.without_reconnect do
|
63
|
+
# TODO: Add config to support set namespace
|
64
|
+
subscription_conn.subscribe('_tamashii_internal') do |on|
|
65
|
+
on.message { |_, message| process_message(message) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def process_message(message)
|
71
|
+
Client.clients.each { |client| client.transmit(unpack(message)) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def ensure_listener_running
|
75
|
+
@thread ||= Thread.new { listen }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/tamashii/version.rb
CHANGED
data/tamashii.gemspec
CHANGED
@@ -35,6 +35,8 @@ Gem::Specification.new do |spec|
|
|
35
35
|
spec.add_runtime_dependency "tamashii-common"
|
36
36
|
spec.add_runtime_dependency "websocket-driver"
|
37
37
|
spec.add_runtime_dependency "nio4r"
|
38
|
+
spec.add_runtime_dependency "redis"
|
39
|
+
spec.add_runtime_dependency "logger-colors"
|
38
40
|
|
39
41
|
spec.add_development_dependency 'bundler', '~> 1.14'
|
40
42
|
spec.add_development_dependency 'rake', '~> 10.0'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tamashii
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 蒼時弦也
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: exe
|
12
12
|
cert_chain: []
|
13
|
-
date: 2017-05-
|
13
|
+
date: 2017-05-16 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: concurrent-ruby
|
@@ -82,6 +82,34 @@ dependencies:
|
|
82
82
|
- - ">="
|
83
83
|
- !ruby/object:Gem::Version
|
84
84
|
version: '0'
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: redis
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
type: :runtime
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
name: logger-colors
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
type: :runtime
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
85
113
|
- !ruby/object:Gem::Dependency
|
86
114
|
name: bundler
|
87
115
|
requirement: !ruby/object:Gem::Requirement
|
@@ -190,7 +218,10 @@ extensions: []
|
|
190
218
|
extra_rdoc_files: []
|
191
219
|
files:
|
192
220
|
- ".gitignore"
|
221
|
+
- ".gitlab-ci.yml"
|
193
222
|
- ".rspec"
|
223
|
+
- ".rubocop.yml"
|
224
|
+
- ".rubocop_todo.yml"
|
194
225
|
- ".travis.yml"
|
195
226
|
- Gemfile
|
196
227
|
- Guardfile
|
@@ -199,6 +230,17 @@ files:
|
|
199
230
|
- bin/console
|
200
231
|
- bin/setup
|
201
232
|
- lib/tamashii.rb
|
233
|
+
- lib/tamashii/server.rb
|
234
|
+
- lib/tamashii/server/base.rb
|
235
|
+
- lib/tamashii/server/client.rb
|
236
|
+
- lib/tamashii/server/connection.rb
|
237
|
+
- lib/tamashii/server/connection/client_socket.rb
|
238
|
+
- lib/tamashii/server/connection/stream.rb
|
239
|
+
- lib/tamashii/server/connection/stream_event_loop.rb
|
240
|
+
- lib/tamashii/server/rack.rb
|
241
|
+
- lib/tamashii/server/response.rb
|
242
|
+
- lib/tamashii/server/subscription.rb
|
243
|
+
- lib/tamashii/server/subscription/redis.rb
|
202
244
|
- lib/tamashii/version.rb
|
203
245
|
- tamashii.gemspec
|
204
246
|
homepage: https://github.com/5xRuby/tamashii
|