backport 0.2.0 → 0.3.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: 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