backport 0.1.0 → 0.2.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: 631a99833ea290a7aa20e2e18f188eb19b81a141e32288feeb96ac514c39a86c
4
- data.tar.gz: 5dcd3ea31192dbb810d18f7e5ca6c3757f3613a8e0a625f0d14e7801714a0e62
3
+ metadata.gz: 2aedfc84d845aedeb6d7146cb21a74c4d1d74d90844407982ce454e8aa2d16b3
4
+ data.tar.gz: e222830378478e7990601880b42e60dd4e34ba1791e63345a2a5fa0a2898bbf8
5
5
  SHA512:
6
- metadata.gz: 1ab759e6cd1fb063dbc9f56648d0f98771b3b2390593937c73c6d6b8e655110644b94028f2739dab3bb2f7b5ca704d2bba0838d604866b9939f607eccd85fae5
7
- data.tar.gz: 8e6531f1ad46e37f01086991bca2c6ac1ddd262c381fb514a3a4b0674b5d7001412c1b469ae56a03d50a2edf69040bd761940413b3b2d741d4fb7ec3d1b3bfec
6
+ metadata.gz: 5cd8632b158f0e5dda4e5377b5fc74a9e7ae416a738941dcf276cd48d8040c671eb9e337a089215e092ec499fecbadfaf6c015f49f09b72bc4cee556780afa4f
7
+ data.tar.gz: e52ea4e36b3bfbd397dabc9410d07af6106d1c7bc1e9dbac98d18a2def432300e64696ee3e277ad1e19639d61027ccb46d3a3f665603031ffab26c81d4f7ba83
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format documentation
2
1
  --color
3
2
  --require spec_helper
data/.travis.yml CHANGED
@@ -3,5 +3,16 @@ sudo: false
3
3
  language: ruby
4
4
  cache: bundler
5
5
  rvm:
6
+ - 2.1
7
+ - 2.2
8
+ - 2.3
9
+ - 2.4
10
+ - 2.5.1
6
11
  - 2.5.3
12
+ - jruby-9.0.5.0
13
+ - jruby-9.1.16.0
14
+ matrix:
15
+ include:
16
+ - rvm: 2.4
17
+ os: osx
7
18
  before_install: gem install bundler -v 1.17.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ ## 0.2.0 - December 21, 2018
2
+ - Minor bug fixes in STDIO server
3
+ - More efficient client reads
4
+ - Rename server methods `prepare` for clarity
5
+ - Improved socket state handling
6
+ - More accurate interval time
7
+ - Adapter#remote attribute
8
+ - Adapter#close method
9
+ - Socket exception handling
10
+
11
+ ## 0.1.0 - December 20, 2018
12
+ First release
data/README.md CHANGED
@@ -20,7 +20,7 @@ gem 'backport'
20
20
 
21
21
  ## Usage
22
22
 
23
- This example demonstrates a simple echo server.
23
+ A simple echo server:
24
24
 
25
25
  ```ruby
26
26
  require 'backport'
@@ -40,8 +40,17 @@ module MyAdapter
40
40
  end
41
41
 
42
42
  Backport.run do
43
- Backport.start_tcp_server(adapter: MyAdapter)
44
- Backport.start_interval 1 do
43
+ Backport.prepare_tcp_server(host: 'localhost', port: 8000, adapter: MyAdapter)
44
+ end
45
+ ```
46
+
47
+ An interval server that runs once per second:
48
+
49
+ ```ruby
50
+ require 'backport'
51
+
52
+ Backport.run do
53
+ Backport.prepare_interval 1 do
45
54
  puts "tick"
46
55
  end
47
56
  end
data/backport.gemspec CHANGED
@@ -23,4 +23,5 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency "bundler", "~> 1.17"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
25
  spec.add_development_dependency "rspec", "~> 3.0"
26
+ spec.add_development_dependency "simplecov", "~> 0.14"
26
27
  end
data/lib/backport.rb CHANGED
@@ -8,28 +8,57 @@ module Backport
8
8
  autoload :Client, 'backport/client'
9
9
 
10
10
  class << self
11
- def start_stdio_server adapter: Adapter
12
- machine.start_server Backport::Server::Stdio.new(adapter: adapter)
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)
13
17
  end
14
18
 
15
- def start_tcp_server host: 'localhost', port: 1117, adapter: Adapter
16
- machine.start_server Backport::Server::Tcpip.new(host: host, port: port, adapter: adapter)
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)
17
27
  end
18
28
 
19
- def start_interval period, &block
20
- machine.start_server Backport::Server::Interval.new(period, &block)
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)
21
35
  end
22
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]
23
48
  def run &block
24
49
  machine.run &block
25
50
  end
26
51
 
52
+ # Stop the Backport machine.
53
+ #
54
+ # @return [void]
27
55
  def stop
28
56
  machine.stop
29
57
  end
30
58
 
31
59
  private
32
60
 
61
+ # @return [Machine]
33
62
  def machine
34
63
  @machine ||= Machine.new
35
64
  end
@@ -1,27 +1,67 @@
1
1
  module Backport
2
2
  class Adapter
3
- def initialize output
3
+ # A hash of information about the client connection. The data can vary
4
+ # based on the transport, e.g., :hostname and :address for TCP connections
5
+ # or :filename for file streams.
6
+ #
7
+ # @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 = {}
4
13
  @out = output
14
+ @remote = remote
5
15
  end
6
16
 
7
- def opening
8
- STDERR.puts "Opening"
9
- end
17
+ # A callback triggered when a client connection is opening. Subclasses
18
+ # and/or modules should override this method to provide their own
19
+ # functionality.
20
+ #
21
+ # @return [void]
22
+ def opening; end
10
23
 
11
- def closing
12
- STDERR.puts "Closing"
13
- end
24
+ # A callback triggered when a client connection is closing. Subclasses
25
+ # and/or modules should override this method to provide their own
26
+ # functionality.
27
+ #
28
+ # @return [void]
29
+ def closing; end
14
30
 
15
- def sending data
16
- STDERR.puts "Client sent #{data}"
17
- end
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.
34
+ #
35
+ # @param data [String]
36
+ # @return [void]
37
+ def sending(data); end
18
38
 
39
+ # Send data to the client.
40
+ #
41
+ # @param data [String]
42
+ # @return [void]
19
43
  def write data
20
- @out.print data
44
+ @out.write data
45
+ @out.flush
21
46
  end
22
47
 
48
+ # Send a line of data to the client.
49
+ #
50
+ # @param data [String]
51
+ # @return [void]
23
52
  def write_line data
24
53
  @out.puts data
54
+ @out.flush
55
+ end
56
+
57
+ def closed?
58
+ @closed ||= false
59
+ end
60
+
61
+ def close
62
+ return if closed?
63
+ @closed = true
64
+ closing
25
65
  end
26
66
  end
27
67
  end
@@ -1,10 +1,12 @@
1
1
  module Backport
2
2
  class Client
3
- def initialize input, output, adapter
3
+ # @return [Adapter]
4
+ attr_reader :adapter
5
+
6
+ def initialize input, output, adapter, remote = {}
4
7
  @in = input
5
8
  @out = output
6
- # @todo Adapter can either be an Adapter class or a module
7
- @adapter = make_adapter(adapter)
9
+ @adapter = make_adapter(adapter, remote)
8
10
  @stopped = true
9
11
  @buffer = ''
10
12
  end
@@ -41,11 +43,11 @@ module Backport
41
43
 
42
44
  private
43
45
 
44
- def make_adapter cls_mod
45
- if cls_mod.is_a?(Class)
46
- @adapter = cls_mod.new(@out)
47
- elsif cls_mod.is_a?(Module)
48
- @adapter = Adapter.new(@out)
46
+ def make_adapter cls_mod, remote
47
+ if cls_mod.is_a?(Class) && cls_mod <= Backport::Adapter
48
+ @adapter = cls_mod.new(@out, remote)
49
+ elsif cls_mod.class == Module
50
+ @adapter = Adapter.new(@out, remote)
49
51
  @adapter.extend cls_mod
50
52
  else
51
53
  raise TypeError, "#{cls_mod} is not a valid Backport adapter"
@@ -59,13 +61,17 @@ module Backport
59
61
  def run_input_thread
60
62
  Thread.new do
61
63
  until stopped?
62
- char = @in.getc
63
- if char.nil?
64
- STDERR.puts "Client received nil. Stopping"
64
+ @in.flush
65
+ begin
66
+ chars = @in.sysread(255)
67
+ rescue EOFError, Errno::ECONNRESET
68
+ chars = nil
69
+ end
70
+ if chars.nil?
65
71
  stop
66
72
  break
67
73
  end
68
- mutex.synchronize { @buffer.concat char }
74
+ mutex.synchronize { @buffer.concat chars }
69
75
  end
70
76
  end
71
77
  end
@@ -4,30 +4,48 @@ module Backport
4
4
  @stopped = true
5
5
  end
6
6
 
7
+ # 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
9
+ # until the machine is stopped.
10
+ #
11
+ # @return [void]
7
12
  def run
8
13
  return unless stopped?
14
+ servers.clear
9
15
  @stopped = false
10
16
  yield if block_given?
11
17
  run_server_thread
12
18
  end
13
19
 
20
+ # Stop the machine.
21
+ #
22
+ # @return [void]
14
23
  def stop
15
24
  servers.map(&:stop)
16
25
  servers.clear
17
26
  @stopped = true
18
27
  end
19
28
 
29
+ # True if the machine is stopped.
30
+ #
20
31
  def stopped?
21
32
  @stopped ||= false
22
33
  end
23
34
 
24
- def start_server server
35
+ # Add a server to the machine. The server will be started when the machine
36
+ # starts. If the machine is already running, the server will be started
37
+ # immediately.
38
+ #
39
+ # @param server [Server::Base]
40
+ # @return [void]
41
+ def prepare server
25
42
  servers.push server
43
+ server.start unless stopped?
26
44
  end
27
45
 
28
46
  private
29
47
 
30
- # @return [Array<Backport::Server>]
48
+ # @return [Array<Server::Base>]
31
49
  def servers
32
50
  @servers ||= []
33
51
  end
@@ -35,10 +53,7 @@ module Backport
35
53
  def run_server_thread
36
54
  servers.map(&:start)
37
55
  until stopped?
38
- servers.each do |server|
39
- server.tick
40
- sleep 0.001
41
- end
56
+ servers.each(&:tick)
42
57
  sleep 0.001
43
58
  end
44
59
  end
@@ -1,21 +1,40 @@
1
1
  module Backport
2
2
  module Server
3
3
  class Base
4
- def stopped?
5
- @stopped = false if @stopped.nil?
6
- @stopped
4
+ def started?
5
+ @started ||= false
6
+ @started
7
7
  end
8
8
 
9
9
  def stop
10
10
  stopping
11
- @stopped = true
11
+ @started = false
12
12
  end
13
13
 
14
+ # A callback triggered when a Machine starts running or the server is
15
+ # added to a running machine. Subclasses should override this method to
16
+ # provide their own functionality.
17
+ #
18
+ # @return [void]
19
+ def starting; end
20
+
21
+ # A callback triggered when the server is stopping. Subclasses should
22
+ # override this method to provide their own functionality.
23
+ #
24
+ # @return [void]
14
25
  def stopping; end
15
26
 
27
+ # A callback triggered from the main loop of a running Machine.
28
+ # Subclasses should override this method to provide their own
29
+ # functionality.
30
+ #
31
+ # @return [void]
16
32
  def tick; end
17
33
 
18
- def start; end
34
+ def start
35
+ starting
36
+ @started = true
37
+ end
19
38
  end
20
39
  end
21
40
  end
@@ -8,7 +8,7 @@ module Backport
8
8
  end
9
9
  end
10
10
 
11
- def start
11
+ def starting
12
12
  clients.map(&:run)
13
13
  end
14
14
 
@@ -16,8 +16,6 @@ module Backport
16
16
  clients.map(&:stop)
17
17
  end
18
18
 
19
- protected
20
-
21
19
  def clients
22
20
  @clients ||= []
23
21
  end
@@ -8,13 +8,10 @@ module Backport
8
8
  end
9
9
 
10
10
  def tick
11
- return unless Time.now - @last_time > @period
11
+ now = Time.now
12
+ return unless now - @last_time > @period
12
13
  @block.call
13
- @last_time = Time.now
14
- end
15
-
16
- def stop
17
- @stopped = true
14
+ @last_time = now
18
15
  end
19
16
  end
20
17
  end
@@ -6,7 +6,7 @@ module Backport
6
6
  def initialize input: STDIN, output: STDOUT, adapter: Adapter
7
7
  @in = input
8
8
  @out = output
9
- @in.binmode
9
+ @out.binmode
10
10
  @adapter = adapter
11
11
  clients.push Client.new(input, output, adapter)
12
12
  end
@@ -8,28 +8,44 @@ module Backport
8
8
  def initialize host: 'localhost', port: 1117, adapter: Adapter
9
9
  @socket = TCPServer.new(host, port)
10
10
  @adapter = adapter
11
- end
12
-
13
- def stopped?
14
- @stopped ||= false
11
+ @stopped = false
15
12
  end
16
13
 
17
14
  def tick
18
15
  mutex.synchronize do
19
16
  clients.each do |client|
17
+ if client.adapter.closed?
18
+ client.stop
19
+ next
20
+ end
20
21
  input = client.read
21
22
  client.sending input unless input.nil?
22
23
  end
24
+ clients.delete_if(&:stopped?)
23
25
  end
24
26
  end
25
27
 
26
- def start
27
- super
28
+ def starting
28
29
  start_accept_thread
29
30
  end
30
31
 
32
+ def stopping
33
+ super
34
+ begin
35
+ socket.shutdown
36
+ rescue Errno::ENOTCONN, IOError
37
+ # ignore
38
+ end
39
+ socket.close
40
+ end
41
+
42
+ def stopped?
43
+ @stopped
44
+ end
45
+
31
46
  private
32
47
 
48
+ # @return [TCPSocket]
33
49
  attr_reader :socket
34
50
 
35
51
  def mutex
@@ -39,10 +55,23 @@ module Backport
39
55
  def start_accept_thread
40
56
  Thread.new do
41
57
  until stopped?
42
- conn = socket.accept
43
- mutex.synchronize do
44
- clients.push Client.new(conn, conn, @adapter)
45
- clients.last.run
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
46
75
  end
47
76
  end
48
77
  end
@@ -1,3 +1,3 @@
1
1
  module Backport
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
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.1.0
4
+ version: 0.2.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-20 00:00:00.000000000 Z
11
+ date: 2018-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.14'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.14'
55
69
  description:
56
70
  email:
57
71
  - fsnyder@castwide.com
@@ -62,6 +76,7 @@ files:
62
76
  - ".gitignore"
63
77
  - ".rspec"
64
78
  - ".travis.yml"
79
+ - CHANGELOG.md
65
80
  - Gemfile
66
81
  - LICENSE.txt
67
82
  - README.md