anycable-rack-server 0.0.1 → 0.1.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/LICENSE +1 -1
- data/README.md +63 -14
- data/lib/anycable-rack-server.rb +2 -1
- data/lib/anycable/{rack-server → rack}/broadcast_subscribers/redis_subscriber.rb +8 -6
- data/lib/anycable/{rack-server → rack}/coders/json.rb +4 -2
- data/lib/anycable/{rack-server → rack}/connection.rb +48 -50
- data/lib/anycable/{rack-server → rack}/errors.rb +1 -2
- data/lib/anycable/{rack-server → rack}/hub.rb +10 -1
- data/lib/anycable/{rack-server → rack}/logging.rb +2 -2
- data/lib/anycable/{rack-server → rack}/middleware.rb +31 -18
- data/lib/anycable/{rack-server → rack}/pinger.rb +3 -4
- data/lib/anycable/rack/railtie.rb +55 -0
- data/lib/anycable/{rack-server → rack}/rpc/client.rb +3 -2
- data/lib/anycable/{rack-server → rack}/rpc/rpc.proto +0 -0
- data/lib/anycable/rack/rpc_runner.rb +60 -0
- data/lib/anycable/rack/server.rb +93 -0
- data/lib/anycable/{rack-server → rack}/socket.rb +8 -12
- data/lib/anycable/{rack-server → rack}/version.rb +2 -2
- metadata +49 -19
- data/lib/anycable/rack-server.rb +0 -107
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e57a096cf575590410db4c25e824d0b754af2b85eb0927e4d9fdf553da4ccea
|
4
|
+
data.tar.gz: e6c5fce5b74f12bc41ce06955812cff608f50c4c66b063a459fa34ca8ed5e8e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 304f417fc3d477625effcc7134c81708d222df3da565613f4f5878b5c0365fecd5ed2a3e9620cc692976051f9a634a66623f8adfce2729664f34882135eeec2b
|
7
|
+
data.tar.gz: d5a926617db60ec606f15b73189da24ed80d69012e87b8b4968ea6e0c1e0b7f26b86a3af593a48804f575dfd2c5c7867070555df968c69ba43926d4c97df4501
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,37 +1,86 @@
|
|
1
|
+
[](https://rubygems.org/gems/anycable-rack-server) [](https://travis-ci.org/anycable/anycable-rack-server)
|
2
|
+
|
1
3
|
# anycable-rack-server
|
2
4
|
|
3
|
-
AnyCable-compatible Rack hijack based Ruby Web Socket server
|
5
|
+
[AnyCable](https://anycable.io)-compatible Rack hijack based Ruby Web Socket server designed for development and testing purposes.
|
4
6
|
|
5
|
-
##
|
7
|
+
## Using with Rack
|
6
8
|
|
7
|
-
Mount the rack middleware
|
8
9
|
```ruby
|
9
|
-
#
|
10
|
-
|
11
|
-
|
10
|
+
# Initialize server instance first.
|
11
|
+
#
|
12
|
+
# NOTE: you must run RPC server yourself and provide its host
|
13
|
+
ws_server = AnyCable::Rack::Server.new rpc_host: "localhost:50051"
|
14
|
+
|
15
|
+
app = Rack::Builder.new do
|
16
|
+
map "/cable" do
|
17
|
+
run ws_server
|
18
|
+
end
|
12
19
|
end
|
20
|
+
|
21
|
+
# NOTE: don't forget to call `start!` method
|
22
|
+
ws_server.start!
|
23
|
+
|
24
|
+
run app
|
13
25
|
```
|
14
26
|
|
27
|
+
## Usage with Rails
|
28
|
+
|
29
|
+
Add `gem "anycable-rack-server"` to you `Gemfile` and make sure your Action Cable adapter is set to `:any_cable`. That's it! We automatically start AnyCable Rack server for your at `/cable` path.
|
30
|
+
|
15
31
|
## Settings
|
16
32
|
|
17
|
-
|
33
|
+
You can customize the headers being sent with each gRPC request.
|
18
34
|
|
19
35
|
Default headers: `'cookie', 'x-api-token'`.
|
20
36
|
|
21
|
-
Can be specified via
|
37
|
+
Can be specified via options:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
ws_server = AnyCable::Rack::Server.new(
|
41
|
+
rpc_host: "localhost:50051",
|
42
|
+
headers: ["cookie", "x-my-header"]
|
43
|
+
)
|
22
44
|
```
|
23
|
-
|
45
|
+
|
46
|
+
In case of Rails you can set server options via `config.any_cable_rack`:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# <environment>.rb
|
50
|
+
config.any_cable_rack.headers = %w[cookie]
|
51
|
+
config.any_cable_rack.mount_path = "/cable"
|
52
|
+
# NOTE: here we specify only the port (we assume that a server is running locally)
|
53
|
+
config.any_cable_rack.rpc_port = 50051
|
24
54
|
```
|
25
55
|
|
26
|
-
|
56
|
+
## Running RPC from the same process
|
57
|
+
|
58
|
+
The goal of the Rack server is to simplify the development/testing process. But we still have to run the RPC server.
|
59
|
+
|
60
|
+
This gem also provides a way to run RPC server from the same process (spawning a new process):
|
27
61
|
|
28
62
|
```ruby
|
29
|
-
|
30
|
-
|
63
|
+
# in Rack app
|
64
|
+
|
65
|
+
AnyCable::Rack::RPCRunner.run(
|
66
|
+
root_dir: "<path to your app root directory to run `anycable` from>",
|
67
|
+
rpc_host: "...", # optional host to run RPC server on (defaults to '[::]::50051')
|
68
|
+
command_args: [] # additional CLI arguments
|
69
|
+
)
|
70
|
+
|
71
|
+
# in Rails app you can just specify the configuration parameter (`false` by default)
|
72
|
+
# and we'll take care of it
|
73
|
+
config.any_cable_rack.run_rpc = true
|
31
74
|
```
|
32
75
|
|
33
76
|
## Testing
|
34
77
|
|
35
|
-
Run units with `rake`.
|
78
|
+
Run units with `bundle exec rake`.
|
79
|
+
|
80
|
+
## Contributing
|
81
|
+
|
82
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/anycable/anycable-rack-server.
|
83
|
+
|
84
|
+
## License
|
36
85
|
|
37
|
-
|
86
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/lib/anycable-rack-server.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "redis"
|
4
|
+
require "json"
|
4
5
|
|
5
6
|
module AnyCable
|
6
|
-
module
|
7
|
+
module Rack
|
7
8
|
module BroadcastSubscribers
|
9
|
+
# Redis Pub/Sub subscriber
|
8
10
|
class RedisSubscriber
|
9
11
|
attr_reader :hub, :coder, :redis_conn, :threads
|
10
12
|
|
11
|
-
def initialize(hub:, coder:, options
|
13
|
+
def initialize(hub:, coder:, **options)
|
12
14
|
@hub = hub
|
13
15
|
@coder = coder
|
14
16
|
@redis_conn = ::Redis.new(options)
|
@@ -18,13 +20,13 @@ module AnyCable
|
|
18
20
|
def subscribe(channel)
|
19
21
|
@threads[channel] = Thread.new do
|
20
22
|
redis_conn.subscribe(channel) do |on|
|
21
|
-
on.message { |_channel, msg|
|
23
|
+
on.message { |_channel, msg| handle_message(msg) }
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
28
|
def unsubscribe(channel)
|
27
|
-
@threads[channel]
|
29
|
+
@threads[channel]&.terminate
|
28
30
|
@threads.delete(channel)
|
29
31
|
end
|
30
32
|
|
@@ -32,7 +34,7 @@ module AnyCable
|
|
32
34
|
|
33
35
|
def handle_message(msg)
|
34
36
|
data = JSON.parse(msg)
|
35
|
-
hub.broadcast(data[
|
37
|
+
hub.broadcast(data["stream"], data["data"], coder)
|
36
38
|
end
|
37
39
|
end
|
38
40
|
end
|
@@ -1,33 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "securerandom"
|
4
|
+
require "set"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
require "anycable/rack/rpc/client"
|
8
|
+
require "anycable/rack/logging"
|
9
|
+
require "anycable/rack/errors"
|
6
10
|
|
7
11
|
module AnyCable
|
8
|
-
|
9
|
-
|
10
|
-
class Connection
|
11
|
-
# rubocop:enable Metrics/LineLength
|
12
|
+
module Rack
|
13
|
+
class Connection # :nodoc:
|
12
14
|
include Logging
|
13
15
|
|
14
16
|
attr_reader :coder,
|
15
|
-
:
|
17
|
+
:headers,
|
16
18
|
:hub,
|
17
19
|
:socket,
|
18
20
|
:rpc_client,
|
19
|
-
:
|
21
|
+
:sid
|
20
22
|
|
21
|
-
def initialize(socket, hub
|
22
|
-
@socket
|
23
|
-
@coder
|
24
|
-
@
|
25
|
-
@
|
26
|
-
@
|
23
|
+
def initialize(socket, hub:, coder:, rpc_host:, headers:)
|
24
|
+
@socket = socket
|
25
|
+
@coder = coder
|
26
|
+
@headers = headers
|
27
|
+
@hub = hub
|
28
|
+
@sid = SecureRandom.hex(6)
|
27
29
|
|
28
|
-
@rpc_client = RPC::Client.new(
|
30
|
+
@rpc_client = RPC::Client.new(rpc_host)
|
29
31
|
|
30
|
-
@_identifiers =
|
32
|
+
@_identifiers = "{}"
|
31
33
|
@_subscriptions = Set.new
|
32
34
|
end
|
33
35
|
|
@@ -44,17 +46,21 @@ module AnyCable
|
|
44
46
|
|
45
47
|
def handle_command(websocket_message)
|
46
48
|
decoded = decode(websocket_message)
|
47
|
-
command = decoded.delete(
|
49
|
+
command = decoded.delete("command")
|
50
|
+
|
51
|
+
channel_identifier = decoded["identifier"]
|
48
52
|
|
49
|
-
|
53
|
+
log(:debug) { "Command: #{decoded}" }
|
50
54
|
|
51
55
|
case command
|
52
|
-
when
|
53
|
-
when
|
54
|
-
when
|
56
|
+
when "subscribe" then subscribe(channel_identifier)
|
57
|
+
when "unsubscribe" then unsubscribe(channel_identifier)
|
58
|
+
when "message" then send_message(channel_identifier, decoded["data"])
|
55
59
|
else
|
56
|
-
|
60
|
+
log(:error, "Command not found #{command}")
|
57
61
|
end
|
62
|
+
rescue Exception => e
|
63
|
+
log(:error, "Failed to execute command #{command}: #{e.message}")
|
58
64
|
end
|
59
65
|
|
60
66
|
private
|
@@ -88,7 +94,7 @@ module AnyCable
|
|
88
94
|
)
|
89
95
|
end
|
90
96
|
|
91
|
-
def rpc_command(command, identifier, data =
|
97
|
+
def rpc_command(command, identifier, data = "")
|
92
98
|
rpc_client.command(
|
93
99
|
command: command,
|
94
100
|
identifier: identifier,
|
@@ -98,43 +104,31 @@ module AnyCable
|
|
98
104
|
end
|
99
105
|
|
100
106
|
def subscribe(identifier)
|
101
|
-
response = rpc_command(
|
107
|
+
response = rpc_command("subscribe", identifier)
|
102
108
|
if response.status == :SUCCESS
|
103
109
|
@_subscriptions.add(identifier)
|
104
|
-
|
105
|
-
log(:
|
110
|
+
elsif response.status == :ERROR
|
111
|
+
log(:error, "RPC subscribe command failed: #{response.inspect}")
|
106
112
|
end
|
107
113
|
process_command(response, identifier)
|
108
114
|
end
|
109
115
|
|
110
116
|
def unsubscribe(identifier)
|
111
|
-
response = rpc_command(
|
117
|
+
response = rpc_command("unsubscribe", identifier)
|
112
118
|
if response.status == :SUCCESS
|
113
119
|
@_subscriptions.delete(identifier)
|
114
|
-
|
115
|
-
log(:
|
120
|
+
elsif response.status == :ERROR
|
121
|
+
log(:error, "RPC unsubscribe command failed: #{response.inspect}")
|
116
122
|
end
|
117
123
|
process_command(response, identifier)
|
118
124
|
end
|
119
125
|
|
120
126
|
def send_message(identifier, data)
|
121
|
-
response = rpc_command(
|
122
|
-
|
123
|
-
log(:debug, log_fmt("RPC message command failed: #{response.inspect}"))
|
124
|
-
end
|
127
|
+
response = rpc_command("message", identifier, data)
|
128
|
+
log(:error, "RPC message command failed: #{response.inspect}") if response.status == :ERROR
|
125
129
|
process_command(response, identifier)
|
126
130
|
end
|
127
131
|
|
128
|
-
def headers
|
129
|
-
@headers ||= begin
|
130
|
-
header_names.inject({}) do |acc, name|
|
131
|
-
header_val = request.env["HTTP_#{name.gsub(/-/,'_').upcase}"]
|
132
|
-
acc[name] = header_val unless header_val.nil? || header_val.empty?
|
133
|
-
acc
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
132
|
def process_command(response, identifier)
|
139
133
|
response.transmissions.each { |transmission| transmit(decode(transmission)) }
|
140
134
|
hub.remove_channel(socket, identifier) if response.stop_streams
|
@@ -146,23 +140,23 @@ module AnyCable
|
|
146
140
|
if response.status == :SUCCESS
|
147
141
|
@_identifiers = response.identifiers
|
148
142
|
response.transmissions.each { |transmission| transmit(decode(transmission)) }
|
149
|
-
log(:debug) {
|
143
|
+
log(:debug) { "Opened" }
|
150
144
|
else
|
151
|
-
log(:error,
|
145
|
+
log(:error, "RPC connection command failed: #{response.inspect}")
|
152
146
|
close_connection
|
153
147
|
end
|
154
148
|
end
|
155
149
|
|
156
150
|
def process_close(response)
|
157
151
|
if response.status == :SUCCESS
|
158
|
-
log(:debug) {
|
152
|
+
log(:debug) { "Closed" }
|
159
153
|
else
|
160
|
-
log(:error,
|
154
|
+
log(:error, "RPC disconnection command failed: #{response.inspect}")
|
161
155
|
end
|
162
156
|
end
|
163
157
|
|
164
158
|
def reset_connection
|
165
|
-
@_identifiers =
|
159
|
+
@_identifiers = "{}"
|
166
160
|
@_subscriptions = []
|
167
161
|
|
168
162
|
hub.remove_socket(socket)
|
@@ -181,8 +175,12 @@ module AnyCable
|
|
181
175
|
coder.decode(websocket_message)
|
182
176
|
end
|
183
177
|
|
178
|
+
def log(level, msg = nil)
|
179
|
+
super(level, msg ? log_fmt(msg) : nil) { log_fmt(yield) }
|
180
|
+
end
|
181
|
+
|
184
182
|
def log_fmt(msg)
|
185
|
-
"[
|
183
|
+
"[sid=#{sid}] #{msg}"
|
186
184
|
end
|
187
185
|
end
|
188
186
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set"
|
4
|
+
|
3
5
|
module AnyCable
|
4
|
-
module
|
6
|
+
module Rack
|
5
7
|
# From https://github.com/rails/rails/blob/v5.0.1/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
|
6
8
|
class Hub
|
7
9
|
attr_reader :streams, :sockets
|
@@ -67,6 +69,13 @@ module AnyCable
|
|
67
69
|
end
|
68
70
|
end
|
69
71
|
|
72
|
+
def close_all
|
73
|
+
hub.sockets.dup.each do |socket|
|
74
|
+
hub.remove_socket(socket)
|
75
|
+
socket.close
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
70
79
|
private
|
71
80
|
|
72
81
|
def cleanup(stream, socket, channel)
|
@@ -1,27 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
|
5
|
-
require
|
3
|
+
require "websocket"
|
4
|
+
|
5
|
+
require "anycable/rack/connection"
|
6
|
+
require "anycable/rack/errors"
|
7
|
+
require "anycable/rack/socket"
|
6
8
|
|
7
9
|
module AnyCable
|
8
|
-
module
|
9
|
-
class Middleware
|
10
|
-
PROTOCOLS = [
|
10
|
+
module Rack
|
11
|
+
class Middleware # :nodoc:
|
12
|
+
PROTOCOLS = ["actioncable-v1-json", "actioncable-unsupported"].freeze
|
11
13
|
attr_reader :pinger,
|
12
14
|
:hub,
|
13
15
|
:coder,
|
14
16
|
:rpc_host,
|
15
|
-
:
|
16
|
-
:server_id
|
17
|
+
:header_names
|
17
18
|
|
18
|
-
def initialize(
|
19
|
+
def initialize(pinger:, hub:, coder:, rpc_host:, header_names:)
|
19
20
|
@pinger = pinger
|
20
21
|
@hub = hub
|
21
22
|
@coder = coder
|
22
23
|
@rpc_host = rpc_host
|
23
|
-
@
|
24
|
-
@server_id = server_id
|
24
|
+
@header_names = header_names
|
25
25
|
end
|
26
26
|
|
27
27
|
def call(env)
|
@@ -40,34 +40,40 @@ module AnyCable
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def rack_hijack(env)
|
43
|
-
raise Errors::HijackNotAvailable unless env[
|
43
|
+
raise Errors::HijackNotAvailable unless env["rack.hijack"]
|
44
44
|
|
45
|
-
env[
|
45
|
+
env["rack.hijack"].call
|
46
46
|
send_handshake(env)
|
47
47
|
end
|
48
48
|
|
49
49
|
def send_handshake(env)
|
50
50
|
handshake.from_rack(env)
|
51
|
-
env[
|
51
|
+
env["rack.hijack_io"].write(handshake.to_s)
|
52
52
|
end
|
53
53
|
|
54
54
|
def listen_socket(env)
|
55
|
-
socket = Socket.new(env, env[
|
55
|
+
socket = Socket.new(env, env["rack.hijack_io"], handshake.version)
|
56
56
|
init_connection(socket)
|
57
57
|
init_pinger(socket)
|
58
58
|
socket.listen
|
59
59
|
end
|
60
60
|
|
61
61
|
def not_found
|
62
|
-
[404, {
|
62
|
+
[404, { "Content-Type" => "text/plain" }, ["Not Found"]]
|
63
63
|
end
|
64
64
|
|
65
65
|
def websocket?(env)
|
66
|
-
env[
|
66
|
+
env["HTTP_UPGRADE"] == "websocket"
|
67
67
|
end
|
68
68
|
|
69
69
|
def init_connection(socket)
|
70
|
-
connection = Connection.new(
|
70
|
+
connection = Connection.new(
|
71
|
+
socket,
|
72
|
+
hub: hub,
|
73
|
+
coder: coder,
|
74
|
+
rpc_host: rpc_host,
|
75
|
+
headers: fetch_headers(socket.request)
|
76
|
+
)
|
71
77
|
socket.onopen { connection.handle_open }
|
72
78
|
socket.onclose { connection.handle_close }
|
73
79
|
socket.onmessage { |data| connection.handle_command(data) }
|
@@ -77,6 +83,13 @@ module AnyCable
|
|
77
83
|
pinger.add(socket)
|
78
84
|
socket.onclose { pinger.remove(socket) }
|
79
85
|
end
|
86
|
+
|
87
|
+
def fetch_headers(request)
|
88
|
+
header_names.each_with_object({}) do |name, acc|
|
89
|
+
header_val = request.env["HTTP_#{name.tr('-', '_').upcase}"]
|
90
|
+
acc[name] = header_val unless header_val.nil? || header_val.empty?
|
91
|
+
end
|
92
|
+
end
|
80
93
|
end
|
81
94
|
end
|
82
95
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "json"
|
4
|
+
|
3
5
|
module AnyCable
|
4
|
-
module
|
6
|
+
module Rack
|
5
7
|
# Sends pings to sockets
|
6
8
|
class Pinger
|
7
9
|
INTERVAL = 3
|
@@ -9,7 +11,6 @@ module AnyCable
|
|
9
11
|
def initialize
|
10
12
|
@_sockets = []
|
11
13
|
@_stopped = false
|
12
|
-
run
|
13
14
|
end
|
14
15
|
|
15
16
|
def add(socket)
|
@@ -24,7 +25,6 @@ module AnyCable
|
|
24
25
|
@_stopped = true
|
25
26
|
end
|
26
27
|
|
27
|
-
# rubocop: disable Metrics/MethodLength
|
28
28
|
def run
|
29
29
|
Thread.new do
|
30
30
|
loop do
|
@@ -41,7 +41,6 @@ module AnyCable
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
44
|
-
# rubocop: enable Metrics/MethodLength
|
45
44
|
|
46
45
|
private
|
47
46
|
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module Rack
|
5
|
+
class Railtie < ::Rails::Railtie # :nodoc: all
|
6
|
+
class Config < Anyway::Config
|
7
|
+
config_name :anycable_rack
|
8
|
+
env_prefix "ANYCABLE_RACK"
|
9
|
+
|
10
|
+
attr_config mount_path: "/cable",
|
11
|
+
headers: AnyCable::Rack::Server::DEFAULT_HEADERS,
|
12
|
+
rpc_port: 50_051,
|
13
|
+
rpc_host: "localhost",
|
14
|
+
run_rpc: false,
|
15
|
+
running_rpc: false
|
16
|
+
|
17
|
+
private :running_rpc=
|
18
|
+
end
|
19
|
+
|
20
|
+
config.before_configuration do
|
21
|
+
config.any_cable_rack = Config.new
|
22
|
+
end
|
23
|
+
|
24
|
+
initializer "anycable.rack.mount", after: "action_cable.routes" do
|
25
|
+
config.after_initialize do |app|
|
26
|
+
config = app.config.any_cable_rack
|
27
|
+
|
28
|
+
# Only if AnyCable adapter is used
|
29
|
+
next unless ::ActionCable.server.config.cable&.fetch("adapter", nil) == "any_cable"
|
30
|
+
|
31
|
+
server = AnyCable::Rack::Server.new(
|
32
|
+
headers: config.headers,
|
33
|
+
rpc_host: "#{config.rpc_host}:#{config.rpc_port}"
|
34
|
+
)
|
35
|
+
|
36
|
+
app.routes.prepend do
|
37
|
+
mount server => config.mount_path
|
38
|
+
end
|
39
|
+
|
40
|
+
if config.run_rpc && !config.running_rpc
|
41
|
+
AnyCable::Rack::RPCRunner.run(
|
42
|
+
rpc_host: "[::]:#{config.rpc_port}",
|
43
|
+
root_dir: ::Rails.root.to_s,
|
44
|
+
env: {
|
45
|
+
"ANYCABLE_RACK_RUNNING_RPC" => "true"
|
46
|
+
}
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
server.start!
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
File without changes
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable"
|
4
|
+
require "anycable/rack/logging"
|
5
|
+
|
6
|
+
$stdout.sync = true
|
7
|
+
|
8
|
+
module AnyCable
|
9
|
+
module Rack
|
10
|
+
# Runs AnyCable CLI in a separate process
|
11
|
+
module RPCRunner
|
12
|
+
class << self
|
13
|
+
include Logging
|
14
|
+
|
15
|
+
attr_accessor :running, :pid
|
16
|
+
|
17
|
+
def run(root_dir:, command_args: [], rpc_host: "[::]:50051", env: {})
|
18
|
+
return if @running
|
19
|
+
|
20
|
+
command_args << "--rpc-host=\"#{rpc_host}\""
|
21
|
+
|
22
|
+
command = "bundle exec anycable #{command_args.join(' ')}"
|
23
|
+
|
24
|
+
log(:info, "Running AnyCable (from #{root_dir}): #{command}")
|
25
|
+
|
26
|
+
out = AnyCable.config.debug? ? STDOUT : IO::NULL
|
27
|
+
|
28
|
+
@pid = Dir.chdir(root_dir) do
|
29
|
+
Process.spawn(
|
30
|
+
env,
|
31
|
+
command,
|
32
|
+
out: out,
|
33
|
+
err: out
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
log(:debug) { "AnyCable PID: #{pid}" }
|
38
|
+
|
39
|
+
@running = true
|
40
|
+
|
41
|
+
at_exit { stop }
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop
|
45
|
+
return unless running
|
46
|
+
|
47
|
+
log(:debug) { "Terminate PID: #{pid}" }
|
48
|
+
|
49
|
+
Process.kill("SIGKILL", pid)
|
50
|
+
|
51
|
+
@running = false
|
52
|
+
end
|
53
|
+
|
54
|
+
def running?
|
55
|
+
running == true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable"
|
4
|
+
|
5
|
+
require "anycable/rack/hub"
|
6
|
+
require "anycable/rack/pinger"
|
7
|
+
require "anycable/rack/errors"
|
8
|
+
require "anycable/rack/middleware"
|
9
|
+
require "anycable/rack/logging"
|
10
|
+
require "anycable/rack/rpc_runner"
|
11
|
+
require "anycable/rack/broadcast_subscribers/redis_subscriber"
|
12
|
+
require "anycable/rack/coders/json"
|
13
|
+
|
14
|
+
module AnyCable # :nodoc: all
|
15
|
+
module Rack
|
16
|
+
class Server
|
17
|
+
include Logging
|
18
|
+
|
19
|
+
DEFAULT_HEADERS = %w[cookie x-api-token].freeze
|
20
|
+
|
21
|
+
attr_reader :broadcast,
|
22
|
+
:coder,
|
23
|
+
:hub,
|
24
|
+
:middleware,
|
25
|
+
:pinger,
|
26
|
+
:pubsub_channel,
|
27
|
+
:rpc_host,
|
28
|
+
:headers
|
29
|
+
|
30
|
+
def initialize(*args)
|
31
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
32
|
+
|
33
|
+
@hub = Hub.new
|
34
|
+
@pinger = Pinger.new
|
35
|
+
@coder = options.fetch(:coder, Coders::JSON)
|
36
|
+
@pubsub_channel = pubsub_channel
|
37
|
+
|
38
|
+
@headers = options.fetch(:headers, DEFAULT_HEADERS)
|
39
|
+
@rpc_host = options.fetch(:rpc_host)
|
40
|
+
|
41
|
+
@broadcast = BroadcastSubscribers::RedisSubscriber.new(
|
42
|
+
hub: hub,
|
43
|
+
coder: coder,
|
44
|
+
**AnyCable.config.to_redis_params
|
45
|
+
)
|
46
|
+
|
47
|
+
@middleware = Middleware.new(
|
48
|
+
header_names: headers,
|
49
|
+
pinger: pinger,
|
50
|
+
hub: hub,
|
51
|
+
rpc_host: rpc_host,
|
52
|
+
coder: coder
|
53
|
+
)
|
54
|
+
|
55
|
+
log(:info) { "Using RPC server at #{rpc_host}" }
|
56
|
+
end
|
57
|
+
# rubocop:enable
|
58
|
+
|
59
|
+
def start!
|
60
|
+
log(:info) { "Starting..." }
|
61
|
+
|
62
|
+
pinger.run
|
63
|
+
|
64
|
+
broadcast.subscribe(AnyCable.config.redis_channel)
|
65
|
+
|
66
|
+
log(:info) { "Subscribed to #{AnyCable.config.redis_channel}" }
|
67
|
+
|
68
|
+
@_started = true
|
69
|
+
end
|
70
|
+
|
71
|
+
def started?
|
72
|
+
@_started == true
|
73
|
+
end
|
74
|
+
|
75
|
+
def stop
|
76
|
+
return unless started?
|
77
|
+
|
78
|
+
@_started = false
|
79
|
+
broadcast_subscriber.unsubscribe(@_redis_channel)
|
80
|
+
pinger.stop
|
81
|
+
hub.close_all
|
82
|
+
end
|
83
|
+
|
84
|
+
def call(env)
|
85
|
+
middleware.call(env)
|
86
|
+
end
|
87
|
+
|
88
|
+
def inspect
|
89
|
+
"#<AnyCable::Rack::Server(rpc_host: #{rpc_host}, headers: [#{headers.join(', ')}])>"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "anycable/rack/logging"
|
4
4
|
|
5
5
|
module AnyCable
|
6
|
-
module
|
6
|
+
module Rack
|
7
|
+
# Socket wrapper
|
7
8
|
class Socket
|
8
9
|
include Logging
|
9
10
|
attr_reader :version, :socket
|
@@ -34,7 +35,7 @@ module AnyCable
|
|
34
35
|
end
|
35
36
|
|
36
37
|
def request
|
37
|
-
@
|
38
|
+
@request ||= ::Rack::Request.new(@env)
|
38
39
|
end
|
39
40
|
|
40
41
|
def onopen(&block)
|
@@ -53,11 +54,10 @@ module AnyCable
|
|
53
54
|
@_error_handlers << block
|
54
55
|
end
|
55
56
|
|
56
|
-
# rubocop: disable Metrics/MethodLength
|
57
57
|
def listen
|
58
58
|
keepalive
|
59
59
|
Thread.new do
|
60
|
-
Thread.current.
|
60
|
+
Thread.current.report_on_exception = true
|
61
61
|
begin
|
62
62
|
@_open_handlers.each(&:call)
|
63
63
|
each_frame do |data|
|
@@ -76,7 +76,6 @@ module AnyCable
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
end
|
79
|
-
# rubocop: enable Metrics/MethodLength
|
80
79
|
|
81
80
|
def close
|
82
81
|
return unless @_active
|
@@ -105,7 +104,7 @@ module AnyCable
|
|
105
104
|
frame = WebSocket::Frame::Outgoing::Server.new(version: version, type: :close, code: 1000)
|
106
105
|
socket.write(frame.to_s) if frame.supported?
|
107
106
|
socket.close
|
108
|
-
rescue IOError, Errno::EPIPE, Errno::ETIMEDOUT
|
107
|
+
rescue IOError, Errno::EPIPE, Errno::ETIMEDOUT
|
109
108
|
# already closed
|
110
109
|
end
|
111
110
|
|
@@ -124,17 +123,14 @@ module AnyCable
|
|
124
123
|
end
|
125
124
|
end
|
126
125
|
|
127
|
-
# rubocop:disable Metrics/AbcSize
|
128
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
129
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
130
|
-
# rubocop:disable Metrics/MethodLength
|
131
126
|
def each_frame
|
132
127
|
framebuffer = WebSocket::Frame::Incoming::Server.new(version: version)
|
133
128
|
while IO.select([socket])
|
134
129
|
if socket.respond_to?(:recvfrom)
|
135
130
|
data, _addrinfo = socket.recvfrom(2000)
|
136
131
|
else
|
137
|
-
data
|
132
|
+
data = socket.readpartial(2000)
|
133
|
+
_addrinfo = socket.peeraddr
|
138
134
|
end
|
139
135
|
|
140
136
|
break if data.empty?
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: anycable-rack-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yulia Oletskaya
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: anycable
|
@@ -58,28 +58,42 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 0.8.4
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 0.8.4
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: minitest
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '5.
|
75
|
+
version: '5.10'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '5.
|
82
|
+
version: '5.10'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: puma
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: rake
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,6 +108,20 @@ dependencies:
|
|
94
108
|
- - "~>"
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '12.3'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.60.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.60.0
|
97
125
|
description: AnyCable-compatible Ruby Rack middleware
|
98
126
|
email: yulia.oletskaya@gmail.com
|
99
127
|
executables: []
|
@@ -103,19 +131,21 @@ files:
|
|
103
131
|
- LICENSE
|
104
132
|
- README.md
|
105
133
|
- lib/anycable-rack-server.rb
|
106
|
-
- lib/anycable/rack
|
107
|
-
- lib/anycable/rack
|
108
|
-
- lib/anycable/rack
|
109
|
-
- lib/anycable/rack
|
110
|
-
- lib/anycable/rack
|
111
|
-
- lib/anycable/rack
|
112
|
-
- lib/anycable/rack
|
113
|
-
- lib/anycable/rack
|
114
|
-
- lib/anycable/rack
|
115
|
-
- lib/anycable/rack
|
116
|
-
- lib/anycable/rack
|
117
|
-
- lib/anycable/rack
|
118
|
-
- lib/anycable/rack
|
134
|
+
- lib/anycable/rack/broadcast_subscribers/redis_subscriber.rb
|
135
|
+
- lib/anycable/rack/coders/json.rb
|
136
|
+
- lib/anycable/rack/connection.rb
|
137
|
+
- lib/anycable/rack/errors.rb
|
138
|
+
- lib/anycable/rack/hub.rb
|
139
|
+
- lib/anycable/rack/logging.rb
|
140
|
+
- lib/anycable/rack/middleware.rb
|
141
|
+
- lib/anycable/rack/pinger.rb
|
142
|
+
- lib/anycable/rack/railtie.rb
|
143
|
+
- lib/anycable/rack/rpc/client.rb
|
144
|
+
- lib/anycable/rack/rpc/rpc.proto
|
145
|
+
- lib/anycable/rack/rpc_runner.rb
|
146
|
+
- lib/anycable/rack/server.rb
|
147
|
+
- lib/anycable/rack/socket.rb
|
148
|
+
- lib/anycable/rack/version.rb
|
119
149
|
homepage:
|
120
150
|
licenses:
|
121
151
|
- MIT
|
data/lib/anycable/rack-server.rb
DELETED
@@ -1,107 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'set'
|
4
|
-
require 'json'
|
5
|
-
require 'anycable'
|
6
|
-
require 'websocket'
|
7
|
-
require 'securerandom'
|
8
|
-
require 'anycable/rack-server/hub'
|
9
|
-
require 'anycable/rack-server/pinger'
|
10
|
-
require 'anycable/rack-server/errors'
|
11
|
-
require 'anycable/rack-server/middleware'
|
12
|
-
require 'anycable/rack-server/broadcast_subscribers/redis_subscriber'
|
13
|
-
require 'anycable/rack-server/coders/json'
|
14
|
-
|
15
|
-
module AnyCable
|
16
|
-
module RackServer
|
17
|
-
DEFAULT_OPTIONS = {
|
18
|
-
headers: ['cookie', 'x-api-token']
|
19
|
-
}.freeze
|
20
|
-
|
21
|
-
class << self
|
22
|
-
attr_reader :broadcast_subscriber,
|
23
|
-
:coder,
|
24
|
-
:hub,
|
25
|
-
:middleware,
|
26
|
-
:pinger,
|
27
|
-
:server_id
|
28
|
-
|
29
|
-
def start(options = {})
|
30
|
-
options = DEFAULT_OPTIONS.merge(options)
|
31
|
-
@hub = Hub.new
|
32
|
-
@pinger = Pinger.new
|
33
|
-
@coder = Coders::JSON
|
34
|
-
|
35
|
-
rpc_host = unpack_host(AnyCable.config.rpc_host)
|
36
|
-
headers = parse_env_headers || options[:headers]
|
37
|
-
|
38
|
-
@server_id = "anycable-rack-server-#{SecureRandom.hex}"
|
39
|
-
@middleware = Middleware.new(
|
40
|
-
nil,
|
41
|
-
pinger: pinger,
|
42
|
-
hub: hub,
|
43
|
-
coder: coder,
|
44
|
-
rpc_host: rpc_host,
|
45
|
-
headers: headers,
|
46
|
-
server_id: server_id
|
47
|
-
)
|
48
|
-
|
49
|
-
broadcast_subscribe
|
50
|
-
|
51
|
-
@_started = true
|
52
|
-
end
|
53
|
-
|
54
|
-
def started?
|
55
|
-
@_started == true
|
56
|
-
end
|
57
|
-
|
58
|
-
def stop
|
59
|
-
return unless started?
|
60
|
-
|
61
|
-
@_started = false
|
62
|
-
broadcast_subscriber.unsubscribe(@_redis_channel)
|
63
|
-
pinger.stop
|
64
|
-
|
65
|
-
hub.sockets.each do |socket|
|
66
|
-
hub.remove_socket(socket)
|
67
|
-
socket.close
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
private
|
72
|
-
|
73
|
-
def broadcast_subscribe
|
74
|
-
@_redis_params = AnyCable.config.to_redis_params
|
75
|
-
@_redis_channel = AnyCable.config.redis_channel
|
76
|
-
|
77
|
-
@broadcast_subscriber = BroadcastSubscribers::RedisSubscriber.new(
|
78
|
-
hub: @hub,
|
79
|
-
coder: @coder,
|
80
|
-
options: @_redis_params
|
81
|
-
)
|
82
|
-
|
83
|
-
@broadcast_subscriber.subscribe(@_redis_channel)
|
84
|
-
end
|
85
|
-
|
86
|
-
def parse_env_headers
|
87
|
-
headers = ENV['ANYCABLE_HEADERS'].to_s.split(',')
|
88
|
-
return nil if headers.empty?
|
89
|
-
headers
|
90
|
-
end
|
91
|
-
|
92
|
-
def unpack_host(str)
|
93
|
-
str.gsub('[::]', '0.0.0.0')
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
class Rack
|
99
|
-
def initialize(_app = nil, options = {})
|
100
|
-
AnyCable::RackServer.start(options)
|
101
|
-
end
|
102
|
-
|
103
|
-
def call(env)
|
104
|
-
AnyCable::RackServer.middleware.call(env)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|