tamashii 0.1.0 → 0.2.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 +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
|