tipi 0.42 → 0.47

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +1 -3
  4. data/CHANGELOG.md +27 -0
  5. data/Gemfile +3 -1
  6. data/Gemfile.lock +35 -29
  7. data/README.md +184 -8
  8. data/Rakefile +1 -7
  9. data/benchmarks/bm_http1_parser.rb +45 -21
  10. data/bin/benchmark +0 -0
  11. data/bin/h1pd +0 -0
  12. data/bm.png +0 -0
  13. data/df/agent.rb +1 -1
  14. data/df/sample_agent.rb +2 -2
  15. data/df/server.rb +2 -0
  16. data/df/server_utils.rb +12 -15
  17. data/examples/hello.rb +5 -0
  18. data/examples/hello.ru +3 -3
  19. data/examples/http_server.js +1 -1
  20. data/examples/http_server_graceful.rb +1 -1
  21. data/examples/https_server.rb +41 -18
  22. data/examples/rack_server_forked.rb +26 -0
  23. data/examples/rack_server_https_forked.rb +1 -1
  24. data/examples/websocket_demo.rb +1 -1
  25. data/lib/tipi/acme.rb +51 -39
  26. data/lib/tipi/cli.rb +79 -16
  27. data/lib/tipi/config_dsl.rb +13 -13
  28. data/lib/tipi/configuration.rb +2 -2
  29. data/lib/tipi/controller/bare_polyphony.rb +0 -0
  30. data/lib/tipi/controller/bare_stock.rb +10 -0
  31. data/lib/tipi/controller/extensions.rb +37 -0
  32. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  33. data/lib/tipi/controller/web_polyphony.rb +353 -0
  34. data/lib/tipi/controller/web_stock.rb +635 -0
  35. data/lib/tipi/controller.rb +12 -0
  36. data/lib/tipi/digital_fabric/agent.rb +3 -3
  37. data/lib/tipi/digital_fabric/agent_proxy.rb +11 -5
  38. data/lib/tipi/digital_fabric/executive.rb +1 -1
  39. data/lib/tipi/digital_fabric/protocol.rb +1 -1
  40. data/lib/tipi/digital_fabric/service.rb +12 -8
  41. data/lib/tipi/handler.rb +2 -2
  42. data/lib/tipi/http1_adapter.rb +36 -30
  43. data/lib/tipi/http2_adapter.rb +10 -10
  44. data/lib/tipi/http2_stream.rb +14 -15
  45. data/lib/tipi/rack_adapter.rb +2 -2
  46. data/lib/tipi/response_extensions.rb +1 -1
  47. data/lib/tipi/supervisor.rb +75 -0
  48. data/lib/tipi/version.rb +1 -1
  49. data/lib/tipi/websocket.rb +3 -3
  50. data/lib/tipi.rb +4 -83
  51. data/test/coverage.rb +2 -2
  52. data/test/helper.rb +0 -1
  53. data/test/test_http_server.rb +14 -14
  54. data/test/test_request.rb +1 -1
  55. data/tipi.gemspec +6 -7
  56. metadata +58 -53
  57. data/ext/tipi/extconf.rb +0 -13
  58. data/ext/tipi/http1_parser.c +0 -823
  59. data/ext/tipi/http1_parser.h +0 -18
  60. data/ext/tipi/tipi_ext.c +0 -5
  61. data/security/http1.rb +0 -12
  62. data/test/test_http1_parser.rb +0 -586
@@ -83,7 +83,7 @@ module DigitalFabric
83
83
  raise 'Invalid output from top (cpu)'
84
84
  end
85
85
  cpu_utilization = 100 - Regexp.last_match(1).to_i
86
-
86
+
87
87
  unless top =~ TOP_MEM_REGEXP && Regexp.last_match(1) =~ TOP_MEM_FREE_REGEXP
88
88
  raise 'Invalid output from top (mem)'
89
89
  end
@@ -141,7 +141,7 @@ module DigitalFabric
141
141
  def ws_data(id, data)
142
142
  [ WS_DATA, id, data ]
143
143
  end
144
-
144
+
145
145
  def ws_close(id)
146
146
  [ WS_CLOSE, id ]
147
147
  end
@@ -50,6 +50,8 @@ module DigitalFabric
50
50
  switch_rate = backend_stats[:switch_count] / elapsed
51
51
  poll_rate = backend_stats[:poll_count] / elapsed
52
52
 
53
+ object_space_stats = ObjectSpace.count_objects
54
+
53
55
  {
54
56
  service: {
55
57
  agent_count: @agents.size,
@@ -73,6 +75,8 @@ module DigitalFabric
73
75
  process: {
74
76
  cpu_usage: cpu,
75
77
  rss: rss.to_f / 1024,
78
+ objects_total: object_space_stats[:TOTAL],
79
+ objects_free: object_space_stats[:FREE]
76
80
  }
77
81
  }
78
82
  end
@@ -86,7 +90,7 @@ module DigitalFabric
86
90
  rescue Exception
87
91
  [nil, nil]
88
92
  end
89
-
93
+
90
94
  def get_stats
91
95
  calculate_stats
92
96
  end
@@ -119,14 +123,14 @@ module DigitalFabric
119
123
 
120
124
  puts format('slow request (%.1f): %p', latency, req.headers)
121
125
  end
122
-
126
+
123
127
  def http_request(req, allow_df_upgrade = false)
124
128
  @current_request_count += 1
125
129
  @counters[:http_requests] += 1
126
130
  @counters[:connections] += 1 if req.headers[':first']
127
131
 
128
132
  return upgrade_request(req, allow_df_upgrade) if req.upgrade_protocol
129
-
133
+
130
134
  inject_request_headers(req)
131
135
  agent = find_agent(req)
132
136
  unless agent
@@ -155,7 +159,7 @@ module DigitalFabric
155
159
  req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
156
160
  req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
157
161
  end
158
-
162
+
159
163
  def upgrade_request(req, allow_df_upgrade)
160
164
  case (protocol = req.upgrade_protocol)
161
165
  when 'df'
@@ -174,7 +178,7 @@ module DigitalFabric
174
178
  agent.http_upgrade(req, protocol)
175
179
  end
176
180
  end
177
-
181
+
178
182
  def df_upgrade(req)
179
183
  # we don't want to count connected agents
180
184
  @current_request_count -= 1
@@ -187,7 +191,7 @@ module DigitalFabric
187
191
  ensure
188
192
  @current_request_count += 1
189
193
  end
190
-
194
+
191
195
  def mount(route, agent)
192
196
  if route[:path]
193
197
  route[:path_regexp] = path_regexp(route[:path])
@@ -196,7 +200,7 @@ module DigitalFabric
196
200
  @agents[agent] = route
197
201
  @routing_changed = true
198
202
  end
199
-
203
+
200
204
  def unmount(agent)
201
205
  route = @agents[agent]
202
206
  return unless route
@@ -207,7 +211,7 @@ module DigitalFabric
207
211
  end
208
212
 
209
213
  INVALID_HOST = 'INVALID_HOST'
210
-
214
+
211
215
  def find_agent(req)
212
216
  compile_agent_routes if @routing_changed
213
217
 
data/lib/tipi/handler.rb CHANGED
@@ -20,14 +20,14 @@ module Tipi
20
20
  ensure
21
21
  socket.close
22
22
  end
23
-
23
+
24
24
  ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
25
25
  H2_PROTOCOL = 'h2'
26
26
 
27
27
  def protocol_adapter(socket, opts)
28
28
  use_http2 = socket.respond_to?(:alpn_protocol) &&
29
29
  socket.alpn_protocol == H2_PROTOCOL
30
-
30
+
31
31
  klass = use_http2 ? HTTP2Adapter : HTTP1Adapter
32
32
  klass.new(socket, opts)
33
33
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'tipi_ext'
4
- require_relative './http2_adapter'
3
+ require 'h1p'
5
4
  require 'qeweney/request'
6
5
 
6
+ require_relative './http2_adapter'
7
+
7
8
  module Tipi
8
9
  # HTTP1 protocol implementation
9
10
  class HTTP1Adapter
@@ -14,26 +15,29 @@ module Tipi
14
15
  @conn = conn
15
16
  @opts = opts
16
17
  @first = true
17
- @parser = Tipi::HTTP1Parser.new(@conn)
18
+ @parser = H1P::Parser.new(@conn, :server)
18
19
  end
19
-
20
+
20
21
  def each(&block)
21
22
  while true
22
23
  headers = @parser.parse_headers
23
24
  break unless headers
24
-
25
+
25
26
  # handle_request returns true if connection is not persistent or was
26
27
  # upgraded
27
28
  break if handle_request(headers, &block)
28
29
  end
29
- rescue Tipi::HTTP1Parser::Error
30
+ rescue H1P::Error, ArgumentError
31
+ # an ArgumentError might be raised in the parser if an invalid input
32
+ # string is given as the HTTP method (String#upcase will raise on invalid HTTP string)
33
+ #
30
34
  # ignore
31
35
  rescue SystemCallError, IOError
32
36
  # ignore
33
37
  ensure
34
38
  finalize_client_loop
35
39
  end
36
-
40
+
37
41
  def handle_request(headers, &block)
38
42
  scheme = (proto = headers['x-forwarded-proto']) ?
39
43
  proto.downcase : scheme_from_connection
@@ -43,9 +47,9 @@ module Tipi
43
47
  headers[':first'] = true
44
48
  @first = nil
45
49
  end
46
-
50
+
47
51
  return true if upgrade_connection(headers, &block)
48
-
52
+
49
53
  request = Qeweney::Request.new(headers, self)
50
54
  if !@parser.complete?
51
55
  request.buffer_body_chunk(@parser.read_body_chunk(true))
@@ -62,14 +66,14 @@ module Tipi
62
66
  return connection && connection != 'close'
63
67
  end
64
68
  end
65
-
69
+
66
70
  def finalize_client_loop
67
71
  @parser = nil
68
72
  @splicing_pipe = nil
69
73
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
70
74
  @conn.close
71
75
  end
72
-
76
+
73
77
  # Reads a body chunk for the current request. Transfers control to the parse
74
78
  # loop, and resumes once the parse_loop has fired the on_body callback
75
79
  def get_body_chunk(request, buffered_only = false)
@@ -83,11 +87,11 @@ module Tipi
83
87
  def complete?(request)
84
88
  @parser.complete?
85
89
  end
86
-
90
+
87
91
  def protocol
88
92
  @protocol
89
93
  end
90
-
94
+
91
95
  # Upgrades the connection to a different protocol, if the 'Upgrade' header is
92
96
  # given. By default the only supported upgrade protocol is HTTP2. Additional
93
97
  # protocols, notably WebSocket, can be specified by passing a hash to the
@@ -113,28 +117,28 @@ module Tipi
113
117
  def upgrade_connection(headers, &block)
114
118
  upgrade_protocol = headers['upgrade']
115
119
  return nil unless upgrade_protocol
116
-
120
+
117
121
  upgrade_protocol = upgrade_protocol.downcase.to_sym
118
122
  upgrade_handler = @opts[:upgrade] && @opts[:upgrade][upgrade_protocol]
119
123
  return upgrade_with_handler(upgrade_handler, headers) if upgrade_handler
120
124
  return upgrade_to_http2(headers, &block) if upgrade_protocol == :h2c
121
-
125
+
122
126
  nil
123
127
  end
124
-
128
+
125
129
  def upgrade_with_handler(handler, headers)
126
130
  @parser = nil
127
131
  handler.(self, headers)
128
132
  true
129
133
  end
130
-
134
+
131
135
  def upgrade_to_http2(headers, &block)
132
136
  headers = http2_upgraded_headers(headers)
133
137
  body = @parser.read_body
134
138
  HTTP2Adapter.upgrade_each(@conn, @opts, headers, body, &block)
135
139
  true
136
140
  end
137
-
141
+
138
142
  # Returns headers for HTTP2 upgrade
139
143
  # @param headers [Hash] request headers
140
144
  # @return [Hash] headers for HTTP2 upgrade
@@ -152,10 +156,10 @@ module Tipi
152
156
  def scheme_from_connection
153
157
  @conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
154
158
  end
155
-
159
+
156
160
  # response API
157
161
 
158
- CRLF = "\r\n"
162
+ CRLF = "\r\n"
159
163
  CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
160
164
 
161
165
  # Sends response including headers and body. Waits for the request to complete
@@ -173,17 +177,19 @@ module Tipi
173
177
  end
174
178
  end
175
179
 
180
+ CHUNK_LENGTH_PROC = ->(len) { "#{len.to_s(16)}\r\n" }
181
+
176
182
  def respond_from_io(request, io, headers, chunk_size = 2**14)
177
183
  formatted_headers = format_headers(headers, true, true)
178
184
  request.tx_incr(formatted_headers.bytesize)
179
-
185
+
180
186
  # assume chunked encoding
181
187
  Thread.current.backend.splice_chunks(
182
188
  io,
183
189
  @conn,
184
190
  formatted_headers,
185
191
  "0\r\n\r\n",
186
- ->(len) { "#{len.to_s(16)}\r\n" },
192
+ CHUNK_LENGTH_PROC,
187
193
  "\r\n",
188
194
  chunk_size
189
195
  )
@@ -205,7 +211,7 @@ module Tipi
205
211
  def http1_1?(request)
206
212
  request.headers[':protocol'] == 'http/1.1'
207
213
  end
208
-
214
+
209
215
  # Sends a response body chunk. If no headers were sent, default headers are
210
216
  # sent using #send_headers. if the done option is true(thy), an empty chunk
211
217
  # will be sent to signal response completion to the client.
@@ -222,7 +228,7 @@ module Tipi
222
228
  request.tx_incr(data.bytesize)
223
229
  @conn.write(data)
224
230
  end
225
-
231
+
226
232
  def send_chunk_from_io(request, io, r, w, chunk_size)
227
233
  len = w.splice(io, chunk_size)
228
234
  if len > 0
@@ -244,12 +250,12 @@ module Tipi
244
250
  request.tx_incr(5)
245
251
  @conn << "0\r\n\r\n"
246
252
  end
247
-
253
+
248
254
  def close
249
255
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
250
256
  @conn.close
251
257
  end
252
-
258
+
253
259
  private
254
260
 
255
261
  INTERNAL_HEADER_REGEXP = /^:/.freeze
@@ -266,13 +272,13 @@ module Tipi
266
272
  lines = format_status_line(body, status, chunked)
267
273
  headers.each do |k, v|
268
274
  next if k =~ INTERNAL_HEADER_REGEXP
269
-
275
+
270
276
  collect_header_lines(lines, k, v)
271
277
  end
272
278
  lines << CRLF
273
279
  lines
274
280
  end
275
-
281
+
276
282
  def format_status_line(body, status, chunked)
277
283
  if !body
278
284
  empty_status_line(status)
@@ -280,7 +286,7 @@ module Tipi
280
286
  with_body_status_line(status, body, chunked)
281
287
  end
282
288
  end
283
-
289
+
284
290
  def empty_status_line(status)
285
291
  if status == 204
286
292
  +"HTTP/1.1 #{status}\r\n"
@@ -288,7 +294,7 @@ module Tipi
288
294
  +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
289
295
  end
290
296
  end
291
-
297
+
292
298
  def with_body_status_line(status, body, chunked)
293
299
  if chunked
294
300
  +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
@@ -21,7 +21,7 @@ module Tipi
21
21
  adapter = new(socket, opts, headers, body)
22
22
  adapter.each(&block)
23
23
  end
24
-
24
+
25
25
  def initialize(conn, opts, upgrade_headers = nil, upgrade_body = nil)
26
26
  @conn = conn
27
27
  @opts = opts
@@ -36,7 +36,7 @@ module Tipi
36
36
  @interface.on(:frame, &method(:send_frame))
37
37
  @streams = {}
38
38
  end
39
-
39
+
40
40
  def send_frame(data)
41
41
  if @transfer_count_request
42
42
  @transfer_count_request.tx_incr(data.bytesize)
@@ -47,14 +47,14 @@ module Tipi
47
47
  rescue Exception => e
48
48
  @connection_fiber.transfer e
49
49
  end
50
-
50
+
51
51
  UPGRADE_MESSAGE = <<~HTTP.gsub("\n", "\r\n")
52
52
  HTTP/1.1 101 Switching Protocols
53
53
  Connection: Upgrade
54
54
  Upgrade: h2c
55
-
55
+
56
56
  HTTP
57
-
57
+
58
58
  def upgrade
59
59
  @conn << UPGRADE_MESSAGE
60
60
  @tx += UPGRADE_MESSAGE.bytesize
@@ -63,7 +63,7 @@ module Tipi
63
63
  ensure
64
64
  @upgrade_headers = nil
65
65
  end
66
-
66
+
67
67
  # Iterates over incoming requests
68
68
  def each(&block)
69
69
  @interface.on(:stream) { |stream| start_stream(stream, &block) }
@@ -84,26 +84,26 @@ module Tipi
84
84
  @rx = 0
85
85
  count
86
86
  end
87
-
87
+
88
88
  def get_tx_count
89
89
  count = @tx
90
90
  @tx = 0
91
91
  count
92
92
  end
93
-
93
+
94
94
  def start_stream(stream, &block)
95
95
  stream = HTTP2StreamHandler.new(self, stream, @conn, @first, &block)
96
96
  @first = nil if @first
97
97
  @streams[stream] = true
98
98
  end
99
-
99
+
100
100
  def finalize_client_loop
101
101
  @interface = nil
102
102
  @streams.each_key(&:stop)
103
103
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
104
104
  @conn.close
105
105
  end
106
-
106
+
107
107
  def close
108
108
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
109
109
  @conn.close
@@ -6,9 +6,8 @@ require 'qeweney/request'
6
6
  module Tipi
7
7
  # Manages an HTTP 2 stream
8
8
  class HTTP2StreamHandler
9
- attr_accessor :__next__
10
9
  attr_reader :conn
11
-
10
+
12
11
  def initialize(adapter, stream, conn, first, &block)
13
12
  @adapter = adapter
14
13
  @stream = stream
@@ -32,7 +31,7 @@ module Tipi
32
31
  stream.on(:data, &method(:on_data))
33
32
  stream.on(:half_close, &method(:on_half_close))
34
33
  end
35
-
34
+
36
35
  def run(&block)
37
36
  request = receive
38
37
  error = nil
@@ -45,7 +44,7 @@ module Tipi
45
44
  ensure
46
45
  @connection_fiber.schedule error
47
46
  end
48
-
47
+
49
48
  def on_headers(headers)
50
49
  @request = Qeweney::Request.new(headers.to_h, self)
51
50
  @request.rx_incr(@adapter.get_rx_count)
@@ -59,7 +58,7 @@ module Tipi
59
58
 
60
59
  def on_data(data)
61
60
  data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
62
-
61
+
63
62
  (@buffered_chunks ||= []) << data
64
63
  @get_body_chunk_fiber&.schedule
65
64
  end
@@ -68,7 +67,7 @@ module Tipi
68
67
  @get_body_chunk_fiber&.schedule
69
68
  @complete = true
70
69
  end
71
-
70
+
72
71
  def protocol
73
72
  'h2'
74
73
  end
@@ -84,7 +83,7 @@ module Tipi
84
83
  @buffered_chunks ||= []
85
84
  return @buffered_chunks.shift unless @buffered_chunks.empty?
86
85
  return nil if @complete
87
-
86
+
88
87
  begin
89
88
  @get_body_chunk_fiber = Fiber.current
90
89
  suspend
@@ -112,7 +111,7 @@ module Tipi
112
111
  def complete?(request)
113
112
  @complete
114
113
  end
115
-
114
+
116
115
  # response API
117
116
  def respond(request, chunk, headers)
118
117
  headers[':status'] ||= Qeweney::Status::OK
@@ -149,10 +148,10 @@ module Tipi
149
148
  end
150
149
  end
151
150
  end
152
-
151
+
153
152
  def send_headers(request, headers, empty_response: false)
154
153
  return if @headers_sent
155
-
154
+
156
155
  headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
157
156
  with_transfer_count(request) do
158
157
  @stream.headers(transform_headers(headers), end_stream: false)
@@ -161,10 +160,10 @@ module Tipi
161
160
  rescue HTTP2::Error::StreamClosed
162
161
  # ignore
163
162
  end
164
-
163
+
165
164
  def send_chunk(request, chunk, done: false)
166
165
  send_headers({}, false) unless @headers_sent
167
-
166
+
168
167
  if chunk
169
168
  with_transfer_count(request) do
170
169
  @stream.data(chunk, end_stream: done)
@@ -175,7 +174,7 @@ module Tipi
175
174
  rescue HTTP2::Error::StreamClosed
176
175
  # ignore
177
176
  end
178
-
177
+
179
178
  def finish(request)
180
179
  if @headers_sent
181
180
  @stream.close
@@ -188,10 +187,10 @@ module Tipi
188
187
  rescue HTTP2::Error::StreamClosed
189
188
  # ignore
190
189
  end
191
-
190
+
192
191
  def stop
193
192
  return if @complete
194
-
193
+
195
194
  @stream.close
196
195
  @stream_fiber.schedule(Polyphony::MoveOn.new)
197
196
  end
@@ -3,12 +3,12 @@
3
3
  require 'rack'
4
4
 
5
5
  module Tipi
6
- module RackAdapter
6
+ module RackAdapter
7
7
  class << self
8
8
  def run(app)
9
9
  ->(req) { respond(req, app.(env(req))) }
10
10
  end
11
-
11
+
12
12
  def load(path)
13
13
  src = IO.read(path)
14
14
  instance_eval(src, path, 1)
@@ -9,7 +9,7 @@ module Tipi
9
9
  def serve_io(io, opts)
10
10
  if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD
11
11
  @adapter.respond_from_io(self, io, opts[:headers], opts[:chunk_size] || 2**14)
12
- else
12
+ else
13
13
  respond(io.read, opts[:headers] || {})
14
14
  end
15
15
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'polyphony'
4
+ require 'json'
5
+
6
+ module Tipi
7
+ module Supervisor
8
+ class << self
9
+ def run(opts)
10
+ puts "Start supervisor pid: #{Process.pid}"
11
+ @opts = opts
12
+ @controller_watcher = start_controller_watcher
13
+ supervise_loop
14
+ end
15
+
16
+ def start_controller_watcher
17
+ spin do
18
+ cmd = controller_cmd
19
+ puts "Starting controller..."
20
+ pid = Kernel.spawn(*cmd)
21
+ @controller_pid = pid
22
+ puts "Controller pid: #{pid}"
23
+ _pid, status = Polyphony.backend_waitpid(pid)
24
+ puts "Controller has terminated with status: #{status.inspect}"
25
+ terminated = true
26
+ ensure
27
+ if pid && !terminated
28
+ puts "Terminate controller #{pid.inspect}"
29
+ Polyphony::Process.kill_process(pid)
30
+ end
31
+ Fiber.current.parent << pid
32
+ end
33
+ end
34
+
35
+ def controller_cmd
36
+ [
37
+ 'ruby',
38
+ File.join(__dir__, 'controller.rb'),
39
+ @opts.to_json
40
+ ]
41
+ end
42
+
43
+ def supervise_loop
44
+ this_fiber = Fiber.current
45
+ trap('SIGUSR2') { this_fiber << :replace_controller }
46
+ loop do
47
+ case (msg = receive)
48
+ when :replace_controller
49
+ replace_controller
50
+ when Integer
51
+ pid = msg
52
+ if pid == @controller_pid
53
+ puts 'Detected dead controller. Restarting...'
54
+ exit!
55
+ @controller_watcher.restart
56
+ end
57
+ else
58
+ raise "Invalid message received: #{msg.inspect}"
59
+ end
60
+ end
61
+ end
62
+
63
+ def replace_controller
64
+ puts "Replacing controller"
65
+ old_watcher = @controller_watcher
66
+ @controller_watcher = start_controller_watcher
67
+
68
+ # TODO: we'll want to get some kind of signal from the new controller once it's ready
69
+ sleep 1
70
+
71
+ old_watcher.terminate(true)
72
+ end
73
+ end
74
+ end
75
+ end
data/lib/tipi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tipi
4
- VERSION = '0.42'
4
+ VERSION = '0.47'
5
5
  end
@@ -20,12 +20,12 @@ module Tipi
20
20
  @version = headers['sec-websocket-version'].to_i
21
21
  @reader = ::WebSocket::Frame::Incoming::Server.new(version: @version)
22
22
  end
23
-
23
+
24
24
  def recv
25
25
  if (msg = @reader.next)
26
26
  return msg.to_s
27
27
  end
28
-
28
+
29
29
  @conn.recv_loop do |data|
30
30
  @reader << data
31
31
  if (msg = @reader.next)
@@ -48,7 +48,7 @@ module Tipi
48
48
  end
49
49
  end
50
50
  end
51
-
51
+
52
52
  OutgoingFrame = ::WebSocket::Frame::Outgoing::Server
53
53
 
54
54
  def send(data)