backport 0.2.0 → 0.3.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: 2aedfc84d845aedeb6d7146cb21a74c4d1d74d90844407982ce454e8aa2d16b3
4
- data.tar.gz: e222830378478e7990601880b42e60dd4e34ba1791e63345a2a5fa0a2898bbf8
3
+ metadata.gz: 9693d9fb93dc1efb03c85d3abe258cd54e2fa4bca03e8bc2f64a45a56783269a
4
+ data.tar.gz: a7abdaa602ccbfda748cdace2af071af73651ecff42663ac237def501260f457
5
5
  SHA512:
6
- metadata.gz: 5cd8632b158f0e5dda4e5377b5fc74a9e7ae416a738941dcf276cd48d8040c671eb9e337a089215e092ec499fecbadfaf6c015f49f09b72bc4cee556780afa4f
7
- data.tar.gz: e52ea4e36b3bfbd397dabc9410d07af6106d1c7bc1e9dbac98d18a2def432300e64696ee3e277ad1e19639d61027ccb46d3a3f665603031ffab26c81d4f7ba83
6
+ metadata.gz: bbc92f9838a571f7cb9ea71334961cf34013a41e1010147ec4f54d4c5efde68c37b4f7cb9f0d9d52ad8ebe23ac9fa83e24568d690044ed200d838a1cb2c50358
7
+ data.tar.gz: a5cdf05eb09d40bc352fd07ed2d23b85107875551c9718e0145e7abdf92f670ff784e99b50b8daf5879c7a0cbe92dca1d8ca4b3a42ed73a482610085fd56e986
data/.gitignore CHANGED
@@ -6,6 +6,9 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ /.vscode/
9
10
 
10
11
  # rspec failure tracking
11
12
  .rspec_status
13
+
14
+ Gemfile.lock
@@ -0,0 +1,4 @@
1
+ Style/MethodDefParentheses:
2
+ Enabled: false
3
+ Layout/EmptyLineAfterGuardClause:
4
+ Enabled: false
@@ -1,3 +1,9 @@
1
+ ## 0.3.0 - January 10, 2019
2
+ - Basic logging
3
+ - Differentiate between "expected" and "unexpected" exceptions in Tcpip
4
+ - Prefer #start to #run for non-blocking client/server methods
5
+ - Interval servers can stop themselves
6
+
1
7
  ## 0.2.0 - December 21, 2018
2
8
  - Minor bug fixes in STDIO server
3
9
  - More efficient client reads
data/README.md CHANGED
@@ -20,6 +20,8 @@ gem 'backport'
20
20
 
21
21
  ## Usage
22
22
 
23
+ ### Examples
24
+
23
25
  A simple echo server:
24
26
 
25
27
  ```ruby
@@ -27,11 +29,11 @@ require 'backport'
27
29
 
28
30
  module MyAdapter
29
31
  def opening
30
- write "Opening the connection"
32
+ puts "Opening a connection"
31
33
  end
32
34
 
33
35
  def closing
34
- write "Closing the connection"
36
+ puts "Closing a connection"
35
37
  end
36
38
 
37
39
  def sending data
@@ -55,3 +57,24 @@ Backport.run do
55
57
  end
56
58
  end
57
59
  ```
60
+
61
+ ### Using Adapters
62
+
63
+ Backport servers that handle client connections, such as TCP servers, use an
64
+ adapter to provide an application interface to the client. Developers can
65
+ provide their own adapter implementations in two ways: a Ruby module that will
66
+ be used to extend a Backport::Adapter object, or a class that extends
67
+ Backport::Adapter. In either case, the adapter should provide the following
68
+ methods:
69
+
70
+ * `opening`: A callback triggered when the client connection is accepted
71
+ * `closing`: A callback triggered when the client connection is closed
72
+ * `sending(data)`: A callback triggered when the server receives data from the client
73
+
74
+ Backport::Adapter also provides the following methods:
75
+
76
+ * `write(data)`: Send raw data to the client
77
+ * `write_line(data)`: Send a line of data to the client
78
+ * `close`: Disconnect the client from the server
79
+ * `closed?`: True if the connection is closed
80
+ * `remote`: A hash of data about the client, e.g., the remote IP address
@@ -1,66 +1,72 @@
1
- require "backport/version"
2
- require 'socket'
3
-
4
- module Backport
5
- autoload :Adapter, 'backport/adapter'
6
- autoload :Machine, 'backport/machine'
7
- autoload :Server, 'backport/server'
8
- autoload :Client, 'backport/client'
9
-
10
- class << self
11
- # Prepare a STDIO server to run in Backport.
12
- #
13
- # @param adapter [Adapter]
14
- # @return [void]
15
- def prepare_stdio_server adapter: Adapter
16
- machine.prepare Backport::Server::Stdio.new(adapter: adapter)
17
- end
18
-
19
- # Prepare a TCP server to run in Backport.
20
- #
21
- # @param host [String]
22
- # @param port [Integer]
23
- # @param adapter [Adapter]
24
- # @return [void]
25
- def prepare_tcp_server host: 'localhost', port: 1117, adapter: Adapter
26
- machine.prepare Backport::Server::Tcpip.new(host: host, port: port, adapter: adapter)
27
- end
28
-
29
- # Prepare an interval server to run in Backport.
30
- #
31
- # @param period [Float] Seconds between intervals
32
- # @return [void]
33
- def prepare_interval period, &block
34
- machine.prepare Backport::Server::Interval.new(period, &block)
35
- end
36
-
37
- # Run the Backport machine. The provided block will be executed before the
38
- # machine starts. Program execution is halted until the machine stops.
39
- #
40
- # @example Print "tick" once per second
41
- # Backport.run do
42
- # Backport.prepare_interval 1 do
43
- # puts "tick"
44
- # end
45
- # end
46
- #
47
- # @return [void]
48
- def run &block
49
- machine.run &block
50
- end
51
-
52
- # Stop the Backport machine.
53
- #
54
- # @return [void]
55
- def stop
56
- machine.stop
57
- end
58
-
59
- private
60
-
61
- # @return [Machine]
62
- def machine
63
- @machine ||= Machine.new
64
- end
65
- end
66
- end
1
+ require "backport/version"
2
+ require 'logger'
3
+
4
+ # An event-driven IO library.
5
+ #
6
+ module Backport
7
+ autoload :Adapter, 'backport/adapter'
8
+ autoload :Machine, 'backport/machine'
9
+ autoload :Server, 'backport/server'
10
+ autoload :Client, 'backport/client'
11
+
12
+ class << self
13
+ # Prepare a STDIO server to run in Backport.
14
+ #
15
+ # @param adapter [Adapter]
16
+ # @return [void]
17
+ def prepare_stdio_server adapter: Adapter
18
+ machine.prepare Backport::Server::Stdio.new(adapter: adapter)
19
+ end
20
+
21
+ # Prepare a TCP server to run in Backport.
22
+ #
23
+ # @param host [String]
24
+ # @param port [Integer]
25
+ # @param adapter [Adapter]
26
+ # @return [void]
27
+ def prepare_tcp_server host: 'localhost', port: 1117, adapter: Adapter
28
+ machine.prepare Backport::Server::Tcpip.new(host: host, port: port, adapter: adapter)
29
+ end
30
+
31
+ # Prepare an interval server to run in Backport.
32
+ #
33
+ # @param period [Float] Seconds between intervals
34
+ # @return [void]
35
+ def prepare_interval period, &block
36
+ machine.prepare Backport::Server::Interval.new(period, &block)
37
+ end
38
+
39
+ # Run the Backport machine. The provided block will be executed before the
40
+ # machine starts. Program execution is blocked until the machine stops.
41
+ #
42
+ # @example Print "tick" once per second
43
+ # Backport.run do
44
+ # Backport.prepare_interval 1 do
45
+ # puts "tick"
46
+ # end
47
+ # end
48
+ #
49
+ # @return [void]
50
+ def run &block
51
+ machine.run &block
52
+ end
53
+
54
+ # Stop the Backport machine.
55
+ #
56
+ # @return [void]
57
+ def stop
58
+ machine.stop
59
+ end
60
+
61
+ def logger
62
+ @logger ||= Logger.new(STDERR, level: Logger::WARN, progname: 'Backport')
63
+ end
64
+
65
+ private
66
+
67
+ # @return [Machine]
68
+ def machine
69
+ @machine ||= Machine.new
70
+ end
71
+ end
72
+ end
@@ -1,17 +1,29 @@
1
1
  module Backport
2
+ # The application interface between Backport servers and clients.
3
+ #
2
4
  class Adapter
5
+ # @param output [IO]
6
+ # @param remote [Hash{Symbol => String, Integer}]
7
+ def initialize output, remote = {}
8
+ # Store internal data in a singleton method to avoid instance variable
9
+ # collisions in custom adapters
10
+ data = {
11
+ out: output,
12
+ remote: remote,
13
+ closed: false
14
+ }
15
+ define_singleton_method :_data do
16
+ data
17
+ end
18
+ end
19
+
3
20
  # A hash of information about the client connection. The data can vary
4
21
  # based on the transport, e.g., :hostname and :address for TCP connections
5
22
  # or :filename for file streams.
6
23
  #
7
24
  # @return [Hash{Symbol => String, Integer}]
8
- attr_reader :remote
9
-
10
- # @param output [IO]
11
- # @param remote [Hash{Symbol => String, Integer}]
12
- def initialize output, remote = {}
13
- @out = output
14
- @remote = remote
25
+ def remote
26
+ _data[:remote]
15
27
  end
16
28
 
17
29
  # A callback triggered when a client connection is opening. Subclasses
@@ -28,9 +40,9 @@ module Backport
28
40
  # @return [void]
29
41
  def closing; end
30
42
 
31
- # A callback triggered when the client is sending data to the server.
32
- # Subclasses and/or modules should override this method to provide their
33
- # own functionality.
43
+ # A callback triggered when the client sends data to the server. Subclasses
44
+ # and/or modules should override this method to provide their own
45
+ # functionality.
34
46
  #
35
47
  # @param data [String]
36
48
  # @return [void]
@@ -41,8 +53,8 @@ module Backport
41
53
  # @param data [String]
42
54
  # @return [void]
43
55
  def write data
44
- @out.write data
45
- @out.flush
56
+ _data[:out].write data
57
+ _data[:out].flush
46
58
  end
47
59
 
48
60
  # Send a line of data to the client.
@@ -50,17 +62,23 @@ module Backport
50
62
  # @param data [String]
51
63
  # @return [void]
52
64
  def write_line data
53
- @out.puts data
54
- @out.flush
65
+ _data[:out].puts data
66
+ _data[:out].flush
55
67
  end
56
68
 
57
69
  def closed?
58
- @closed ||= false
70
+ _data[:closed] ||= false
59
71
  end
60
72
 
73
+ # Close the client connection.
74
+ #
75
+ # @note The adapter sets #closed? to true and runs the #closing callback.
76
+ # The server is responsible for implementation details like closing the
77
+ # client's socket.
78
+ #
61
79
  def close
62
80
  return if closed?
63
- @closed = true
81
+ _data[:closed] = true
64
82
  closing
65
83
  end
66
84
  end
@@ -1,4 +1,6 @@
1
1
  module Backport
2
+ # A client connected to a connectable Backport server.
3
+ #
2
4
  class Client
3
5
  # @return [Adapter]
4
6
  attr_reader :adapter
@@ -15,23 +17,40 @@ module Backport
15
17
  @stopped ||= false
16
18
  end
17
19
 
20
+ # Close the client connection.
21
+ #
22
+ # @note The client sets #stopped? to true and runs the adapter's #closing
23
+ # callback. The server is responsible for implementation details like
24
+ # closing the client's socket.
25
+ #
18
26
  def stop
19
27
  return if stopped?
20
28
  @adapter.closing
21
29
  @stopped = true
22
30
  end
23
31
 
24
- def run
32
+ # Start running the client. This method will start the thread that reads
33
+ # client input from IO.
34
+ #
35
+ def start
25
36
  return unless stopped?
26
37
  @stopped = false
27
38
  @adapter.opening
28
39
  run_input_thread
29
40
  end
41
+ # @deprecated Prefer #start to #run for non-blocking client/server methods
42
+ alias run start
30
43
 
44
+ # Notify the adapter that the client is sending data.
45
+ #
46
+ # @param data [String]
31
47
  def sending data
32
48
  @adapter.sending data
33
49
  end
34
50
 
51
+ # Read the client input. Return nil if the input buffer is empty.
52
+ #
53
+ # @return [String, nil]
35
54
  def read
36
55
  tmp = nil
37
56
  mutex.synchronize do
@@ -1,11 +1,13 @@
1
1
  module Backport
2
+ # The Backport server controller.
3
+ #
2
4
  class Machine
3
5
  def initialize
4
6
  @stopped = true
5
7
  end
6
8
 
7
9
  # Run the machine. If a block is provided, it gets executed before the
8
- # maching starts its main loop. The main loop halts program execution
10
+ # maching starts its main loop. The main loop blocks program execution
9
11
  # until the machine is stopped.
10
12
  #
11
13
  # @return [void]
@@ -43,17 +45,23 @@ module Backport
43
45
  server.start unless stopped?
44
46
  end
45
47
 
46
- private
47
-
48
48
  # @return [Array<Server::Base>]
49
49
  def servers
50
50
  @servers ||= []
51
51
  end
52
52
 
53
+ def tick
54
+ servers.delete_if(&:stopped?)
55
+ stop if servers.empty?
56
+ servers.each(&:tick)
57
+ end
58
+
59
+ private
60
+
53
61
  def run_server_thread
54
62
  servers.map(&:start)
55
63
  until stopped?
56
- servers.each(&:tick)
64
+ tick
57
65
  sleep 0.001
58
66
  end
59
67
  end
@@ -1,4 +1,6 @@
1
1
  module Backport
2
+ # Classes and modules for Backport servers.
3
+ #
2
4
  module Server
3
5
  autoload :Base, 'backport/server/base'
4
6
  autoload :Connectable, 'backport/server/connectable'
@@ -1,16 +1,33 @@
1
1
  module Backport
2
2
  module Server
3
+ # An extendable server class that provides basic start/stop functionality
4
+ # and common callbacks.
5
+ #
3
6
  class Base
4
- def started?
5
- @started ||= false
6
- @started
7
+ # Start the server.
8
+ #
9
+ def start
10
+ return if started?
11
+ starting
12
+ @started = true
7
13
  end
8
14
 
15
+ # Stop the server.
16
+ #
9
17
  def stop
18
+ return if stopped?
10
19
  stopping
11
20
  @started = false
12
21
  end
13
22
 
23
+ def started?
24
+ @started ||= false
25
+ end
26
+
27
+ def stopped?
28
+ !started?
29
+ end
30
+
14
31
  # A callback triggered when a Machine starts running or the server is
15
32
  # added to a running machine. Subclasses should override this method to
16
33
  # provide their own functionality.
@@ -30,11 +47,6 @@ module Backport
30
47
  #
31
48
  # @return [void]
32
49
  def tick; end
33
-
34
- def start
35
- starting
36
- @started = true
37
- end
38
50
  end
39
51
  end
40
52
  end
@@ -1,5 +1,9 @@
1
1
  module Backport
2
2
  module Server
3
+ # A mixin for Backport servers that communicate with clients.
4
+ #
5
+ # Connectable servers check clients for incoming data on each tick.
6
+ #
3
7
  module Connectable
4
8
  def tick
5
9
  clients.each do |client|
@@ -16,6 +20,7 @@ module Backport
16
20
  clients.map(&:stop)
17
21
  end
18
22
 
23
+ # @return [Array<Client>]
19
24
  def clients
20
25
  @clients ||= []
21
26
  end
@@ -1,16 +1,25 @@
1
1
  module Backport
2
2
  module Server
3
+ # A Backport periodical interval server.
4
+ #
3
5
  class Interval < Base
6
+ # @param period [Float] The interval time in seconds.
7
+ # @param block [Proc] The proc to run on each interval.
8
+ # @yieldparam [Interval]
4
9
  def initialize period, &block
5
10
  @period = period
6
11
  @block = block
7
12
  @last_time = Time.now
8
13
  end
9
14
 
15
+ def starting
16
+ @last_time = Time.now
17
+ end
18
+
10
19
  def tick
11
20
  now = Time.now
12
- return unless now - @last_time > @period
13
- @block.call
21
+ return unless now - @last_time >= @period
22
+ @block.call self
14
23
  @last_time = now
15
24
  end
16
25
  end
@@ -1,5 +1,7 @@
1
1
  module Backport
2
2
  module Server
3
+ # A Backport STDIO server.
4
+ #
3
5
  class Stdio < Base
4
6
  include Connectable
5
7
 
@@ -2,11 +2,14 @@ require 'socket'
2
2
 
3
3
  module Backport
4
4
  module Server
5
+ # A Backport TCP server. It runs a thread to accept incoming connections
6
+ # and automatically stops when the socket is closed.
7
+ #
5
8
  class Tcpip < Base
6
9
  include Connectable
7
10
 
8
- def initialize host: 'localhost', port: 1117, adapter: Adapter
9
- @socket = TCPServer.new(host, port)
11
+ def initialize host: 'localhost', port: 1117, adapter: Adapter, socket_class: TCPServer
12
+ @socket = socket_class.new(host, port)
10
13
  @adapter = adapter
11
14
  @stopped = false
12
15
  end
@@ -31,16 +34,46 @@ module Backport
31
34
 
32
35
  def stopping
33
36
  super
37
+ return if socket.closed?
34
38
  begin
35
- socket.shutdown
36
- rescue Errno::ENOTCONN, IOError
37
- # ignore
39
+ socket.shutdown Socket::SHUT_RDWR
40
+ rescue Errno::ENOTCONN, IOError => e
41
+ Backport.logger.info "Minor exception while stopping server [#{e.class}] #{e.message}"
38
42
  end
39
43
  socket.close
40
44
  end
41
45
 
42
- def stopped?
43
- @stopped
46
+ # Accept an incoming connection using accept_nonblock. Return the
47
+ # resulting Client if a connection was accepted or nil if no connections
48
+ # are pending.
49
+ #
50
+ # @return [Client, nil]
51
+ def accept
52
+ result = nil
53
+ mutex.synchronize do
54
+ begin
55
+ conn = socket.accept_nonblock
56
+ addr = conn.addr(true)
57
+ data = {
58
+ family: addr[0],
59
+ port: addr[1],
60
+ hostname: addr[2],
61
+ address: addr[3]
62
+ }
63
+ clients.push Client.new(conn, conn, @adapter, data)
64
+ clients.last.run
65
+ result = clients.last
66
+ rescue IO::WaitReadable, Errno::EAGAIN => e
67
+ # ignore
68
+ rescue Errno::ENOTSOCK, IOError => e
69
+ Backport.logger.info "Server stopped with minor exception [#{e.class}] #{e.message}"
70
+ stop
71
+ rescue Exception => e
72
+ Backport.logger.warn "Server stopped with major exception [#{e.class}] #{e.message}"
73
+ stop
74
+ end
75
+ end
76
+ result
44
77
  end
45
78
 
46
79
  private
@@ -55,24 +88,10 @@ module Backport
55
88
  def start_accept_thread
56
89
  Thread.new do
57
90
  until stopped?
58
- begin
59
- conn = socket.accept
60
- mutex.synchronize do
61
- addr = conn.addr(true)
62
- data = {
63
- family: addr[0],
64
- port: addr[1],
65
- hostname: addr[2],
66
- address: addr[3]
67
- }
68
- clients.push Client.new(conn, conn, @adapter, data)
69
- clients.last.run
70
- end
71
- rescue Exception => e
72
- STDERR.puts "Server stopped with exception [#{e.class}] #{e.message}"
73
- stop
74
- break
75
- end
91
+ client = accept
92
+ Backport.logger.info "Client connected: #{client.adapter.remote}" unless client.nil?
93
+ sleep 0.01
94
+ stop if socket.closed?
76
95
  end
77
96
  end
78
97
  end
@@ -1,3 +1,3 @@
1
- module Backport
2
- VERSION = "0.2.0"
3
- end
1
+ module Backport
2
+ VERSION = '0.3.0'.freeze
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backport
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fred Snyder
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-21 00:00:00.000000000 Z
11
+ date: 2019-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -75,6 +75,7 @@ extra_rdoc_files: []
75
75
  files:
76
76
  - ".gitignore"
77
77
  - ".rspec"
78
+ - ".rubocop.yml"
78
79
  - ".travis.yml"
79
80
  - CHANGELOG.md
80
81
  - Gemfile