anycable-rack-server 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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