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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 416a34b5853c2b0f81f4693e59fe4c9b10021b41fc8eb2e710b8fd82b5a67c04
4
- data.tar.gz: fcb0f8b569adc737b704025393237b573b7de616ea4e5deedaa77e442a51bc09
3
+ metadata.gz: 9e57a096cf575590410db4c25e824d0b754af2b85eb0927e4d9fdf553da4ccea
4
+ data.tar.gz: e6c5fce5b74f12bc41ce06955812cff608f50c4c66b063a459fa34ca8ed5e8e1
5
5
  SHA512:
6
- metadata.gz: 7cd8bcd7b5afda208b3df2e476b0729faf9fbfbfd9d6fb36735f26cba48b9589e3a4704a9dbc69c6743bdf4f209c2ee9e6d44f8521a5fc89036a42859f0f9090
7
- data.tar.gz: 68f4b98948bb3ec8fb330947faed0f933ee7dd12dab2f1ca6abbb35af41ab3f7b7f3ad720ea74be82af486f309b250c340603e8e10716478a03975704c3ba3f5
6
+ metadata.gz: 304f417fc3d477625effcc7134c81708d222df3da565613f4f5878b5c0365fecd5ed2a3e9620cc692976051f9a634a66623f8adfce2729664f34882135eeec2b
7
+ data.tar.gz: d5a926617db60ec606f15b73189da24ed80d69012e87b8b4968ea6e0c1e0b7f26b86a3af593a48804f575dfd2c5c7867070555df968c69ba43926d4c97df4501
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018 Yulia Oletskaya
3
+ Copyright (c) 2019 Yulia Oletskaya
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,37 +1,86 @@
1
+ [![Gem Version](https://badge.fury.io/rb/anycable-rack-server.svg)](https://rubygems.org/gems/anycable-rack-server) [![Build Status](https://travis-ci.org/anycable/anycable-rack-server.svg?branch=master)](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 middleware designed for development and testing purposes.
5
+ [AnyCable](https://anycable.io)-compatible Rack hijack based Ruby Web Socket server designed for development and testing purposes.
4
6
 
5
- ## Usage
7
+ ## Using with Rack
6
8
 
7
- Mount the rack middleware
8
9
  ```ruby
9
- # config/routes.rb
10
- Rails.application.routes.draw do
11
- mount AnyCable::Rack.new => '/cable'
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
- Customizable options: headers being sent with each gRPC request. The gem uses AnyCable config for redis and gRPC host settings.
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 env variable
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
- ANYCABLE_HEADERS=cookie,x-api-token,origin
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
- Or
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
- options = { headers: ['cookie', 'origin'] }
30
- AnyCable::Rack.new(nil, options)
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
- Instructions for testing with [anyt](https://github.com/anycable/anyt) (anycable conformance testing) can be found [here](https://github.com/tuwukee/anycable-rack-server/tree/master/test/support).
86
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'anycable/rack-server'
3
+ require "anycable/rack/server"
4
+ require "anycable/rack/railtie" if defined?(Rails)
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'redis'
3
+ require "redis"
4
+ require "json"
4
5
 
5
6
  module AnyCable
6
- module RackServer
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| handle_message(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].terminate unless @threads[channel].nil?
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['stream'], data['data'], coder)
37
+ hub.broadcast(data["stream"], data["data"], coder)
36
38
  end
37
39
  end
38
40
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module AnyCable
4
- module RackServer
6
+ module Rack
5
7
  module Coders
6
- module JSON
8
+ module JSON # :nodoc:
7
9
  class << self
8
10
  def decode(json_str)
9
11
  ::JSON.parse(json_str)
@@ -1,33 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'anycable/rack-server/rpc/client'
4
- require 'anycable/rack-server/logging'
5
- require 'anycable/rack-server/errors'
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
- # rubocop:disable Metrics/LineLength
9
- module RackServer
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
- :header_names,
17
+ :headers,
16
18
  :hub,
17
19
  :socket,
18
20
  :rpc_client,
19
- :server_id
21
+ :sid
20
22
 
21
- def initialize(socket, hub, coder, host, header_names, server_id)
22
- @socket = socket
23
- @coder = coder
24
- @hub = hub
25
- @header_names = header_names
26
- @server_id = server_id
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(host)
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('command')
49
+ command = decoded.delete("command")
50
+
51
+ channel_identifier = decoded["identifier"]
48
52
 
49
- channel_identifier = decoded['identifier']
53
+ log(:debug) { "Command: #{decoded}" }
50
54
 
51
55
  case command
52
- when 'subscribe' then subscribe(channel_identifier)
53
- when 'unsubscribe' then unsubscribe(channel_identifier)
54
- when 'message' then send_message(channel_identifier, decoded['data'])
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
- raise Errors::UnknownCommand, "Command not found #{command}"
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('subscribe', identifier)
107
+ response = rpc_command("subscribe", identifier)
102
108
  if response.status == :SUCCESS
103
109
  @_subscriptions.add(identifier)
104
- else
105
- log(:debug, log_fmt("RPC subscribe command failed: #{response.inspect}"))
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('unsubscribe', identifier)
117
+ response = rpc_command("unsubscribe", identifier)
112
118
  if response.status == :SUCCESS
113
119
  @_subscriptions.delete(identifier)
114
- else
115
- log(:debug, log_fmt("RPC unsubscribe command failed: #{response.inspect}"))
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('message', identifier, data)
122
- unless response.status == :SUCCESS
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) { log_fmt('Opened') }
143
+ log(:debug) { "Opened" }
150
144
  else
151
- log(:error, log_fmt("RPC connection command failed: #{response.inspect}"))
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) { log_fmt('Closed') }
152
+ log(:debug) { "Closed" }
159
153
  else
160
- log(:error, log_fmt("RPC disconnection command failed: #{response.inspect}"))
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
- "[connection:#{server_id}] #{msg}"
183
+ "[sid=#{sid}] #{msg}"
186
184
  end
187
185
  end
188
186
  end
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnyCable
4
- module RackServer
4
+ module Rack
5
5
  module Errors
6
6
  class HijackNotAvailable < RuntimeError; end
7
- class UnknownCommand < StandardError; end
8
7
  class MiddlewareSetup < StandardError; end
9
8
  end
10
9
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module AnyCable
4
- module RackServer
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,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnyCable
4
- module RackServer
4
+ module Rack
5
5
  module Logging # :nodoc:
6
- PREFIX = 'AnycableRackServer'
6
+ PREFIX = "AnyCableRackServer"
7
7
 
8
8
  private
9
9
 
@@ -1,27 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'anycable/rack-server/connection'
4
- require 'anycable/rack-server/errors'
5
- require 'anycable/rack-server/socket'
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 RackServer
9
- class Middleware
10
- PROTOCOLS = ['actioncable-v1-json', 'actioncable-unsupported'].freeze
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
- :headers,
16
- :server_id
17
+ :header_names
17
18
 
18
- def initialize(_app, pinger:, hub:, coder:, rpc_host:, headers:, server_id:)
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
- @headers = headers
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['rack.hijack']
43
+ raise Errors::HijackNotAvailable unless env["rack.hijack"]
44
44
 
45
- env['rack.hijack'].call
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['rack.hijack_io'].write(handshake.to_s)
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['rack.hijack_io'], handshake.version)
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, { 'Content-Type' => 'text/plain' }, ['Not Found']]
62
+ [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
63
63
  end
64
64
 
65
65
  def websocket?(env)
66
- env['HTTP_UPGRADE'] == 'websocket'
66
+ env["HTTP_UPGRADE"] == "websocket"
67
67
  end
68
68
 
69
69
  def init_connection(socket)
70
- connection = Connection.new(socket, hub, coder, rpc_host, headers, server_id)
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 RackServer
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
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grpc'
3
+ require "grpc"
4
4
 
5
5
  module AnyCable
6
- module RackServer
6
+ module Rack
7
7
  module RPC
8
+ # AnyCable RPC client
8
9
  class Client
9
10
  attr_reader :stub
10
11
 
@@ -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 'anycable/rack-server/logging'
3
+ require "anycable/rack/logging"
4
4
 
5
5
  module AnyCable
6
- module RackServer
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
- @_request ||= ::Rack::Request.new(@env)
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.abort_on_exception = true
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 # rubocop:disable Lint/HandleExceptions
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, _addrinfo = socket.readpartial(2000), socket.peeraddr
132
+ data = socket.readpartial(2000)
133
+ _addrinfo = socket.peeraddr
138
134
  end
139
135
 
140
136
  break if data.empty?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnyCable
4
- module RackServer
5
- VERSION = '0.0.1'
4
+ module Rack
5
+ VERSION = "0.1.0"
6
6
  end
7
7
  end
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.1
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-06 00:00:00.000000000 Z
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: '0.8'
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: '0.8'
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.11'
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.11'
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-server.rb
107
- - lib/anycable/rack-server/broadcast_subscribers/redis_subscriber.rb
108
- - lib/anycable/rack-server/coders/json.rb
109
- - lib/anycable/rack-server/connection.rb
110
- - lib/anycable/rack-server/errors.rb
111
- - lib/anycable/rack-server/hub.rb
112
- - lib/anycable/rack-server/logging.rb
113
- - lib/anycable/rack-server/middleware.rb
114
- - lib/anycable/rack-server/pinger.rb
115
- - lib/anycable/rack-server/rpc/client.rb
116
- - lib/anycable/rack-server/rpc/rpc.proto
117
- - lib/anycable/rack-server/socket.rb
118
- - lib/anycable/rack-server/version.rb
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
@@ -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