iodine 0.1.8 → 0.1.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 015d21e31dd393be87f10ecb19fe9d94f565255c
4
- data.tar.gz: 8919757b4d844e02b110496fff6d8e84dc87f60f
3
+ metadata.gz: 0b1ab6392f66ca513bb10c39e27ede18cf3588f8
4
+ data.tar.gz: 138fe6afa9327403a8811d036dccc5f3d91b66b9
5
5
  SHA512:
6
- metadata.gz: 6198a6eaa862ce000905693bad9afb670359ab20b80987713f95df11eefe8d1b951f243f761bac2546701eb10c215e5cea157f215eb3fd48f4bc229d50927e6a
7
- data.tar.gz: e96c344aad2a559a5af5b4f2f6365f876335ad3fc741f5cf212eede1002ec3a87751dc78f51d3b0228fdf16d729369f05cde90aaa573af521e25315fb5086ff4
6
+ metadata.gz: 79797e0676d69aa78131e911b65d88fd1dd65305dc39d1ee62f4aecbeab96ae4d80ee021d01d337e78a84ddce23ac8a3dcebc03a9934df88f9168e1b4b2bb4e6
7
+ data.tar.gz: 16a7dfe4bfc94c03718ab69c40f8ea207a7f5ea57c986b118da08eb7afc144554266cfe42a88760a555cc69ea65ddb16081e9830cba8c74e1b3ca8fd1a7946d3
@@ -8,6 +8,16 @@ Please notice that this change log contains changes for upcoming releases as wel
8
8
 
9
9
  ***
10
10
 
11
+ Change log v.0.1.9
12
+
13
+ **Fix**: WebsocketClient connection renewal will now keep the same WebsocketClient instance object.
14
+
15
+ **Update** Creating a TimedEvent before Iodine starts running will automatically 'nudge' Iodine into "Task polling" mode, cycling until the user signals a stop.
16
+
17
+ **Update**: repeatedly calling `Iodine.force_start!` will now be ignored, as might have been expected. Once Iodine had started, `force_start!` cannot be called until Iodine had finished (and even than, Iodine might never be as fresh nor as young as it was).
18
+
19
+ ***
20
+
11
21
  Change log v.0.1.8
12
22
 
13
23
  **Fix**: Websocket broadcasts are now correctly executed within the IO's mutex locker. This maintains the idea that only one thread at a time should be executing code on behald of any given Protocol object ("yes" to concurrency between objects but "no" to concurrency within objects).
data/README.md CHANGED
@@ -62,7 +62,7 @@ In this mode, Iodine will continue running until all the tasks have completed an
62
62
 
63
63
  This mode of operation is effective if want Iodine to periodically initiates new tasks, for instance if you cannot use `cron`.
64
64
 
65
- To initiate this mode, simply set: `Iodine.protocol = :timers`
65
+ To initiate this mode, simply set: `Iodine.protocol = :timers` OR create a TimedEvent.
66
66
 
67
67
  In example:
68
68
 
@@ -260,6 +260,8 @@ Iodine applies this concept when running in Server mode by locking the Protocol
260
260
 
261
261
  For instance, in Iodine's implementation for the Websocket protocol: Websocket messages to different connections can run concurrently, however multiple messages to the same connection are only executed one at a time, maintaining their order (lately a fix in version 0.1.8 made sure that also websocket broadcasting will be executed within the Protocol lock, preventing concurrency within the same connection).
262
262
 
263
+ The exception to the rule is the `ping` implementation. Your protocol's `ping` method will execute in parallel with other parts of your protocol's code. Pinging is a machanism that is often time sensitive and is required to maintain an open connection. For this reason, if your code is working hard on a long task, a ping will still occure automatically in the background and offset any connection timeout.
264
+
263
265
  ## Development
264
266
 
265
267
  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.
@@ -1,7 +1,17 @@
1
1
  require 'logger'
2
2
  require 'socket'
3
3
  require 'openssl'
4
- # require 'securerandom'
4
+
5
+ require "iodine/version"
6
+ require "iodine/settings"
7
+ require "iodine/logging"
8
+ require "iodine/core"
9
+ require "iodine/timers"
10
+ require "iodine/protocol"
11
+ require "iodine/ssl_connector"
12
+ require "iodine/io"
13
+ require "iodine/core_init"
14
+
5
15
 
6
16
 
7
17
  # Iodine is an easy Object-Oriented library for writing network applications (servers) with your own
@@ -57,13 +67,5 @@ module Iodine
57
67
  end
58
68
 
59
69
 
60
- require "iodine/version"
61
- require "iodine/settings"
62
- require "iodine/logging"
63
- require "iodine/core"
64
- require "iodine/timers"
65
- require "iodine/protocol"
66
- require "iodine/ssl_connector"
67
- require "iodine/io"
68
70
 
69
71
  # require 'iodine/http'
@@ -31,29 +31,26 @@ module Iodine
31
31
  nil
32
32
  end
33
33
 
34
- # forces Iodine to start prematurely and asyncronously. This might case Iodine to exit abruptly, depending how the hosting application behaves.
34
+ # forces Iodine to start prematurely and asyncronously. This might cause Iodine's exit sequence to end abruptly, depending how the hosting application behaves.
35
+ #
36
+ # calling this method repeatedly will be ignored unless Iodine's threads have all died.
37
+ #
38
+ # @return [Iodine] the method will allways return `self` (Iodine).
35
39
  def force_start!
36
- thread = Thread.new { startup true }
40
+ return self if @force_running
41
+ @force_running = true
42
+ thread = Thread.new do
43
+ startup true
44
+ @force_running = false
45
+ initialize_tasks
46
+ @stop = false if @protocol
47
+ end
37
48
  Kernel.at_exit {thread.raise("stop"); thread.join}
38
49
  self
39
50
  end
40
51
 
41
52
  protected
42
53
 
43
- @queue = Queue.new
44
- @shutdown_queue = Queue.new
45
- @stop = true
46
- @done = false
47
- @logger = Logger.new(STDOUT)
48
- @spawn_count = 1
49
- @thread_count = nil
50
- @ios = {}
51
- @io_in = Queue.new
52
- @io_out = Queue.new
53
- @shutdown_mutex = Mutex.new
54
- @servers = {}
55
-
56
-
57
54
  def cycle
58
55
  work until @stop && @queue.empty?
59
56
  @shutdown_mutex.synchronize { shutdown }
@@ -73,17 +70,18 @@ module Iodine
73
70
  end
74
71
 
75
72
  def startup use_rescue = false, hide_message = false
73
+ @force_running = true
76
74
  threads = []
77
75
  (@thread_count ||= 1).times { threads << Thread.new { cycle } }
78
76
  unless @stop
79
77
  if use_rescue
80
78
  sleep rescue true
81
79
  else
82
- old_int_trap = trap('INT') { throw :stop; trap('INT', old_int_trap) if old_int_trap }
83
- old_term_trap = trap('TERM') { throw :stop; trap('TERM', old_term_trap) if old_term_trap }
80
+ old_int_trap = trap('INT') { throw :stop; old_int_trap.respond_to?(:call) && old_int_trap.call }
81
+ old_term_trap = trap('TERM') { throw :stop; old_term_trap.respond_to?(:call) && old_term_trap.call }
84
82
  catch(:stop) { sleep }
85
83
  end
86
- log "\nShutting down Iodine. Setting shutdown timeout to 25 seconds.\n" unless hide_message
84
+ log "\nShutting down #{self == Iodine ? 'Iodine' : "#{self.name} (Iodine)"}. Setting shutdown timeout to 25 seconds.\n" unless hide_message
87
85
  @stop = true
88
86
  # setup exit timeout.
89
87
  threads.each {|t| Thread.new {sleep 25; t.kill; t.kill } }
@@ -91,10 +89,6 @@ module Iodine
91
89
  threads.each {|t| t.join rescue true }
92
90
  end
93
91
 
94
- Kernel.at_exit do
95
- startup
96
- end
97
-
98
92
  # performed once - the shutdown sequence.
99
93
  def shutdown
100
94
  return if @done
@@ -0,0 +1,143 @@
1
+ module Iodine
2
+ protected
3
+
4
+ def extended base
5
+ base.instance_exec do
6
+ initialize_core
7
+ initialize_io
8
+ initialize_timers
9
+ initialize_tasks
10
+ end
11
+ base.protocol = :cycle unless base == Iodine
12
+ end
13
+
14
+ def initialize_core
15
+ # initializes all the core components
16
+ # referenced in `core.rb`
17
+ @queue = Queue.new
18
+ @shutdown_queue = Queue.new
19
+ @stop = true
20
+ @done = false
21
+ @logger = Logger.new(STDOUT)
22
+ @spawn_count = 1
23
+ @thread_count = nil
24
+ @ios = {}
25
+ @io_in = Queue.new
26
+ @io_out = Queue.new
27
+ @shutdown_mutex = Mutex.new
28
+ @servers = {}
29
+ Kernel.at_exit do
30
+ if self == Iodine
31
+ startup
32
+ else
33
+ Iodine.protocol ||= :cycle if @timers.any? || @protocol
34
+ thread = Thread.new { startup true }
35
+ Iodine.on_shutdown { thread.raise 'stop' ; thread.join }
36
+ end
37
+ end
38
+ end
39
+
40
+ def initialize_io
41
+ # initializes all the IO components
42
+ # referenced in `io.rb`
43
+ @port = ((ARGV.index('-p') && ARGV[ARGV.index('-p') + 1]) || ENV['PORT'] || 3000).to_i
44
+ @bind = (ARGV.index('-ip') && ARGV[ARGV.index('-ip') + 1]) || ENV['IP'] || "0.0.0.0"
45
+ @ssl = (ARGV.index('ssl') && true) || (@port == 443)
46
+ @protocol = nil
47
+ @ssl_context = nil
48
+ @ssl_protocols = {}
49
+ @time = Time.now
50
+
51
+ @timeout_proc = Proc.new {|prot| prot.timeout?(@time) }
52
+ @status_loop = Proc.new {|io| @io_out << io if io.closed? || !(io.stat.readable? rescue false) }
53
+ @close_callback = Proc.new {|prot| prot.on_close if prot }
54
+ @reactor = [ (Proc.new do
55
+ if @queue.empty?
56
+ #clear any closed IO objects.
57
+ @time = Time.now
58
+ @ios.keys.each &@status_loop
59
+ @ios.values.each &@timeout_proc
60
+ until @io_in.empty?
61
+ n_io = @io_in.pop
62
+ @ios[n_io[0]] = n_io[1]
63
+ end
64
+ until @io_out.empty?
65
+ o_io = @io_out.pop
66
+ o_io.close unless o_io.closed?
67
+ run @ios.delete(o_io), &@close_callback
68
+ end
69
+ # react to IO events
70
+ begin
71
+ r = IO.select(@ios.keys, nil, nil, 0.15)
72
+ r[0].each {|io| @queue << [@ios[io]] } if r
73
+ rescue => e
74
+
75
+ end
76
+ unless @stop && @queue.empty?
77
+ # @ios.values.each &@timeout_loop
78
+ @check_timers && (@queue << @check_timers)
79
+ @queue << @reactor
80
+ end
81
+ else
82
+ @queue << @reactor
83
+ end
84
+ end )]
85
+ end
86
+
87
+ def initialize_timers
88
+ @timer_locker = Mutex.new
89
+ @timers = []
90
+ # cycles through timed jobs, executing and/or deleting them if their time has come.
91
+ @check_timers = [(Proc.new do
92
+ @timer_locker.synchronize { @timers.delete_if {|t| t.done? } }
93
+ end)]
94
+ end
95
+
96
+ def initialize_tasks
97
+ # initializes actions to be taken when starting to run
98
+ run do
99
+ @protocol ||= :cycle if @timers.any?
100
+ next unless @protocol
101
+ if @protocol.is_a?( ::Class ) && @protocol.ancestors.include?( ::Iodine::Protocol )
102
+ begin
103
+ @server = ::TCPServer.new(@bind, @port)
104
+ rescue => e
105
+ fatal e.message
106
+ fatal "Running existing tasks and exiting."
107
+ @queue << @reactor
108
+ Process.kill("INT", 0)
109
+ next
110
+ end
111
+ on_shutdown do
112
+ log "Stopping to listen on port #{@port} and shutting down.\n"
113
+ @server.close unless @server.closed?
114
+ end
115
+ ::Iodine::Base::Listener.accept(@server, false)
116
+ log "Iodine #{VERSION} is listening on port #{@port}#{ ' to SSL/TLS connections.' if @ssl}\n"
117
+ if @spawn_count && @spawn_count.to_i > 1 && Process.respond_to?(:fork)
118
+ log "Server will run using #{@spawn_count.to_i} processes - Spawning #{@spawn_count.to_i - 1 } more processes.\n"
119
+ (@spawn_count.to_i - 1).times do
120
+ Process.fork do
121
+ log "Spawned process: #{Process.pid}.\n"
122
+ on_shutdown { log "Shutting down process #{Process.pid}.\n" }
123
+ @queue.clear
124
+ @queue << @reactor
125
+ startup false, true
126
+ end
127
+ end
128
+
129
+ end
130
+ log "Press ^C to stop the server.\n"
131
+ else
132
+ log "#{self == Iodine ? 'Iodine' : "#{self.name} (Iodine)"} #{VERSION} is running.\n"
133
+ log "Press ^C to stop the cycling.\n"
134
+ end
135
+ on_shutdown do
136
+ shut_down_proc = Proc.new {|prot| prot.on_shutdown ; prot.close }
137
+ @ios.values.each {|p| run p, &shut_down_proc }
138
+ @queue << @reactor
139
+ end
140
+ @queue << @reactor
141
+ end
142
+ end
143
+ end
@@ -6,10 +6,7 @@ module Iodine
6
6
  module_function
7
7
  def run(app, options = {})
8
8
  @app = app
9
-
10
- puts "reading threads = #{Iodine.threads.to_s}"
11
-
12
- Iodine.threads = 18 unless Iodine.threads
9
+ Iodine.threads ||= 18
13
10
  Iodine.port = options[:Port]
14
11
  Iodine.protocol ||= Iodine::Http::Http1
15
12
  @pre_rack_handler = Iodine::Http.on_http unless Iodine::Http.on_http == Iodine::Http::NOT_IMPLEMENTED
@@ -10,15 +10,19 @@ module Iodine
10
10
 
11
11
  attr_accessor :response, :request, :params
12
12
 
13
- def initialize request
13
+ def initialize options
14
14
  @response = nil
15
- @request = request
16
- @params = request[:ws_client_params]
17
- @on_message = @params[:on_message]
15
+ @options = options
16
+ @on_message = @options[:on_message]
18
17
  raise "Websocket client must have an #on_message Proc or handler." unless @on_message && @on_message.respond_to?(:call)
19
- @on_open = @params[:on_open]
20
- @on_close = @params[:on_close]
21
- @renew = @params[:renew].to_i
18
+ @on_open = @options[:on_open]
19
+ @on_close = @options[:on_close]
20
+ @on_error = @options[:on_error]
21
+ @renew = @options[:renew].to_i
22
+ @options[:url] = URI.parse(@options[:url]) unless @options[:url].is_a?(URI)
23
+ @connection_lock = Mutex.new
24
+ raise TypeError, "Websocket Client `:send` should be either a String or a Proc object." if @options[:send] && !(@options[:send].is_a?(String) || @options[:send].is_a?(Proc))
25
+ on_close && (@io || raise("Connection error, cannot create websocket client")) unless connect
22
26
  end
23
27
 
24
28
  def on event_name, &block
@@ -39,17 +43,25 @@ module Iodine
39
43
  @on_message = block if block
40
44
  return @on_message
41
45
  end
42
- instance_exec( data, &@on_message)
46
+ begin
47
+ instance_exec( data, &@on_message)
48
+ rescue => e
49
+ @on_error ? @on_error.call(e) : raise(e)
50
+ end
43
51
  end
44
52
 
45
53
  def on_open
46
54
  raise 'The on_open even is invalid at this point.' if block_given?
55
+ @renew = @options[:renew].to_i
47
56
  @io = @request[:io]
48
57
  Iodine::Http::Request.parse @request
49
- instance_exec(&@on_open) if @on_open
50
- if request[:ws_client_params][:every] && @params[:send]
51
- raise TypeError, "Websocket Client `:send` should be either a String or a Proc object." unless @params[:send].is_a?(String) || @params[:send].is_a?(Proc)
52
- Iodine.run_every @params[:every], self, @params do |ws, client_params, timer|
58
+ begin
59
+ instance_exec(&@on_open) if @on_open
60
+ rescue => e
61
+ @on_error ? @on_error.call(e) : raise(e)
62
+ end
63
+ if @options[:every] && @options[:send]
64
+ Iodine.run_every @options[:every], self, @options do |ws, client_params, timer|
53
65
  if ws.closed?
54
66
  timer.stop!
55
67
  next
@@ -57,7 +69,11 @@ module Iodine
57
69
  if client_params[:send].is_a?(String)
58
70
  ws.write client_params[:send]
59
71
  elsif client_params[:send].is_a?(Proc)
60
- ws.instance_exec(&client_params[:send])
72
+ begin
73
+ ws.instance_exec(&client_params[:send])
74
+ rescue => e
75
+ @on_error ? @on_error.call(e) : raise(e)
76
+ end
61
77
  end
62
78
  end
63
79
  end
@@ -67,25 +83,35 @@ module Iodine
67
83
  return @on_close = block if block
68
84
  if @renew > 0
69
85
  renew_proc = Proc.new do
86
+ @io = nil
70
87
  begin
71
- Iodine::Http::WebsocketClient.connect(@params[:url], @params)
88
+ raise unless connect
72
89
  rescue
73
90
  @renew -= 1
74
91
  if @renew <= 0
75
- Iodine.fatal "WebsocketClient renewal FAILED for #{@params[:url]}"
76
- instance_exec(&@on_close) if @on_close
92
+ Iodine.fatal "WebsocketClient renewal FAILED for #{@options[:url]}"
93
+ on_close
77
94
  else
78
- Iodine.run_after 2, &renew_proc
79
- Iodine.warn "WebsocketClient renewal failed for #{@params[:url]}, #{@renew} attempts left"
95
+ Iodine.warn "WebsocketClient renewal failed for #{@options[:url]}, #{@renew} attempts left"
96
+ renew_proc.call
80
97
  end
81
98
  false
82
99
  end
83
100
  end
84
- renew_proc.call
101
+ @connection_lock.synchronize { renew_proc.call }
85
102
  else
86
- instance_exec(&@on_close) if @on_close
103
+ begin
104
+ instance_exec(&@on_close) if @on_close
105
+ rescue => e
106
+ @on_error ? @on_error.call(e) : raise(e)
107
+ end
87
108
  end
88
109
  end
110
+ def on_error(error = nil, &block)
111
+ return @on_error = block if block
112
+ instance_exec(error, &@on_error) if @on_error
113
+ on_close unless @io # if the connection was initialized, :on_close will be called by Iodine
114
+ end
89
115
 
90
116
  def on_shutdown
91
117
  @renew = 0
@@ -120,7 +146,6 @@ module Iodine
120
146
  # @return [true, false] Returns the true if the data was actually sent or nil if no data was sent.
121
147
  def write data, op_code = nil, fin = true, ext = 0
122
148
  return false if !data || data.empty?
123
- return false if @io.closed?
124
149
  data = data.dup # needed?
125
150
  unless op_code # apply extenetions to the message as a whole
126
151
  op_code = (data.encoding == ::Encoding::UTF_8 ? 1 : 2)
@@ -129,8 +154,8 @@ module Iodine
129
154
  byte_size = data.bytesize
130
155
  if byte_size > (::Iodine::Http::Websockets::FRAME_SIZE_LIMIT+2)
131
156
  sections = byte_size/FRAME_SIZE_LIMIT + (byte_size % ::Iodine::Http::Websockets::FRAME_SIZE_LIMIT ? 1 : 0)
132
- send_data( data.slice!( 0...::Iodine::Http::Websockets::FRAME_SIZE_LIMIT ), op_code, data.empty?, ext) && (ext = op_code = 0) until data.empty?
133
- return true # avoid sending an empty frame.
157
+ ret = write( data.slice!( 0...::Iodine::Http::Websockets::FRAME_SIZE_LIMIT ), op_code, data.empty?, ext) && (ext = op_code = 0) until data.empty?
158
+ return ret # avoid sending an empty frame.
134
159
  end
135
160
  # @ws_extentions.each { |ex| ext |= ex.edit_frame data } if @ws_extentions
136
161
  header = ( (fin ? 0b10000000 : 0) | (op_code & 0b00001111) | ext).chr.force_encoding(::Encoding::ASCII_8BIT)
@@ -147,74 +172,22 @@ module Iodine
147
172
  @@make_mask_proc ||= Proc.new {Random.rand(251) + 1}
148
173
  mask = Array.new(4, &(@@make_mask_proc))
149
174
  header << mask.pack('C*'.freeze)
150
- @io.write header
151
- i = -1;
152
- @io.write(data.bytes.map! {|b| (b ^ mask[i = (i + 1)%4]) } .pack('C*'.freeze)) && true
175
+ @connection_lock.synchronize do
176
+ return false if @io.nil? || @io.closed?
177
+ @io.write header
178
+ i = -1;
179
+ @io.write(data.bytes.map! {|b| (b ^ mask[i = (i + 1)%4]) } .pack('C*'.freeze)) && true
180
+ end
153
181
  end
154
182
  alias :<< :write
155
183
 
156
- # Create a simple Websocket Client(!).
157
- #
158
- # This method accepts two parameters:
159
- # url:: a String representing the URL of the websocket. i.e.: 'ws://foo.bar.com:80/ws/path'
160
- # options:: a Hash with options to be used. The options will be used to define the connection's details (i.e. ssl etc') and the Websocket callbacks (i.e. on_open(ws), on_close(ws), on_message(ws))
161
- # &block:: an optional block that accepts one parameter (data) and will be used as the `#on_message(data)`
162
- #
163
- # Acceptable options are:
164
- # on_open:: the on_open callback. Must be an objects that answers `call(ws)`, usually a Proc.
165
- # on_message:: the on_message callback. Must be an objects that answers `call(ws)`, usually a Proc.
166
- # on_close:: the on_close callback - this will ONLY be called if the connection WASN'T renewed. Must be an objects that answers `call(ws)`, usually a Proc.
167
- # headers:: a Hash of custom HTTP headers to be sent with the request. Header data, including cookie headers, should be correctly encoded.
168
- # cookies:: a Hash of cookies to be sent with the request. cookie data will be encoded before being sent.
169
- # timeout:: the number of seconds to wait before the connection is established. Defaults to 5 seconds.
170
- # every:: this option, together with `:send` and `:renew`, implements a polling websocket. :every is the number of seconds between each polling event. without `:send`, this option will be ignored. defaults to nil.
171
- # send:: a String to be sent or a Proc to be performed each polling interval. This option, together with `:every` and `:renew`, implements a polling websocket. without `:every`, this option will be ignored. defaults to nil. If `:send` is a Proc, it will be executed within the context of the websocket client object, with acess to the websocket client's instance variables and methods.
172
- # renew:: the number of times to attempt to renew the connection if the connection is terminated by the remote server. Attempts are made in 2 seconds interval. The default for a polling websocket is 5 attempts to renew. For all other clients, the default is 0 (no renewal).
173
- #
174
- # The method will block until the connection is established or until 5 seconds have passed (the timeout). The method will either return a WebsocketClient instance object or raise an exception it the connection was unsuccessful.
175
- #
176
- # Use Iodine::Http.ws_connect for a non-blocking initialization.
177
- #
178
- # An #on_close callback will only be called if the connection isn't or cannot be renewed. If the connection is renewed,
179
- # the #on_open callback will be called again for a new Websocket client instance - but the #on_close callback will NOT be called.
180
- #
181
- # Due to this design, the #on_open and #on_close methods should NOT be used for opening IO resources (i.e. file handles) nor for cleanup IF the `:renew` option is enabled.
182
- #
183
- # An on_message Proc must be defined, or the method will fail.
184
- #
185
- # The on_message Proc can be defined using the optional block:
186
- #
187
- # Iodine::Http::WebsocketClient.connect("ws://localhost:3000/") {|data| write data} #echo example
188
- #
189
- # OR, the on_message Proc can be defined using the options Hash:
190
- #
191
- # Iodine::Http::WebsocketClient.connect("ws://localhost:3000/", on_open: -> {}, on_message: -> {|data| write data })
192
- #
193
- # The #on_message(data), #on_open and #on_close methods will be executed within the context of the WebsocketClient
194
- # object, and will have native acess to the Websocket response object.
195
- #
196
- # After the WebsocketClient had been created, it's possible to update the #on_message and #on_close methods:
197
- #
198
- # # updates #on_message
199
- # wsclient.on_message do |data|
200
- # response << "I'll disconnect on the next message!"
201
- # # updates #on_message again.
202
- # on_message {|data| disconnect }
203
- # end
204
- #
205
- #
206
- # !!please be aware that the Websockt Client will not attempt to verify SSL certificates,
207
- # so that even SSL connections are vulnerable to a possible man in the middle attack.
208
- #
209
- # @return [Iodine::Http::WebsocketClient] this method returns the connected {Iodine::Http::WebsocketClient} or raises an exception if something went wrong (such as a connection timeout).
210
- def self.connect url, options={}, &block
184
+ protected
185
+
186
+ def connect
187
+ return false if @io && !@io.closed?
211
188
  socket = nil
212
- options = options.dup
213
- options[:on_message] ||= block
214
- raise "No #on_message handler defined! please pass a block or define an #on_message handler!" unless options[:on_message]
215
- url = URI.parse(url) unless url.is_a?(URI)
216
- options[:url] = url
217
- options[:renew] ||= 5 if options[:every] && options[:send]
189
+ url = @options[:url]
190
+ @options[:renew] ||= 5 if @options[:every] && @options[:send]
218
191
 
219
192
  ssl = url.scheme == "https" || url.scheme == "wss"
220
193
 
@@ -225,33 +198,32 @@ module Iodine
225
198
  context = OpenSSL::SSL::SSLContext.new
226
199
  context.cert_store = OpenSSL::X509::Store.new
227
200
  context.cert_store.set_default_paths
228
- context.set_params verify_mode: (options[:verify_mode] || OpenSSL::SSL::VERIFY_NONE) # OpenSSL::SSL::VERIFY_PEER #OpenSSL::SSL::VERIFY_NONE
201
+ context.set_params verify_mode: (@options[:verify_mode] || OpenSSL::SSL::VERIFY_NONE) # OpenSSL::SSL::VERIFY_PEER #OpenSSL::SSL::VERIFY_NONE
229
202
  ssl = OpenSSL::SSL::SSLSocket.new(socket, context)
230
203
  ssl.sync_close = true
231
204
  ssl.connect
232
205
  end
233
206
  # prep custom headers
234
207
  custom_headers = ''
235
- custom_headers = options[:headers] if options[:headers].is_a?(String)
236
- options[:headers].each {|k, v| custom_headers << "#{k.to_s}: #{v.to_s}\r\n"} if options[:headers].is_a?(Hash)
237
- options[:cookies].each {|k, v| raise 'Illegal cookie name' if k.to_s.match(/[\x00-\x20\(\)<>@,;:\\\"\/\[\]\?\=\{\}\s]/); custom_headers << "Cookie: #{ k }=#{ Iodine::Http::Request.encode_url v }\r\n"} if options[:cookies].is_a?(Hash)
208
+ custom_headers = @options[:headers] if @options[:headers].is_a?(String)
209
+ @options[:headers].each {|k, v| custom_headers << "#{k.to_s}: #{v.to_s}\r\n"} if @options[:headers].is_a?(Hash)
210
+ @options[:cookies].each {|k, v| raise 'Illegal cookie name' if k.to_s.match(/[\x00-\x20\(\)<>@,;:\\\"\/\[\]\?\=\{\}\s]/); custom_headers << "Cookie: #{ k }=#{ Iodine::Http::Request.encode_url v }\r\n"} if @options[:cookies].is_a?(Hash)
238
211
 
239
212
  # send protocol upgrade request
240
213
  websocket_key = [(Array.new(16) {rand 255} .pack 'c*' )].pack('m0*')
241
- (ssl || socket).write "GET #{url.path}#{url.query.to_s.empty? ? '' : ('?' + url.query)} HTTP/1.1\r\nHost: #{url.host}#{url.port ? (':'+url.port.to_s) : ''}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nOrigin: #{options[:ssl_client] ? 'https' : 'http'}://#{url.host}\r\nSec-WebSocket-Key: #{websocket_key}\r\nSec-WebSocket-Version: 13\r\n#{custom_headers}\r\n"
214
+ (ssl || socket).write "GET #{url.path}#{url.query.to_s.empty? ? '' : ('?' + url.query)} HTTP/1.1\r\nHost: #{url.host}#{url.port ? (':'+url.port.to_s) : ''}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nOrigin: #{ssl ? 'https' : 'http'}://#{url.host}\r\nSec-WebSocket-Key: #{websocket_key}\r\nSec-WebSocket-Version: 13\r\n#{custom_headers}\r\n"
242
215
  # wait for answer - make sure we don't over-read
243
216
  # (a websocket message might be sent immidiately after connection is established)
244
217
  reply = ''
245
218
  reply.force_encoding(::Encoding::ASCII_8BIT)
246
- stop_time = Time.now + (options[:timeout] || 5)
219
+ stop_time = Time.now + (@options[:timeout] || 5)
247
220
  stop_reply = "\r\n\r\n"
248
- sleep 0.2
249
221
  until reply[-4..-1] == stop_reply
250
222
  begin
251
223
  reply << ( ssl ? ssl.read_nonblock(1) : socket.recv_nonblock(1) )
252
- rescue Errno::EWOULDBLOCK => e
224
+ rescue Errno::EWOULDBLOCK, OpenSSL::SSL::SSLErrorWaitReadable => e
253
225
  raise "Websocket client handshake timed out (HTTP reply not recieved)\n\n Got Only: #{reply}" if Time.now >= stop_time
254
- IO.select [socket], nil, nil, (options[:timeout] || 5)
226
+ IO.select [socket], nil, nil, (@options[:timeout] || 5)
255
227
  retry
256
228
  end
257
229
  raise "Connection failed" if socket.closed?
@@ -260,22 +232,22 @@ module Iodine
260
232
  raise "Connection Refused. Reply was:\r\n #{reply}" unless reply.lines[0].match(/^HTTP\/[\d\.]+ 101/i)
261
233
  raise 'Websocket Key Authentication failed.' unless reply.match(/^Sec-WebSocket-Accept:[\s]*([^\s]*)/i) && reply.match(/^Sec-WebSocket-Accept:[\s]*([^\s]*)/i)[1] == Digest::SHA1.base64digest(websocket_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
262
234
  # read the body's data and parse any incoming data.
263
- request = Iodine::Http::Request.new
264
- request[:method] = 'GET'
265
- request['host'] = "#{url.host}:#{url.port}"
266
- request[:query] = url.path
267
- request[:version] = '1.1'
235
+ @request = Iodine::Http::Request.new
236
+ @request[:method] = 'GET'
237
+ @request['host'] = "#{url.host}:#{url.port}"
238
+ @request[:query] = url.path
239
+ @request[:version] = '1.1'
268
240
  reply = StringIO.new reply
269
241
  reply.gets
270
242
 
271
243
  until reply.eof?
272
- until request[:headers_complete] || (l = reply.gets).nil?
244
+ until @request[:headers_complete] || (l = reply.gets).nil?
273
245
  if l.include? ':'
274
246
  l = l.strip.split(/:[\s]?/, 2)
275
247
  l[0].strip! ; l[0].downcase!;
276
- request[l[0]] ? (request[l[0]].is_a?(Array) ? (request[l[0]] << l[1]) : request[l[0]] = [request[l[0]], l[1] ]) : (request[l[0]] = l[1])
248
+ @request[l[0]] ? (@request[l[0]].is_a?(Array) ? (@request[l[0]] << l[1]) : @request[l[0]] = [@request[l[0]], l[1] ]) : (@request[l[0]] = l[1])
277
249
  elsif l =~ /^[\r]?\n/
278
- request[:headers_complete] = true
250
+ @request[:headers_complete] = true
279
251
  else
280
252
  #protocol error
281
253
  raise 'Protocol Error, closing connection.'
@@ -285,15 +257,80 @@ module Iodine
285
257
  end
286
258
  reply.string.clear
287
259
 
288
- request[:ws_client_params] = options
289
- client = self.new(request)
290
- Iodine::Http::Websockets.new( ( ssl || socket), handler: client, request: request )
260
+ return Iodine::Http::Websockets.new( ( ssl || socket), handler: self, request: @request )
291
261
 
292
- return client
262
+ rescue => e
263
+ (ssl || socket).tap {|io| next if io.nil?; io.close unless io.closed?}
264
+ if @options[:on_error]
265
+ @options[:on_error].call(e)
266
+ return false
267
+ end
268
+ raise e unless @io
269
+ end
293
270
 
294
- rescue => e
295
- (ssl || socket).tap {|io| next if io.nil?; io.close unless io.closed?}
296
- raise e
271
+ # Create a simple Websocket Client(!).
272
+ #
273
+ # This method accepts two parameters:
274
+ # url:: a String representing the URL of the websocket. i.e.: 'ws://foo.bar.com:80/ws/path'
275
+ # options:: a Hash with options to be used. The options will be used to define the connection's details (i.e. ssl etc') and the Websocket callbacks (i.e. on_open(ws), on_close(ws), on_message(ws))
276
+ # &block:: an optional block that accepts one parameter (data) and will be used as the `#on_message(data)`
277
+ #
278
+ # Acceptable options are:
279
+ # on_open:: the on_open callback - Must be an objects that answers `call()`, usually a Proc.
280
+ # on_message:: the on_message callback - Must be an objects that answers `call(data)`, usually a Proc.
281
+ # on_close:: the on_close callback - Must be an objects that answers `call()`, usually a Proc. The method is called when the connection is closed and isn't renewed.
282
+ # on_error:: the on_error callback - Must be an objects that answers `call(err)`, usually a Proc. This is called whenever a connection fails to be established or an exception is raised by any of the callbacks. This is NOT the disconnection websocket message. dafaults to raising the error (error pass-through).
283
+ # headers:: a Hash of custom HTTP headers to be sent with the request. Header data, including cookie headers, should be correctly encoded.
284
+ # cookies:: a Hash of cookies to be sent with the request. cookie data will be encoded before being sent.
285
+ # timeout:: the number of seconds to wait before the connection is established. Defaults to 5 seconds.
286
+ # every:: this option, together with `:send` and `:renew`, implements a polling websocket. :every is the number of seconds between each polling event. without `:send`, this option will be ignored. defaults to nil.
287
+ # send:: a String to be sent or a Proc to be performed each polling interval. This option, together with `:every` and `:renew`, implements a polling websocket. without `:every`, this option will be ignored. defaults to nil. If `:send` is a Proc, it will be executed within the context of the websocket client object, with acess to the websocket client's instance variables and methods.
288
+ # renew:: the number of times to attempt to renew the connection if the connection is terminated by the remote server. Attempts are made in 2 seconds interval. The default for a polling websocket is 5 attempts to renew. For all other clients, the default is 0 (no renewal).
289
+ #
290
+ # The method will block until the connection is established or until 5 seconds have passed (the timeout). The method will either return a WebsocketClient instance object or raise an exception it the connection was unsuccessful.
291
+ #
292
+ # Use Iodine::Http.ws_connect for a non-blocking initialization.
293
+ #
294
+ # An #on_close callback will only be called if the connection isn't or cannot be renewed. If the connection is renewed,
295
+ # the #on_open callback will be called again for a new Websocket client instance - but the #on_close callback will NOT be called.
296
+ #
297
+ # Due to this design, the #on_open and #on_close methods should NOT be used for opening IO resources (i.e. file handles) nor for cleanup IF the `:renew` option is enabled.
298
+ #
299
+ # An on_message Proc must be defined, or the method will fail.
300
+ #
301
+ # The on_message Proc can be defined using the optional block:
302
+ #
303
+ # Iodine::Http::WebsocketClient.connect("ws://localhost:3000/") {|data| write data} #echo example
304
+ #
305
+ # OR, the on_message Proc can be defined using the options Hash:
306
+ #
307
+ # Iodine::Http::WebsocketClient.connect("ws://localhost:3000/", on_open: -> {}, on_message: -> {|data| write data })
308
+ #
309
+ # The #on_message(data), #on_open and #on_close methods will be executed within the context of the WebsocketClient
310
+ # object, and will have native acess to the Websocket response object.
311
+ #
312
+ # After the WebsocketClient had been created, it's possible to update the #on_message and #on_close methods:
313
+ #
314
+ # # updates #on_message
315
+ # wsclient.on_message do |data|
316
+ # response << "I'll disconnect on the next message!"
317
+ # # updates #on_message again.
318
+ # on_message {|data| disconnect }
319
+ # end
320
+ #
321
+ #
322
+ # !!please be aware that the Websockt Client will not attempt to verify SSL certificates,
323
+ # so that even SSL connections are vulnerable to a possible man in the middle attack.
324
+ #
325
+ # @return [Iodine::Http::WebsocketClient] this method returns the connected {Iodine::Http::WebsocketClient} or raises an exception if something went wrong (such as a connection timeout).
326
+ def self.connect url, options={}, &block
327
+ options = url if url.is_a?(Hash) && options.empty?
328
+ options[:renew] ||= 5 if options[:every] && options[:send]
329
+ options[:url] ||= url
330
+ options[:on_message] ||= block
331
+ client = self.new(options)
332
+ return client unless client.closed?
333
+ false
297
334
  end
298
335
  end
299
336
  end
@@ -26,48 +26,6 @@ module Iodine
26
26
 
27
27
  protected
28
28
 
29
- @port = ((ARGV.index('-p') && ARGV[ARGV.index('-p') + 1]) || ENV['PORT'] || 3000).to_i
30
- @bind = (ARGV.index('-ip') && ARGV[ARGV.index('-ip') + 1]) || ENV['IP'] || "0.0.0.0"
31
- @ssl = (ARGV.index('ssl') && true) || (@port == 443)
32
- @protocol = nil
33
- @ssl_context = nil
34
- @ssl_protocols = {}
35
- @time = Time.now
36
-
37
- @timeout_proc = Proc.new {|prot| prot.timeout?(@time) }
38
- @status_loop = Proc.new {|io| @io_out << io if io.closed? || !(io.stat.readable? rescue false) }
39
- @close_callback = Proc.new {|prot| prot.on_close if prot }
40
- REACTOR = [ (Proc.new do
41
- if @queue.empty?
42
- #clear any closed IO objects.
43
- @time = Time.now
44
- @ios.keys.each &@status_loop
45
- @ios.values.each &@timeout_proc
46
- until @io_in.empty?
47
- n_io = @io_in.pop
48
- @ios[n_io[0]] = n_io[1]
49
- end
50
- until @io_out.empty?
51
- o_io = @io_out.pop
52
- o_io.close unless o_io.closed?
53
- run @ios.delete(o_io), &@close_callback
54
- end
55
- # react to IO events
56
- begin
57
- r = IO.select(@ios.keys, nil, nil, 0.15)
58
- r[0].each {|io| @queue << [@ios[io]] } if r
59
- rescue => e
60
-
61
- end
62
- unless @stop && @queue.empty?
63
- # @ios.values.each &@timeout_loop
64
- @check_timers && (@queue << @check_timers)
65
- @queue << REACTOR
66
- end
67
- else
68
- @queue << REACTOR
69
- end
70
- end )]
71
29
  # internal helper methods and classes.
72
30
  module Base
73
31
  # the server listener Protocol.
@@ -95,50 +53,4 @@ module Iodine
95
53
  end
96
54
  end
97
55
  end
98
-
99
- ########
100
- ## remember to set traps (once) when 'listen' is called.
101
- run do
102
- next unless @protocol
103
- if @protocol.is_a?( ::Class ) && @protocol.ancestors.include?( ::Iodine::Protocol )
104
- begin
105
- @server = ::TCPServer.new(@bind, @port)
106
- rescue => e
107
- fatal e.message
108
- fatal "Running existing tasks and exiting."
109
- @queue << REACTOR
110
- Process.kill("INT", 0)
111
- next
112
- end
113
- on_shutdown do
114
- log "Stopping to listen on port #{@port} and shutting down.\n"
115
- @server.close unless @server.closed?
116
- end
117
- ::Iodine::Base::Listener.accept(@server, false)
118
- log "Iodine #{VERSION} is listening on port #{@port}#{ ' to SSL/TLS connections.' if @ssl}\n"
119
- if @spawn_count && @spawn_count.to_i > 1 && Process.respond_to?(:fork)
120
- log "Server will run using #{@spawn_count.to_i} processes - Spawning #{@spawn_count.to_i - 1 } more processes.\n"
121
- (@spawn_count.to_i - 1).times do
122
- Process.fork do
123
- log "Spawned process: #{Process.pid}.\n"
124
- on_shutdown { log "Shutting down process #{Process.pid}.\n" }
125
- @queue.clear
126
- @queue << REACTOR
127
- startup false, true
128
- end
129
- end
130
-
131
- end
132
- log "Press ^C to stop the server.\n"
133
- else
134
- log "Iodine #{VERSION} is running.\n"
135
- log "Press ^C to stop the cycling.\n"
136
- end
137
- on_shutdown do
138
- shut_down_proc = Proc.new {|prot| prot.on_shutdown ; prot.close }
139
- @ios.values.each {|p| run p, &shut_down_proc }
140
- @queue << REACTOR
141
- end
142
- @queue << REACTOR
143
- end
144
56
  end
@@ -3,8 +3,9 @@ module Iodine
3
3
  # This is a mini-protocol used only to implement the SSL Handshake in a non-blocking manner,
4
4
  # allowing for a hardcoded timeout (which you can monkey patch) of 3 seconds.
5
5
  class SSLConnector < Protocol
6
- def initialize io, protocol
6
+ def initialize io, protocol, options = nil
7
7
  @protocol = protocol
8
+ @options = options
8
9
  super(io)
9
10
  end
10
11
  TIMEOUT = 3 # hardcoded SSL/TLS handshake timeout
@@ -34,7 +35,7 @@ module Iodine
34
35
  ensure
35
36
  @locker.unlock
36
37
  end
37
- ( (@ssl_socket.npn_protocol && ::Iodine.ssl_protocols[@ssl_socket.npn_protocol]) || @protocol).new @ssl_socket
38
+ ( (@ssl_socket.npn_protocol && ::Iodine.ssl_protocols[@ssl_socket.npn_protocol]) || @protocol).new @ssl_socket, @options
38
39
  end
39
40
  def on_close
40
41
  # inform
@@ -17,10 +17,11 @@ module Iodine
17
17
 
18
18
  # Initialize a timed event.
19
19
  def initialize reactor, interval, repeat_limit = -1, args=[], job=nil
20
+ @reactor = reactor
20
21
  @interval = interval
21
22
  @repeat_limit = repeat_limit ? repeat_limit.to_i : -1
22
23
  @job = job || (Proc.new { stop! })
23
- @next = Iodine.time + interval
24
+ @next = @reactor.time + interval
24
25
  args << self
25
26
  @args = args
26
27
  end
@@ -37,11 +38,11 @@ module Iodine
37
38
  # If the timed event is due, this method will also add the event to the queue.
38
39
  # @return [true, false]
39
40
  def done?
40
- return false unless @next <= Iodine.time
41
+ return false unless @next <= @reactor.time
41
42
  return true if @repeat_limit == 0
42
43
  @repeat_limit -= 1 if @repeat_limit.to_i > 0
43
- Iodine.run *@args, &@job
44
- @next = Iodine.time + @interval
44
+ @reactor.run *@args, &@job
45
+ @next = @reactor.time + @interval
45
46
  @repeat_limit == 0
46
47
  end
47
48
  end
@@ -85,17 +86,9 @@ module Iodine
85
86
  end
86
87
 
87
88
  protected
88
- @timer_locker = Mutex.new
89
- @timers = []
90
89
 
91
90
  # Creates a TimedEvent object and adds it to the Timers stack.
92
91
  def timed_job seconds, limit = false, args = [], block = nil
93
92
  @timer_locker.synchronize {@timers << TimedEvent.new(self, seconds, limit, args, block); @timers.last}
94
93
  end
95
- # cycles through timed jobs, executing and/or deleting them if their time has come.
96
- @check_timers = Proc.new do
97
- @timer_locker.synchronize { @timers.delete_if {|t| t.done? } }
98
- end
99
- @check_timers = [@check_timers]
100
-
101
94
  end
@@ -1,3 +1,3 @@
1
1
  module Iodine
2
- VERSION = "0.1.8"
2
+ VERSION = "0.1.9"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iodine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Boaz Segev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-10-30 00:00:00.000000000 Z
11
+ date: 2015-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -76,6 +76,7 @@ files:
76
76
  - lib/iodine.rb
77
77
  - lib/iodine/client.rb
78
78
  - lib/iodine/core.rb
79
+ - lib/iodine/core_init.rb
79
80
  - lib/iodine/http.rb
80
81
  - lib/iodine/http/hpack.rb
81
82
  - lib/iodine/http/http1.rb