iodine 0.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of iodine might be problematic. Click here for more details.

checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 51d03f6b35a78289c742fbed134a68963bc5ddd6
4
+ data.tar.gz: 8d541074dd1f8fbde916034e659b28816417e863
5
+ SHA512:
6
+ metadata.gz: 3365f3efe45797ab9fef24fd61e003425debbc680dc190ca14ffc7797fda812e520c1849a95232f199e3a50b220d04f37e8e922d8a72a8cc65bc1eaa605cfec7
7
+ data.tar.gz: 3cb5df7d237484fe18fd8828f33a682d210339821322b7c2e709a826bde3cb66c9e0d638a5843b46e28fbab56cf09a0203bed7848ec3383174d3399ab2190c8f
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in iodine.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Boaz Segev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # Iodine
2
+
3
+ Iodine makes writing evented server applications easy to write.
4
+
5
+ Iodine is intended to replace the use of a generic reacor, such as EventMachine or GReactor and it hides all the nasty details of creating the event loop.
6
+
7
+ To use Iodine, you just set up your tasks - including a single server, if you want one. Iodine will start running once your application is finished and it won't stop runing until all the tasks have completed.
8
+
9
+ Iodine v. 0.0.1 isn't well tested just yet... but I'm releasing it anyway, to reserve the name and because initial testing shows that it works.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'iodine'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install iodine
26
+
27
+ ## Simple Usage
28
+
29
+ Iodine starts to work once you app is finished with setting all the tasks up (upon exit).
30
+
31
+ To see how that works, open your `irb` terminal an try this:
32
+
33
+ ```ruby
34
+ require 'iodine'
35
+
36
+ # Iodine supports shutdown hooks
37
+ Iodine.on_shutdown { puts "Done!" }
38
+ # The last hook is the first scheduled for execution
39
+ Iodine.on_shutdown { puts "Finishing up :-)" }
40
+
41
+ # Setup tasks using the `run` or `callback` methods
42
+ Iodine.run do
43
+ # tasks can create more tasks...
44
+ Iodine.run { puts "Task 2 completed!" }
45
+ puts "Task 1 completed!"
46
+ end
47
+
48
+ # set concurrency level (defaults to a single thread).
49
+ Iodine.threads = 5
50
+
51
+ # Iodine will start executing tasks once your script is done.
52
+ exit
53
+ ```
54
+
55
+ ## Server Usage
56
+
57
+ Iodine is designed to help write network services (Servers) where each script is intended to implement a single server.
58
+
59
+ This is not a philosophy based on any idea or preferences, but rather a response to real-world design where each Ruby script is usually assigned a single port for network access (hence, a single server).
60
+
61
+ ## Development
62
+
63
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
64
+
65
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
66
+
67
+ ## Contributing
68
+
69
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/iodine.
70
+
71
+
72
+ ## License
73
+
74
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
75
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'benchmark'
4
+ $LOAD_PATH.unshift File.expand_path(File.join('..', '..', 'lib'), __FILE__ )
5
+ require "bundler/setup"
6
+ require "iodine"
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ # require "pry"
13
+ # Pry.start
14
+
15
+ require "irb"
16
+ IRB.start
data/bin/echo ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ Root ||= Pathname.new(File.dirname(__FILE__)).expand_path
5
+ Dir.chdir Root.join('..').to_s
6
+
7
+ require "bundler/setup"
8
+ require "iodine"
9
+ require 'stringio'
10
+
11
+ # ab -n 10000 -c 200 -k http://127.0.0.1:3000/ctrl
12
+ # ~/ruby/wrk/wrk -c400 -d10 -t12 http://localhost:3000/ctrl
13
+
14
+
15
+
16
+ class EchoServer < Iodine::Protocol
17
+ def on_message data
18
+ write("-- Closing connection, goodbye.\n") && close if data =~ /^(bye|close|exit|stop)/i
19
+ write(">> #{data.chomp}\n")
20
+ end
21
+
22
+ def ping
23
+ write "-- Are you still there?\n"
24
+ end
25
+
26
+ def on_close
27
+ Iodine.info "Closed connection."
28
+ end
29
+ def on_open
30
+ Iodine.info "Opened connection."
31
+ set_timeout 5
32
+ end
33
+ end
34
+
35
+
36
+ Iodine.protocol = EchoServer
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+ #encoding: UTF-8
3
+
4
+ require 'eventmachine'
5
+ require 'stringio'
6
+
7
+
8
+ module MiniServer
9
+ def post_init
10
+ comm_inactivity_timeout = 5
11
+ @headers = {}
12
+ # start_tls :private_key_file => 'server.key', :cert_chain_file => 'server.crt', :verify_peer => false
13
+ end
14
+
15
+ def receive_data data
16
+ # EventMachine.defer do
17
+ data = ::StringIO.new data
18
+ l = nil
19
+ headers = @headers
20
+ while (l = data.gets)
21
+ unless l =~ /^[\r]?\n/
22
+ if l.include? ':'
23
+ l = l.strip.downcase.split(':', 2)
24
+ headers[l[0]] = l[1]
25
+ else
26
+ headers[:method], headers[:query], headers[:version] = l.split(/[\s]+/, 3)
27
+ end
28
+ next
29
+ end
30
+ if headers['connection'] =~ /keep/i || headers[:version] =~ /1\.1/
31
+ send_data "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: keep-alive\r\nKeep-Alive: 5\r\n\r\nhello world\n"
32
+ else
33
+ send_data "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: close\r\n\r\nhello world\n"
34
+ close_connection true
35
+ end
36
+ headers.clear
37
+ end
38
+ data.string.clear
39
+ # end
40
+ end
41
+
42
+ def unbind
43
+ end
44
+ end
45
+
46
+ # Note that this will block current thread.
47
+ EventMachine.run {
48
+ trap("TERM") { EventMachine.stop_event_loop }
49
+ trap("INT") { EventMachine.stop_event_loop }
50
+ EventMachine.start_server "127.0.0.1", 3000, MiniServer
51
+ }
52
+
53
+
54
+ # ab -n 10000 -c 200 -k http://127.0.0.1:3000/ctrl
55
+ # ~/ruby/wrk/wrk -c400 -d10 -t12 http://localhost:3000/ctrl
56
+
data/bin/http_test ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ Root ||= Pathname.new(File.dirname(__FILE__)).expand_path
5
+ Dir.chdir Root.join('..').to_s
6
+
7
+ require "bundler/setup"
8
+ require "iodine"
9
+ require 'stringio'
10
+
11
+ # ab -n 10000 -c 200 -k http://127.0.0.1:3000/
12
+ # ab -n 10000 -c 200 -k http://localhost:3000/
13
+ # ~/ruby/wrk/wrk -c400 -d10 -t12 http://localhost:3000/
14
+
15
+
16
+
17
+ class MiniServer < Iodine::SSLProtocol
18
+ def on_open
19
+ @headers = {}
20
+ set_timeout 1
21
+ end
22
+
23
+ def on_message data
24
+ data = ::StringIO.new data
25
+ l = nil
26
+ headers = @headers
27
+ while (l = data.gets)
28
+ unless l =~ /^[\r]?\n/
29
+ if l.include? ':'
30
+ l = l.strip.downcase.split(':', 2)
31
+ headers[l[0]] = l[1]
32
+ else
33
+ headers[:method], headers[:query], headers[:version] = l.strip.split(/[\s]+/, 3)
34
+ end
35
+ next
36
+ end
37
+ if headers['connection'] =~ /keep/i || headers[:version] =~ /1\.1/
38
+ write "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: keep-alive\r\nKeep-Alive: 5\r\n\r\nhello world\n"
39
+ else
40
+ write "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: close\r\n\r\nhello world\n"
41
+ close
42
+ end
43
+ headers.clear
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ Iodine.protocol = MiniServer
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/iodine.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'iodine/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "iodine"
8
+ spec.version = Iodine::VERSION
9
+ spec.authors = ["Boaz Segev"]
10
+ spec.email = ["Boaz@2be.co.il"]
11
+
12
+ spec.summary = %q{ IOdine makes writing evented server applications easy to write. }
13
+ spec.description = %q{ IOdine is a super easy way to write network services and tasking scripts.}
14
+ spec.homepage = "https://github.com/boazsegev/iodine"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.10"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "minitest"
33
+ end
data/lib/iodine.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "logger"
2
+ require 'socket'
3
+
4
+
5
+ module Iodine
6
+ extend self
7
+ end
8
+
9
+
10
+ require "iodine/version"
11
+ require "iodine/settings"
12
+ require "iodine/logging"
13
+ require "iodine/core"
14
+ require "iodine/timers"
15
+ require "iodine/protocol"
16
+ require "iodine/ssl_protocol"
17
+ require "iodine/io"
@@ -0,0 +1,112 @@
1
+ module Iodine
2
+ public
3
+
4
+ #######################
5
+ ## Events
6
+
7
+ # Accepts a block and runs it asynchronously. This method runs asynchronously and returns immediately.
8
+ #
9
+ # use:
10
+ #
11
+ # GReactor.run_async(arg1, arg2, arg3 ...) { |arg1, arg2, arg3...| do_something }
12
+ #
13
+ # the block will be run within the current context, allowing access to current methods and variables.
14
+ #
15
+ # @return [GReactor] always returns the reactor object.
16
+ def run *args, &block
17
+ queue block, args
18
+ end
19
+ alias :run_async :run
20
+
21
+ # This method runs an object's method asynchronously and returns immediately. This method will also run an optional callback if a block is supplied.
22
+ #
23
+ # This method accepts:
24
+ # object:: an object who's method will be called.
25
+ # method:: the method's name to be called. type: Symbol.
26
+ # *args:: any arguments to be passed to the method.
27
+ # block (optional):: If a block is supplied, it will be used as a callback and the method's return value will be passed on to the block.
28
+ #
29
+ # @return [GReactor] always returns the reactor object.
30
+ def callback object, method_name, *args, &block
31
+ block ? queue(@callback_proc, [object.method(method_name), args, block]) : queue(object.method(method_name), args)
32
+ end
33
+
34
+ # Adds a job OR a block to the queue. {GReactor.run_async} and {GReactor.callback} extend this core method.
35
+ #
36
+ # This method accepts two possible arguments:
37
+ # job:: An object that answers to `call`, usually a Proc or Lambda.
38
+ # args:: (optional) An Array of arguments to be passed on to the executed method.
39
+ #
40
+ # @return [GReactor] always returns the reactor object.
41
+ #
42
+ # The callback will NOT be called if the executed job failed (raised an exception).
43
+ # @see .run_async
44
+ #
45
+ # @see .callback
46
+ def queue job, args = nil
47
+ @queue << [job, args]
48
+ self
49
+ end
50
+
51
+ # Adds a shutdown tasks. These tasks should be executed in order of creation.
52
+ def on_shutdown *args, &block
53
+ @shutdown_queue << [block, args]
54
+ self
55
+ end
56
+
57
+ protected
58
+
59
+ @queue = Queue.new
60
+ @shutdown_queue = Queue.new
61
+ @stop = true
62
+ @done = false
63
+ @logger = Logger.new(STDOUT)
64
+ @thread_count = 1
65
+ @ios = {}
66
+ @io_in = Queue.new
67
+ @io_out = Queue.new
68
+ @shutdown_mutex = Mutex.new
69
+ @servers = {}
70
+
71
+
72
+ def cycle
73
+ work until @stop && @queue.empty?
74
+ @shutdown_mutex.synchronize { shutdown }
75
+ work until @queue.empty?
76
+ run { true }
77
+ end
78
+
79
+ def work
80
+ job = @queue && @queue.pop
81
+ if job && job[0]
82
+ begin
83
+ job[0].call *job[1]
84
+ rescue => e
85
+ error e
86
+ end
87
+ end
88
+ end
89
+
90
+ Kernel.at_exit do
91
+ threads = []
92
+ @thread_count.times { threads << Thread.new { cycle } }
93
+ unless @stop
94
+ catch(:stop) { sleep }
95
+ @logger << "\nShutting down Iodine. Setting shutdown timeout to 30 seconds.\n"
96
+ @stop = true
97
+ # setup exit timeout.
98
+ threads.each {|t| Thread.new {sleep 30; t.kill; t.kill } }
99
+ end
100
+ threads.each {|t| t.join rescue true }
101
+ end
102
+
103
+ def shutdown
104
+ return if @done
105
+ @stop = @done = true
106
+ arr = []
107
+ arr.push @shutdown_queue.pop until @shutdown_queue.empty?
108
+ @queue.push arr.pop while arr[0]
109
+ @thread_count.times { run { true } }
110
+ end
111
+
112
+ end
data/lib/iodine/io.rb ADDED
@@ -0,0 +1,110 @@
1
+ module Iodine
2
+
3
+ public
4
+
5
+ # Gets the last time at which the IO Reactor was last active (last "tick").
6
+ def time
7
+ @time
8
+ end
9
+
10
+ # replaces an IO's protocol object.
11
+ #
12
+ # accepts:
13
+ # io:: the raw IO object.
14
+ # protocol:: a protocol instance - should be a Protocol or SSLProtocol (subclass) instance. type will NOT be checked - but Iodine could break if there is a type mismatch.
15
+ #
16
+ def switch_protocol *args
17
+ @io_in << args
18
+ end
19
+
20
+
21
+ protected
22
+
23
+ @port = (ARGV.index('-p') && ARGV[ARGV.index('-p') + 1]) || ENV['PORT'] || 3000
24
+ @bind = (ARGV.index('-ip') && ARGV[ARGV.index('-ip') + 1]) || ENV['IP'] || "0.0.0.0"
25
+ @protocol = nil
26
+ @ssl_context = nil
27
+ @time = Time.now
28
+
29
+ @timeout_proc = Proc.new {|prot| prot.timeout?(@time) }
30
+ @status_loop = Proc.new {|io| @io_out << io if io.closed? || !(io.stat.readable? rescue false) }
31
+ @close_callback = Proc.new {|prot| prot.on_close if prot }
32
+ REACTOR = Proc.new do
33
+ if @queue.empty?
34
+ #clear any closed IO objects.
35
+ @time = Time.now
36
+ @ios.keys.each &@status_loop
37
+ @ios.values.each &@timeout_proc
38
+ until @io_in.empty?
39
+ n_io = @io_in.pop
40
+ @ios[n_io[0]] = n_io[1]
41
+ end
42
+ until @io_out.empty?
43
+ o_io = @io_out.pop
44
+ o_io.close unless o_io.closed?
45
+ queue @close_callback, @ios.delete(o_io)
46
+ end
47
+ # react to IO events
48
+ begin
49
+ r = IO.select(@ios.keys, nil, nil, 0.15)
50
+ r[0].each {|io| queue @ios[io] } if r
51
+ rescue => e
52
+
53
+ end
54
+ unless @stop && @queue.empty?
55
+ # @ios.values.each &@timeout_loop
56
+ @check_timers && queue(@check_timers)
57
+ queue REACTOR
58
+ end
59
+ else
60
+ queue REACTOR
61
+ end
62
+ end
63
+ # internal helper methods and classes.
64
+ module Base
65
+ # the server listener Protocol.
66
+ class Listener < ::Iodine::Protocol
67
+ def on_open
68
+ @protocol = Iodine.protocol
69
+ end
70
+ def call
71
+ begin
72
+ n_io = nil
73
+ loop do
74
+ n_io = @io.accept_nonblock
75
+ @protocol.accept(n_io)
76
+ end
77
+ rescue Errno::EWOULDBLOCK => e
78
+
79
+ rescue OpenSSL::SSL::SSLError => e
80
+ warn "SSL Error - Self-signed Certificate?".freeze
81
+ n_io.close if n_io && !n_io.closed?
82
+ rescue => e
83
+ n_io.close if n_io && !n_io.closed?
84
+ @stop = true
85
+ raise e
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ ########
92
+ ## remember to set traps (once) when 'listen' is called.
93
+ run do
94
+ next unless @protocol
95
+
96
+ shut_down_proc = Proc.new {|protocol| protocol.on_shutdown ; protocol.close }
97
+ on_shutdown do
98
+ @logger << "Stopping to listen on port #{@port} and shutting down.\n"
99
+ @server.close unless @server.closed?
100
+ @ios.values.each {|p| queue shut_down_proc, p }
101
+ end
102
+ @server = ::TCPServer.new(@bind, @port)
103
+ ::Iodine::Base::Listener.accept(@server)
104
+ @logger << "Iodine #{VERSION} is listening on port #{@port}\n"
105
+ old_int_trap = trap('INT') { throw :stop; trap('INT', old_int_trap) if old_int_trap }
106
+ old_term_trap = trap('TERM') { throw :stop; trap('TERM', old_term_trap) if old_term_trap }
107
+ @logger << "Press ^C to stop the server.\n"
108
+ queue REACTOR
109
+ end
110
+ end
@@ -0,0 +1,45 @@
1
+ module Iodine
2
+ public
3
+
4
+ # Gets the logging object and allows you to call logging methods (i.e. `Iodine.log.info "Running"`).
5
+ def logger log = nil
6
+ @logger
7
+ end
8
+
9
+ # logs info
10
+ # @return [String, Exception, Object] always returns the Object sent to the log.
11
+ def info data, &block
12
+ @logger.info data, &block if @logger
13
+ data
14
+ end
15
+ # logs debug info
16
+ # @return [String, Exception, Object] always returns the Object sent to the log.
17
+ def debug data, &block
18
+ @logger.debug data, &block if @logger
19
+ data
20
+ end
21
+ # logs warning
22
+ # @return [String, Exception, Object] always returns the Object sent to the log.
23
+ def warn data, &block
24
+ @logger.warn data, &block if @logger
25
+ data
26
+ end
27
+ # logs errors
28
+ # @return [String, Exception, Object] always returns the Object sent to the log.
29
+ def error data, &block
30
+ @logger.error data, &block if @logger
31
+ data
32
+ end
33
+ # logs a fatal error
34
+ # @return [String, Exception, Object] always returns the Object sent to the log.
35
+ def fatal data, &block
36
+ @logger.fatal data, &block if @logger
37
+ data
38
+ end
39
+
40
+
41
+ protected
42
+
43
+ @logger = Logger.new(STDOUT)
44
+
45
+ end
@@ -0,0 +1,155 @@
1
+ module Iodine
2
+
3
+ # This is the Basic Iodine server unit - a network protocol.
4
+ #
5
+ # A new protocol instance will be created for every network connection.
6
+ #
7
+ # The recommended use is to inherit this class (or {SSLProtocol}) and override any of the following:
8
+ # on_open:: called whenever the Protocol is initialized. Override this to initialize the Protocol object.
9
+ # on_message(data):: called whenever data is received from the IO. Override this to implement the actual network protocol.
10
+ # on_close:: called AFTER the Protocol's IO is closed.
11
+ # on_shutdown:: called when the server's shutdown process had started and BEFORE the Protocol's IO is closed. It allows graceful shutdown for network protocols.
12
+ # ping::
13
+ #
14
+ # Once the network protocol class was created, remember to tell Iodine about it:
15
+ # class MyProtocol << Iodine::Protocol
16
+ # # your code here
17
+ # end
18
+ # # tell Iodine
19
+ # Iodine.protocol = MyProtocol
20
+ #
21
+ class Protocol
22
+
23
+ # returns the raw IO object. Using one of the Protocol methods {#write}, {#read}, {#close} is prefered over direct access.
24
+ attr_reader :io
25
+
26
+ # Sets the timeout in seconds for IO activity (set timeout within {#on_open}).
27
+ #
28
+ # After timeout is reached, {#ping} will be closed. The connection will be closed if {#ping} returns `false` or `nil`.
29
+ def set_timeout seconds
30
+ @timeout = seconds
31
+ end
32
+
33
+ # This method is called whenever the Protocol is initialized - i.e.:
34
+ # a new connection is established or an old connection switches to this protocol.
35
+ def on_open
36
+ end
37
+ # This method is called whenever data is received from the IO.
38
+ def on_message data
39
+ end
40
+
41
+ # This method is called AFTER the Protocol's IO is closed - it will only be called once.
42
+ def on_close
43
+ end
44
+
45
+ # This method is called when the server's shutdown process had started and BEFORE the Protocol's IO is closed. It allows graceful shutdown for network protocols.
46
+ def on_shutdown
47
+ end
48
+
49
+ # This method is called whenever a timeout has occurred. Either implement a ping or return `false` to disconnect.
50
+ #
51
+ # A `false` or `nil` return value will cause disconnection
52
+ def ping
53
+ false
54
+ end
55
+
56
+
57
+ # Closes the IO object.
58
+ # @return [nil]
59
+ def close
60
+ @io.close unless @io.closed?
61
+ nil
62
+ end
63
+ alias :disconnect :close
64
+
65
+ # reads from the IO up to the specified number of bytes (defaults to ~2Mb).
66
+ def read size = 2_097_152
67
+ touch
68
+ @io.recv_nonblock( size )
69
+ rescue => e
70
+ nil
71
+ end
72
+
73
+ # this method, writes data to the socket / io object.
74
+ def write data
75
+ begin
76
+ @send_locker.synchronize do
77
+ r = @io.write data
78
+ touch
79
+ r
80
+ end
81
+ rescue => e
82
+ # GReactor.warn e
83
+ close
84
+ end
85
+ end
86
+
87
+
88
+ # This method allows switiching the IO's protocol that will be used the NEXT time
89
+ # Iodine receives data using this protocol's IO.
90
+ #
91
+ # Switing protocols bypasses the {#on_close} method. Override this method for any cleanup needed (if at any),
92
+ # but remember to call `super` for the actual protocol switching implementation.
93
+ def switch_protocol new_protocol
94
+ Iodine.switch_protocol @io, new_protocol
95
+ end
96
+
97
+ #################
98
+ ## the following are Iodine's "system" methods, used internally. Don't override.
99
+
100
+
101
+ # This method is used by Iodine to initialized the Protocol.
102
+ #
103
+ # Normally you won't need to override this method. Override {#on_open} instead.
104
+ def initialize io
105
+ @timeout ||= nil
106
+ @send_locker = Mutex.new
107
+ @locker = Mutex.new
108
+ @io = io
109
+ switch_protocol self
110
+ touch
111
+ on_open
112
+ end
113
+
114
+ # Called by Iodine whenever there is data in the IO's read buffer.
115
+ #
116
+ # Normally you won't need to override this method. Override {#on_message} instead.
117
+ def call
118
+ return unless @locker.try_lock
119
+ begin
120
+ data = read
121
+ if data
122
+ on_message(data)
123
+ data.clear
124
+ end
125
+ ensure
126
+ @locker.unlock
127
+ end
128
+ end
129
+
130
+
131
+ # This method is used by Iodine to ask whether a timeout has occured.
132
+ #
133
+ # Normally you won't need to override this method. See {#ping}
134
+ def timeout? time
135
+ (ping || close) if @timeout && !@send_locker.locked? && ( (time - @last_active) > @timeout )
136
+ end
137
+
138
+
139
+
140
+ # This method is used by Iodine to create the IO handler whenever a new connection is established.
141
+ #
142
+ # Normally you won't need to override this method.
143
+ def self.accept io
144
+ self.new(io)
145
+ end
146
+
147
+ protected
148
+
149
+ # This methos updates the timeout "watch", signifying the IO was active.
150
+ def touch
151
+ @last_active = Iodine.time
152
+ end
153
+ end
154
+
155
+ end
@@ -0,0 +1,98 @@
1
+ module Iodine
2
+ public
3
+
4
+ #######################
5
+ ## Settings - methods that change the way Iodine behaves should go here.
6
+
7
+ # Sets the logging object, which needs to act like `Logger`. The default logger is `Logger.new(STDOUT)`.
8
+ def logger= obj
9
+ @logger = obj
10
+ end
11
+
12
+ # Sets the number of threads in the thread pool used for executing the tasks. Defaults to 1 thread.
13
+ def threads= count
14
+ @thread_count = count
15
+ end
16
+
17
+ # Sets the server port. Defaults to the runtime `-p` argument, or the ENV['PORT'] or 3000 (in this order).
18
+ def port= port
19
+ @port = port
20
+ end
21
+ # Sets the IP binding address. Defaults to the runtime `-ip` argument, or the ENV['IP'] or 0.0.0.0 (in this order).
22
+ def bind= address
23
+ @bind = address
24
+ end
25
+
26
+ # Sets the Protocol the Iodine Server will use. Should be a child of Protocol or SSLProtocol. Defaults to nil (no server).
27
+ def protocol= protocol
28
+ @stop = protocol ? false : true
29
+ @protocol = protocol
30
+ end
31
+ # Returns the cutrently active Iodine protocol (if exists).
32
+ def protocol
33
+ @protocol
34
+ end
35
+
36
+ # Sets the SSL Context to be used when using an SSLProtocol. Defaults to a self signed certificate.
37
+ def ssl_context= context
38
+ @ssl_context = context
39
+ end
40
+ # Gets the SSL Context to be used when using an SSLProtocol.
41
+ def ssl_context
42
+ @ssl_context ||= init_ssl_context
43
+ end
44
+
45
+
46
+ protected
47
+
48
+ #initializes a default SSLContext
49
+ def init_ssl_context
50
+ ssl_context = OpenSSL::SSL::SSLContext.new
51
+ ssl_context.set_params verify_mode: OpenSSL::SSL::VERIFY_NONE
52
+ ssl_context.cert_store = OpenSSL::X509::Store.new
53
+ ssl_context.cert_store.set_default_paths
54
+ ssl_context.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL #SESSION_CACHE_OFF
55
+ ssl_context.cert, ssl_context.key = create_cert
56
+ ssl_context
57
+ end
58
+
59
+ #creates a self-signed certificate
60
+ def create_cert bits=2048, cn=nil, comment='a self signed certificate for when we only need encryption and no more.'
61
+ unless cn
62
+ host_name = Socket::gethostbyname(Socket::gethostname)[0].split('.')
63
+ cn = ''
64
+ host_name.each {|n| cn << "/DC=#{n}"}
65
+ cn << "/CN=Iodine.#{host_name.join('.')}"
66
+ end
67
+ # cn ||= "CN=#{Socket::gethostbyname(Socket::gethostname)[0] rescue Socket::gethostname}"
68
+
69
+ time = Time.now
70
+ rsa = OpenSSL::PKey::RSA.new(bits)
71
+ cert = OpenSSL::X509::Certificate.new
72
+ cert.version = 2
73
+ cert.serial = 1
74
+ name = OpenSSL::X509::Name.parse(cn)
75
+ cert.subject = name
76
+ cert.issuer = name
77
+ cert.not_before = time
78
+ cert.not_after = time + (365*24*60*60)
79
+ cert.public_key = rsa.public_key
80
+
81
+ ef = OpenSSL::X509::ExtensionFactory.new(nil,cert)
82
+ ef.issuer_certificate = cert
83
+ cert.extensions = [
84
+ ef.create_extension("basicConstraints","CA:FALSE"),
85
+ ef.create_extension("keyUsage", "keyEncipherment"),
86
+ ef.create_extension("subjectKeyIdentifier", "hash"),
87
+ ef.create_extension("extendedKeyUsage", "serverAuth"),
88
+ ef.create_extension("nsComment", comment),
89
+ ]
90
+ aki = ef.create_extension("authorityKeyIdentifier",
91
+ "keyid:always,issuer:always")
92
+ cert.add_extension(aki)
93
+ cert.sign(rsa, OpenSSL::Digest::SHA1.new)
94
+
95
+ return cert, rsa
96
+ end
97
+
98
+ end
@@ -0,0 +1,108 @@
1
+ module Iodine
2
+
3
+ # This class inherits from Protocol, but it includes some adjustments related to SSL connection handling.
4
+ class SSLProtocol < Protocol
5
+
6
+ # This is a mini-protocol used only to implement the SSL Handshake in a non-blocking manner,
7
+ # allowing for a hardcoded timeout (which you can monkey patch) of 3 seconds.
8
+ class SSLWait < Protocol
9
+ TIMEOUT = 3 # hardcoded SSL/TLS handshake timeout
10
+ def on_open
11
+ timeout = TIMEOUT
12
+ @ssl_socket = OpenSSL::SSL::SSLSocket.new(@io, Iodine.ssl_context)
13
+ @ssl_socket.sync_close = true
14
+ end
15
+
16
+ # atempt an SSL Handshale
17
+ def call
18
+ return if @locker.locked?
19
+ return unless @locker.try_lock
20
+ begin
21
+ @ssl_socket.accept_nonblock
22
+ @locker.unlock
23
+ rescue IO::WaitReadable, IO::WaitWritable
24
+ @locker.unlock
25
+ return
26
+ rescue OpenSSL::SSL::SSLError
27
+ @e = Exception.new "Self-signed Certificate?".freeze
28
+ @locker.unlock
29
+ return
30
+ rescue => e
31
+ Iodine.warn "SSL Handshake failed with: #{e.message}".freeze
32
+ @e = e
33
+ close
34
+ @locker.unlock
35
+ return
36
+ end
37
+ Iodine.protocol.new @ssl_socket
38
+ end
39
+ def on_close
40
+ # inform
41
+ Iodine.warn "SSL Handshake #{@e ? "failed with: #{@e.message} (#{@e.class.name})" : 'timed-out.'}".freeze
42
+ # the core @io is already closed, but let's make sure the SSL object is closed as well.
43
+ @ssl_socket.close unless @ssl_socket.closed?
44
+ end
45
+ end
46
+
47
+
48
+ attr_reader :ssl_socket
49
+
50
+
51
+
52
+ # reads from the IO up to the specified number of bytes (defaults to ~1Mb).
53
+ def read size = 1048576
54
+ @send_locker.synchronize do
55
+ data = ''
56
+ begin
57
+ (data << @ssl_socket.read_nonblock(size).to_s) until data.bytesize >= size
58
+ rescue OpenSSL::SSL::SSLErrorWaitReadable, IO::WaitReadable, IO::WaitWritable
59
+
60
+ rescue IOError
61
+ close
62
+ rescue => e
63
+ Iodine.warn "SSL Protocol read error: #{e.class.name} #{e.message} (closing connection)"
64
+ close
65
+ end
66
+ return false if data.to_s.empty?
67
+ touch
68
+ data
69
+ end
70
+ end
71
+
72
+ def write data
73
+ begin
74
+ @send_locker.synchronize do
75
+ r = @ssl_socket.write data
76
+ touch
77
+ r
78
+ end
79
+ rescue => e
80
+ close
81
+ false
82
+ end
83
+ end
84
+ alias :send :write
85
+ alias :<< :write
86
+
87
+ def on_close
88
+ @ssl_socket.close unless @ssl_socket.closed?
89
+ super
90
+ end
91
+
92
+
93
+
94
+
95
+
96
+ # This method initializes the SSL Protocol
97
+ def initialize ssl_socket
98
+ @ssl_socket = ssl_socket
99
+ super(ssl_socket.io)
100
+ end
101
+
102
+ def self.accept io
103
+ SSLWait.new(io)
104
+ end
105
+
106
+
107
+ end
108
+ end
@@ -0,0 +1,116 @@
1
+ module Iodine
2
+
3
+ #######################
4
+ ## Timers
5
+
6
+
7
+ # Every timed event is a member of the TimedEvent class and responds to it's methods.
8
+ class TimedEvent
9
+
10
+ # Sets/gets how often a timed event repeats, in seconds.
11
+ attr_accessor :interval
12
+ # Sets/gets how many times a timed event repeats.
13
+ # If set to false or -1, the timed event will repead until the application quits.
14
+ attr_accessor :repeat_limit
15
+
16
+ # Initialize a timed event.
17
+ def initialize reactor, interval, repeat_limit = -1, args=[], job=nil
18
+ @interval = interval
19
+ @repeat_limit = repeat_limit ? repeat_limit.to_i : -1
20
+ @job = job || (Proc.new { stop! })
21
+ @next = Iodine.time + interval
22
+ args << self
23
+ @args = args
24
+ end
25
+
26
+ # stops a timed event.
27
+ # @return [GReactor::TimedEvent] returns the TimedEvent object.
28
+ def stop!
29
+ @repeat_limit = 0
30
+ self
31
+ end
32
+
33
+ # Returns true if the timer is finished.
34
+ #
35
+ # If the timed event is due, this method will also add the event to the queue.
36
+ # @return [true, false]
37
+ def done?
38
+ return false unless @next <= Iodine.time
39
+ return true if @repeat_limit == 0
40
+ @repeat_limit -= 1 if @repeat_limit.to_i > 0
41
+ Iodine.queue @job, @args
42
+ @next = Iodine.time + @interval
43
+ @repeat_limit == 0
44
+ end
45
+ end
46
+
47
+ public
48
+
49
+ # pushes a timed event to the timers's stack
50
+ #
51
+ # accepts:
52
+ # seconds:: the minimal amount of seconds to wait before calling the handler's `call` method.
53
+ # *arg:: any arguments that will be passed to the handler's `call` method.
54
+ # &block:: the block to execute.
55
+ #
56
+ # A block is required.
57
+ #
58
+ # On top of the arguments passed to the `run_after` method, the timer object will be passed as the last agrument to the receiving block.
59
+ #
60
+ # Timed event's time of execution is dependant on the workload and continuous uptime of the process (timed events AREN'T persistent).
61
+ #
62
+ # @return [GReactor::TimedEvent] returns the new TimedEvent object.
63
+ def run_after seconds, *args, &block
64
+ timed_job seconds, 1, args, block
65
+ end
66
+
67
+ # pushes a timed event to the timers's stack
68
+ #
69
+ # accepts:
70
+ # time:: the time at which the job should be executed.
71
+ # *arg:: any arguments that will be passed to the handler's `call` method.
72
+ # &block:: the block to execute.
73
+ #
74
+ # A block is required.
75
+ #
76
+ # On top of the arguments passed to the `run_after` method, the timer object will be passed as the last agrument to the receiving block.
77
+ #
78
+ # Timed event's time of execution is dependant on the workload and continuous uptime of the process (timed events AREN'T persistent).
79
+ #
80
+ # @return [GReactor::TimedEvent] returns the new TimedEvent object.
81
+ def run_at run_time, *args, &block
82
+ timed_job( (@time - run_time), 1, args, block)
83
+ end
84
+ # pushes a repeated timed event to the timers's stack
85
+ #
86
+ # accepts:
87
+ # seconds:: the minimal amount of seconds to wait before calling the handler's `call` method.
88
+ # limit:: the amount of times the event should repeat itself. The event will repeat every x amount of `seconds`. The event will repeat forever if limit is set to false.
89
+ # *arg:: any arguments that will be passed to the handler's `call` method.
90
+ # &block:: the block to execute.
91
+ #
92
+ # A block is required.
93
+ #
94
+ # On top of the arguments passed to the `run_after` method, the timer object will be passed as the last agrument to the receiving block.
95
+ #
96
+ # Timed event's time of execution is dependant on the workload and continuous uptime of the process (timed events AREN'T persistent unless you save and reload them yourself).
97
+ #
98
+ # @return [GReactor::TimedEvent] returns the new TimedEvent object.
99
+ def run_every seconds, limit = -1, *args, &block
100
+ timed_job seconds, limit, args, block
101
+ end
102
+
103
+ protected
104
+ @timer_locker = Mutex.new
105
+ @timers = []
106
+
107
+ # Creates a TimedEvent object and adds it to the Timers stack.
108
+ def timed_job seconds, limit = false, args = [], block = nil
109
+ @timer_locker.synchronize {@timers << TimedEvent.new(self, seconds, limit, args, block); @timers.last}
110
+ end
111
+ # cycles through timed jobs, executing and/or deleting them if their time has come.
112
+ @check_timers = Proc.new do
113
+ @timer_locker.synchronize { @timers.delete_if {|t| t.done? } }
114
+ end
115
+
116
+ end
@@ -0,0 +1,3 @@
1
+ module Iodine
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: iodine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Boaz Segev
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-10-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: " IOdine is a super easy way to write network services and tasking scripts."
56
+ email:
57
+ - Boaz@2be.co.il
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/console
69
+ - bin/echo
70
+ - bin/em playground
71
+ - bin/http_test
72
+ - bin/setup
73
+ - iodine.gemspec
74
+ - lib/iodine.rb
75
+ - lib/iodine/core.rb
76
+ - lib/iodine/io.rb
77
+ - lib/iodine/logging.rb
78
+ - lib/iodine/protocol.rb
79
+ - lib/iodine/settings.rb
80
+ - lib/iodine/ssl_protocol.rb
81
+ - lib/iodine/timers.rb
82
+ - lib/iodine/version.rb
83
+ homepage: https://github.com/boazsegev/iodine
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ allowed_push_host: https://rubygems.org
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.4.5.1
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: IOdine makes writing evented server applications easy to write.
108
+ test_files: []