tipi 0.42 → 0.47

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.
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)