leech 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ README.md
2
+ CHANGELOG.md
3
+ TODO.md
4
+ lib/**/*.rb
5
+ bin/*
6
+ features/**/*.feature
7
+ LICENSE
8
+ VERSION
@@ -0,0 +1,24 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ *.gem
23
+ .yardoc
24
+ doc
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## Jul.29.2010
4
+
5
+ *by Kriss Kowalik*
6
+
7
+ Released 0.1.0 experimental version for testing. For now it provides only server.
8
+
9
+ ## Jul.28.2010
10
+
11
+ *by Kriss Kowalik*
12
+
13
+ ### Misc
14
+
15
+ * Created github repository
16
+ * Writen specs for server, handler and auth handler
17
+ * Writen server, base handler and auth handler
18
+ * Documentation
19
+ * Writen README.md
20
+ * Created TODO.md
21
+ * Rakefile (prepared for YARD)
22
+
23
+ ### Created files
24
+
25
+ lib/leech.rb, lib/leech/server.rb, lib/leech/handler.rb, lib/leach/client.rb,
26
+ lib/handlers/auth_spec.rb, spec/spec_helper.rb, spec/spec.opts, spec/server_spec.rb,
27
+ spec/handler_spec.rb, spec/handlers/auth_spec.rb, LICENSE, CHANGELOG.md, VERSION,
28
+ README.md, Rakefile
29
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Kriss Kowalik
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,132 @@
1
+ # Leech
2
+
3
+ Leech is simple TCP client/server framework. Server is similar to rack,
4
+ but is designed for asynchronously handling a short text commands. It can
5
+ be used for some monitoring purposes or simple communication between few
6
+ machines.
7
+
8
+ ## Installation
9
+
10
+ You can install Leach directly from rubygems:
11
+
12
+ gem install leach
13
+
14
+ ## Gettings started
15
+
16
+ Leech is using simple TCP client/server architecture. It's a simple processor
17
+ for text commands passed through TCP socket.
18
+
19
+ ### Server
20
+
21
+ Server can be created in two ways:
22
+
23
+ server = Leech::Server.new :port => 666
24
+ server.use :auth
25
+ server.run
26
+
27
+ ...or using block style:
28
+
29
+ Leech::Server.new do
30
+ port 666
31
+ use :auth
32
+ run
33
+ end
34
+
35
+ #### Server configuration
36
+
37
+ You can pass frew configuration options when you are creating new server, eg:
38
+
39
+ Leech::Server.new(port => 666, :host => 'myhost.com', :timeout => 60)
40
+
41
+ For more information about allowed parameters visit
42
+ [This doc page](http://yardoc.org/doc/Leech/Server.html#initialize-instance_method).
43
+
44
+ #### Handlers
45
+
46
+ Handlers are extending functionality of server by defining custom
47
+ command handling callbacks or server instance methods.
48
+
49
+ For simple commands handling you can use inline handler, which is automatically
50
+ used by server, eg.
51
+
52
+ Leech::Server.new do
53
+ handle(/^PRINT) (.*)$/ {|env,params| print params[0]}
54
+ handle(/^DELETE FILE (.*)$/) {|env,params| File.delete(params[0])}
55
+ end
56
+
57
+ For advanced tasks, you can write own handler, which will add new functionality
58
+ to server instance.
59
+
60
+ class CustomHandler < Leech::Handler
61
+ # This method is automaticaaly called when server will use this handler
62
+ def self.used(server)
63
+ server.instance_eval { extend ServerMethods }
64
+ end
65
+
66
+ module ServerMethods
67
+ def say_hello
68
+ answer("Hello!")
69
+ end
70
+ end
71
+
72
+ handle /^Hello server!$/, :say_hello
73
+ handle /^Bye!$/ do |env,params|
74
+ env.answer("Take care!")
75
+ end
76
+ end
77
+
78
+ To enable this handler in the server you can simply type:
79
+
80
+ Leech::Server.new do
81
+ use CustomHandler
82
+ end
83
+
84
+ You should notice that block-style callbacks are passing two arguments:
85
+ **env** - instance of server which is using this handler for processing,
86
+ and **params** - array of strings fetched from matched command.
87
+
88
+ #### Answering
89
+
90
+ For sending answers to clients server have the `#answer` method. It can be used
91
+ like here:
92
+
93
+ Leech::Server.new do
94
+ handle /^HELLO$/ do |env,params| env.answer("HELLO MY FRIEND!\n")
95
+ end
96
+
97
+ #### Running / Listening
98
+
99
+ Server is partialy acting as `Thread`. You can join it's instance so application
100
+ will be waiting to interrupt or some unhandled server error:
101
+
102
+ server = Leech::Server.new
103
+ server.run
104
+ server.join # on server.acceptor.join
105
+
106
+ You can also simple use:
107
+
108
+ server.run.join
109
+
110
+ ### Client
111
+
112
+ Client will be implemented in 0.2.0 version.
113
+
114
+ ## Links
115
+
116
+ * [Author blog](http://neverendingcoding.com/)
117
+ * [YARD documentation](http://yardoc.org/doc/Leech)
118
+ * [Changelog](http://yardoc.org/doc/file:CHANGELOG.md)
119
+
120
+ ## Note on Patches/Pull Requests
121
+
122
+ * Fork the project.
123
+ * Make your feature addition or bug fix.
124
+ * Add tests for it. This is important so I don't break it in a
125
+ future version unintentionally.
126
+ * Commit, do not mess with rakefile, version, or history.
127
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
128
+ * Send me a pull request. Bonus points for topic branches.
129
+
130
+ ## Copyright
131
+
132
+ Copyright (c) 2010 Kriss Kowalik. See LICENSE for details.
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "leech"
8
+ gem.summary = %Q{Simple TCP client/server framework with commands handling}
9
+ gem.description = %Q{Leech is simple TCP client/server framework. Server is
10
+ similar to rack. It allows to define own handlers for received text commands. }
11
+ gem.email = "kriss.kowalik@gmail.com"
12
+ gem.homepage = "http://github.com/kriss/leech"
13
+ gem.authors = ["Kriss Kowalik"]
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ begin
39
+ require 'yard'
40
+ YARD::Rake::YardocTask.new do |t|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+ title = "Leech #{version}"
43
+ t.files = ['lib/**/*.rb', 'README*']
44
+ t.options = ['--title', title, '--markup', 'markdown', '--files', 'CHANGELOG.md,TODO.md']
45
+ end
46
+ rescue LoadError
47
+ task :yard do
48
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
49
+ end
50
+ end
data/TODO.md ADDED
@@ -0,0 +1,8 @@
1
+ # TODO
2
+
3
+ ## 0.2.0
4
+
5
+ * Client specs and implementation,
6
+ * Assume client helpers,
7
+ * Client auth helpers,
8
+ * Documentation for client.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,19 @@
1
+ require 'socket'
2
+ require 'logger'
3
+ require 'timeout'
4
+ require 'thread'
5
+
6
+ begin
7
+ require 'fastthread'
8
+ rescue LoadError
9
+ $stderr.puts("The fastthread gem not found. Using standard ruby threads.")
10
+ end
11
+
12
+ # Leech is simple TCP client/server framework. Server is similar to rack,
13
+ # but is designed for asynchronously handling a short text commands. It can
14
+ # be used for some monitoring purposes or simple communication between few
15
+ # machines.
16
+ module Leech
17
+ # Namespace for handlers.
18
+ module Handlers; end
19
+ end # Leech
@@ -0,0 +1,7 @@
1
+ require 'leech'
2
+
3
+ module Leech
4
+ class Client
5
+
6
+ end # Client
7
+ end # Leech
@@ -0,0 +1,120 @@
1
+ module Leech
2
+ # Handlers are extending functionality of server by defining custom
3
+ # command handling callbacks or server instance methods.
4
+ #
5
+ # @example Writing own handler
6
+ # class CustomHandler < Leech::Handler
7
+ # def self.used(server)
8
+ # server.instance_eval { extend ServerMethods }
9
+ # end
10
+ #
11
+ # module ServerMethods
12
+ # def say_hello
13
+ # answer("Hello!")
14
+ # end
15
+ # end
16
+ #
17
+ # handle /^Hello server!$/, :say_hello
18
+ # handle /^Bye!$/ do |env,params|
19
+ # env.answer("Take care!")
20
+ # end
21
+ # end
22
+ #
23
+ # @example Enabling handlers by server
24
+ # Leech::Server.new { use CustomHandler }
25
+ #
26
+ # @abstract
27
+ class Handler
28
+ # Default handler error
29
+ class Error < StandardError; end
30
+
31
+ # List of declared matchers
32
+ def self.matchers
33
+ @matchers ||= {}
34
+ end
35
+
36
+ # It defines matching pattern and callback related with. When specified
37
+ # command will match this pattern then callback will be called.
38
+ #
39
+ # @param [Regexp] patern
40
+ # Block will be executed for passed command will be maching it.
41
+ # @param [Symbol, nil] method
42
+ # Name of server method, which should be called when pattern will
43
+ # match with command
44
+ #
45
+ # @example
46
+ # handle(/^PRINT) (.*)$/ {|env,params| print params[0]}
47
+ # handle(/^DELETE FILE (.*)$/) {|env,params| File.delete(params[0])}
48
+ # handle(/^HELLO$/, :say_hello)
49
+ #
50
+ # @raise [Leech::Error]
51
+ # When specified callback is not valid method name or Proc.
52
+ def self.handle(pattern, method=nil, &block)
53
+ if block_given?
54
+ method = block
55
+ end
56
+ if method.is_a?(Proc) || method.is_a?(Symbol)
57
+ self.matchers[pattern] = method
58
+ else
59
+ raise Error, "Invalid handler callback"
60
+ end
61
+ end
62
+
63
+ # You should implement this method in your handler eg. if you would like
64
+ # to modify server class.
65
+ def self.used(server)
66
+ # nothing...
67
+ end
68
+
69
+ # @return [Leecher::Server]
70
+ # Passed server instance
71
+ attr_reader :env
72
+
73
+ # @return [Array<String>]
74
+ # Parameters matched in passed command
75
+ attr_reader :params
76
+
77
+ # Constructor.
78
+ #
79
+ # @param [Leech::Server]
80
+ # Server instance
81
+ def initialize(env)
82
+ @env = env
83
+ end
84
+
85
+ # Compare specified command with declared patterns.
86
+ #
87
+ # @param [String] command
88
+ # Command to handle.
89
+ #
90
+ # @return [Leech::Handler,nil]
91
+ # When command match one of declared patterns then it returns itself,
92
+ # otherwise it returns nil.
93
+ def match(command)
94
+ self.class.matchers.each_pair do |p,m|
95
+ if @params = p.match(command)
96
+ @matcher = m
97
+ return self
98
+ end
99
+ end
100
+ nil
101
+ end
102
+
103
+ # This method can be called only after #match. It executes callback
104
+ # related with matched pattern.
105
+ #
106
+ # @raise [Leech::Error]
107
+ # When @matcher is not defined, which means that #match method wasn't
108
+ # called before.
109
+ def call
110
+ case @matcher
111
+ when Proc
112
+ @matcher.call(env, params)
113
+ when String, Symbol
114
+ env.send(@matcher.to_sym, params)
115
+ else
116
+ raise Error, "Can not call unmatched command"
117
+ end
118
+ end
119
+ end # Handler
120
+ end # Leech
@@ -0,0 +1,51 @@
1
+ module Leech
2
+ module Handlers
3
+ # Simple authorization handler. It uses password string (passcode) for
4
+ # authorize client session.
5
+ #
6
+ # ### Usage
7
+ # Leech::Server.new { use :auth }
8
+ #
9
+ # ### Supported commands
10
+ # AUTHORIZE passcode
11
+ #
12
+ # ### Possible Answers
13
+ # UNAUTHORIZED
14
+ # AUTHORIZED
15
+ class Auth < Handler
16
+ def self.used(server)
17
+ server.instance_eval do
18
+ extend ServerMethods
19
+ end
20
+ end
21
+
22
+ module ServerMethods
23
+ # Authorize client session using simple passcode.
24
+ #
25
+ # @param [String] passcode
26
+ # Password sent by client
27
+ def authorize(passcode)
28
+ if options[:passcode].to_s.strip == passcode.to_s.strip
29
+ Thread.current[:authorized] = true
30
+ answer("AUTHORIZED\n")
31
+ logger.info("Client #{info[:uri]} authorized")
32
+ else
33
+ Thread.current[:authorized] = false
34
+ answer("UNAUTHORIZED\n")
35
+ logger.info("Client #{info[:uri]} unauthorized: invalid passcode")
36
+ end
37
+ end
38
+
39
+ # @return [Boolean]
40
+ # Is client session authorized?
41
+ def authorized?
42
+ !!Thread.current[:authorized]
43
+ end
44
+ end # ServerMethods
45
+
46
+ # Available commands
47
+
48
+ handle(/^AUTHORIZE[\s+]?(.*)?$/m) {|env,params| env.authorize(params[1])}
49
+ end # AuthHandler
50
+ end # Handlers
51
+ end # Leech
@@ -0,0 +1,370 @@
1
+ require 'leech'
2
+ require 'leech/handler'
3
+
4
+ module Leech
5
+ # This is simple TCP server similar to rack, but designed for asynchronously
6
+ # handling a short text commands. It can be used for some monitoring purposes
7
+ # or simple communication between few machines.
8
+ #
9
+ # ### Creating server instance
10
+ #
11
+ # Server can be created in two ways:
12
+ #
13
+ # server = Leech::Server.new :port => 666
14
+ # server.use :auth
15
+ # server.run
16
+ #
17
+ # ...or using block style:
18
+ #
19
+ # Leech::Server.new do
20
+ # port 666
21
+ # use :auth
22
+ # run
23
+ # end
24
+ #
25
+ # ### Complete server example
26
+ #
27
+ # Simple server with authorization and few commands handling can be configured
28
+ # such like this one:
29
+ #
30
+ # server = Leech::Server.new do
31
+ # # simple authorization handler, see Leech::AuthHandler for
32
+ # # more informations.
33
+ # use :auth
34
+ #
35
+ # host 'localhost'
36
+ # port 666
37
+ # max_workers 100
38
+ # timeout 30
39
+ # logger Logger.new('/var/logs/leech/server.log')
40
+ #
41
+ # handle /^SHOW FILE (.*)$/ do |env,params|
42
+ # if File.exists?(param[1])
43
+ # answer(File.open(params[0]).read)
44
+ # else
45
+ # answer('NOT FOUND')
46
+ # end
47
+ # end
48
+ # handle /^DELETE FILE (.*)$/ do |env,params|
49
+ # answer(File.delete(params[1]))
50
+ # end
51
+ #
52
+ # run
53
+ # end
54
+ #
55
+ # # Now we have to join server thread with main thread.
56
+ # server.join
57
+ class Server
58
+ # Default server error
59
+ class Error < StandardError; end
60
+
61
+ # Used to stop server via Thread#raise
62
+ class StopServer < Error; end
63
+
64
+ # Thrown at a thread when it is timed out.
65
+ class TimeoutError < Timeout::Error; end
66
+
67
+ # Server main thread
68
+ #
69
+ # @return [Thread]
70
+ attr_reader :acceptor
71
+
72
+ # Server configuration
73
+ #
74
+ # @return [Hash]
75
+ attr_reader :options
76
+
77
+ # Server will bind to this host
78
+ #
79
+ # @return [String]
80
+ attr_reader :host
81
+
82
+ # Server will be listening on this port
83
+ #
84
+ # @return [Int]
85
+ attr_reader :port
86
+
87
+ # Logging object
88
+ #
89
+ # @return [Logger]
90
+ attr_reader :logger
91
+
92
+ # The maximum number of concurrent processors to accept, anything over
93
+ # this is closed immediately to maintain server processing performance.
94
+ # This may seem mean but it is the most efficient way to deal with overload.
95
+ # Other schemes involve still parsing the client's request wchich defeats
96
+ # the point of an overload handling system.
97
+ #
98
+ # @return [Int,Float]
99
+ attr_reader :max_workers
100
+
101
+ # Maximum idle time
102
+ #
103
+ # @return [Int,Float]
104
+ attr_reader :timeout
105
+
106
+ # A sleep timeout (in hundredths of a second) that is placed between
107
+ # socket.accept calls in order to give the server a cheap throttle time.
108
+ # It defaults to 0 and actually if it is 0 then the sleep is not done
109
+ # at all.
110
+ #
111
+ # @return [Int,Float]
112
+ attr_reader :throttle
113
+
114
+ # Here we have to define block-style setters for each startup parameter.
115
+ %w{host port logger throttle timeout max_workers}.each do |meth|
116
+ eval <<-EVAL
117
+ def #{meth}(*args)
118
+ @#{meth} = args.first if args.size > 0
119
+ @#{meth}
120
+ end
121
+ EVAL
122
+ end
123
+
124
+ # Creates a working server on host:port. Use #run to start the server
125
+ # and `acceptor.join` to join the thread that's processing incoming requests
126
+ # on the socket.
127
+ #
128
+ # @param [Hash] opts see #options
129
+ # @option opts [String] :host ('localhost') see #host
130
+ # @option opts [Int] :port (9933) see #port
131
+ # @option opts [Logger] :logger (Logger.new(STDOUT)) see #logger
132
+ # @option opts [Int] :max_workers (100) see #max_workers
133
+ # @option opts [Int,Float] :timeout (30) see #timeout
134
+ # @option opts [Int,Float] :throttle (0) see #throttle
135
+ #
136
+ # @see Leech::Server#run
137
+ def initialize(opts={}, &block)
138
+ @handlers = []
139
+ @workers = ThreadGroup.new
140
+ @acceptor = nil
141
+ @mutex = Mutex.new
142
+
143
+ @options = opts
144
+ @host = opts[:host] || 'localhost'
145
+ @port = opts[:port] || 9933
146
+ @logger = opts[:logger] || Logger.new(STDOUT)
147
+ @max_workers = opts[:max_workers] || 100
148
+ @timeout = opts[:timeout] || 30
149
+ @throttle = opts[:throttle].to_i / 100.0
150
+
151
+ @inline_handler = Class.new(Leech::Handler)
152
+ instance_eval(&block) if block_given?
153
+ end
154
+
155
+ # Port to acceptor thread #join method.
156
+ #
157
+ # @see Thread#join
158
+ def join
159
+ @acceptor.join if @acceptor
160
+ end
161
+
162
+ # It registers specified handler for using in this server instance.
163
+ # Handlers are extending functionality of server by defining custom
164
+ # command handling callbacks or server instance methods.
165
+ #
166
+ # @param [Leech::Handler, Symbol] handler
167
+ # Handler class or name
168
+ #
169
+ # @raise [Leech::Server::Error,Leech::Handler::Error]
170
+ # When specified adapter was not found or handler is invalid
171
+ #
172
+ # @see Leech::Handler
173
+ def use(handler)
174
+ case handler
175
+ when Class
176
+ @handlers << handler
177
+ handler.used(self)
178
+ when Symbol, String
179
+ begin
180
+ require "leech/handlers/#{handler.to_s}"
181
+ klass = handler.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
182
+ use(eval("Leech::Handlers::#{klass}"))
183
+ rescue LoadError
184
+ raise Error, "Could not find #{handler} handler"
185
+ end
186
+ else
187
+ raise Leech::Handler::Error, "Invalid handler #{handler}"
188
+ end
189
+ end
190
+
191
+ # It defines matching pattern in @inline_handler. It is similar to
192
+ # `Leech::Handler#handle` but can be used only in block-style.
193
+ #
194
+ # @param [Regexp] patern
195
+ # Block will be executed for passed command will be maching it.
196
+ #
197
+ # @example
198
+ # Leech::Server.new do
199
+ # handle(/^PRINT) (.*)$/ {|env,params| print params[0]}
200
+ # handle(/^DELETE FILE (.*)$/) {|env,params| File.delete(params[0])}
201
+ # end
202
+ #
203
+ # @see Leech::Handler#handle
204
+ def handle(pattern, &block)
205
+ @inline_handler.handle(pattern, &block)
206
+ end
207
+
208
+ # Used internally to kill off any worker threads that have taken too long
209
+ # to complete processing. Only called if there are too many processors
210
+ # currently servicing. It returns the count of workers still active
211
+ # after the reap is done. It only runs if there are workers to reap.
212
+ #
213
+ # @param [String] reason
214
+ # Reason why method was executed
215
+ #
216
+ # @return [Array<Thread>]
217
+ # List of still active workers threads.
218
+ def reap_dead_workers(reason='unknown')
219
+ if @workers.list.length > 0
220
+ logger.error "#{Time.now}: Reaping #{@workers.list.length} threads for slow workers because of '#{reason}'"
221
+ error_msg = "Leech timed out this thread: #{reason}"
222
+ mark = Time.now
223
+ @workers.list.each do |worker|
224
+ worker[:started_on] = Time.now if not worker[:started_on]
225
+ if mark - worker[:started_on] > @timeout + @throttle
226
+ logger.error "Thread #{worker.inspect} is too old, killing."
227
+ worker.raise(TimeoutError.new(error_msg))
228
+ end
229
+ end
230
+ end
231
+ return @workers.list.length
232
+ end
233
+
234
+ # Performs a wait on all the currently running threads and kills any that take
235
+ # too long. It waits by `@timeout seconds`, which can be set in `#initialize`.
236
+ # The `@throttle` setting does extend this waiting period by that much longer.
237
+ def graceful_shutdown
238
+ while reap_dead_workers("shutdown") > 0
239
+ logger.error "Waiting for #{@workers.list.length} requests to finish, could take #{@timeout + @throttle} seconds."
240
+ sleep @timeout / 10
241
+ end
242
+ end
243
+
244
+ # Stops the acceptor thread and then causes the worker threads to finish
245
+ # off the request queue before finally exiting. It's also reseting freezed
246
+ # settings.
247
+ def stop
248
+ if running?
249
+ @acceptor.raise(StopServer.new)
250
+ @handlers = Array.new(@handlers)
251
+ @options = Hash.new(@options)
252
+ sleep(0.5) while @acceptor.alive?
253
+ end
254
+ end
255
+
256
+ # Starts serving TCP listener on host and port declared in options.
257
+ # It returns the thread used so you can join it. Each client connection
258
+ # will be processed in separated thread.
259
+ #
260
+ # @return [Thread]
261
+ # Main thread for this server instance.
262
+ def run
263
+ use(@inline_handler)
264
+ @socket = TCPServer.new(@host, @port)
265
+ @handlers = @handlers.uniq.freeze
266
+ @options = @options.freeze
267
+ @acceptor = Thread.new do
268
+ begin
269
+ logger.debug "Starting leech server on tcp://#{@host}:#{@port}"
270
+ loop do
271
+ begin
272
+ client = @socket.accept
273
+ worker_list = @workers.list
274
+
275
+ if worker_list.length >= @max_workers
276
+ logger.error "Server overloaded with #{worker_list.length} workers (#@max_workers max). Dropping connection."
277
+ client.close rescue nil
278
+ reap_dead_workers("max processors")
279
+ else
280
+ thread = Thread.new(client) {|c| process_client(c) }
281
+ thread[:started_on] = Time.now
282
+ @workers.add(thread)
283
+ sleep @throttle if @throttle > 0
284
+ end
285
+ rescue StopServer
286
+ break
287
+ rescue Errno::EMFILE
288
+ reap_dead_workers("too many open files")
289
+ sleep 0.5
290
+ rescue Errno::ECONNABORTED
291
+ client.close rescue nil
292
+ rescue Object => e
293
+ logger.error "#{Time.now}: Unhandled listen loop exception #{e.inspect}."
294
+ logger.error e.backtrace.join("\n")
295
+ end
296
+ end
297
+ graceful_shutdown
298
+ ensure
299
+ @socket.close
300
+ logger.debug "Closing leech server on tcp://#{@host}:#{@port}"
301
+ end
302
+ end
303
+
304
+ return @acceptor
305
+ end
306
+
307
+ # It is getting information about client connection and starts conversation
308
+ # with him. Received commands are passed to declared handlers, where will
309
+ # be processed.
310
+ #
311
+ # @param [TCPSocket] c
312
+ # Client socket
313
+ def process_client(c)
314
+ Thread.current[:client] = c
315
+ Thread.current[:info] = {
316
+ :port => client.peeraddr[1],
317
+ :host => client.peeraddr[2],
318
+ :addr => client.peeraddr[3],
319
+ }
320
+ info[:uri] = [info[:host], info[:port]].join(':')
321
+ logger.debug "Processing client from #{info[:uri]}"
322
+ while line = client.gets
323
+ line = line.chomp.strip
324
+ logger.info "Dispatching command (#{info[:uri]}): #{line}"
325
+ @handlers.each do |handler|
326
+ if handler = handler.new(self).match(line.chomp.strip)
327
+ handler.call
328
+ next
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ # Sends answer to current connected socket. Method should be called only
335
+ # in worker thread.
336
+ #
337
+ # @param [String] msg
338
+ # Text to send
339
+ def answer(msg)
340
+ logger.debug("Answering to (#{info[:uri]}): #{msg.chomp.strip}")
341
+ client.puts(msg)
342
+ end
343
+ alias_method :say, :answer
344
+
345
+ # @return [Boolean]
346
+ # Actual server state. Returns `true` server acceptor thread is alive.
347
+ def running?
348
+ @acceptor && @acceptor.alive?
349
+ end
350
+
351
+ private
352
+
353
+ # Informations about connected client. Method should be called only in
354
+ # woker thread.
355
+ #
356
+ # @return [Hash]
357
+ # Client informations such as host, remote address and port
358
+ def info
359
+ Thread.current[:info]
360
+ end
361
+
362
+ # Client socket from. Method should be called only in woker thread.
363
+ #
364
+ # @return [TCPSocket]
365
+ # Client socket
366
+ def client
367
+ Thread.current[:client]
368
+ end
369
+ end # Server
370
+ end # Leech
@@ -0,0 +1,43 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ require 'socket'
3
+ require 'leech/handler'
4
+
5
+ describe "An Leech server handler" do
6
+ before do
7
+ @test_handler = Class.new(Leech::Handler)
8
+ end
9
+
10
+ it "should receive #used method when it's used by server" do
11
+ srv = Leech::Server.new
12
+ @test_handler.should_receive(:used).once.with(srv)
13
+ srv.use(@test_handler)
14
+ end
15
+
16
+ it "saves custom matchers described in #handle method" do
17
+ @test_handler.class_eval do
18
+ handle(/^TESTING$/, :test)
19
+ handle(/^ANOTHER$/) {|env,params|}
20
+ end
21
+ @test_handler.matchers.keys.size.should == 2
22
+ end
23
+
24
+ it "should dispatch command to valid matcher" do
25
+ @test_handler.class_eval do
26
+ handle /^HELLO$/, :hello
27
+ handle /^SECOND (.*)$/ do |env,params| "Hello" end
28
+ end
29
+ env = OpenStruct.new
30
+ env.class_eval { define_method(:hello) {|p| }}
31
+ th = @test_handler.new(env)
32
+ th.match('HELLO').should be_kind_of(Leech::Handler)
33
+ env.should_receive(:hello).once.with(an_instance_of(MatchData))
34
+ th.call
35
+ th = @test_handler.new(env)
36
+ @test_handler.matchers[/^SECOND (.*)$/].should_receive(:call).once.with(env, an_instance_of(MatchData))
37
+ th.match('SECOND yadayada')
38
+ th.call
39
+ th = @test_handler.new(env)
40
+ th.match('NOT EXIST').should == nil
41
+ lambda { th.call }.should raise_error(Leech::Handler::Error)
42
+ end
43
+ end
@@ -0,0 +1,59 @@
1
+ require File.join(File.dirname(__FILE__), '../spec_helper')
2
+ require 'socket'
3
+ require 'leech/server'
4
+ require 'leech/handler'
5
+ require 'leech/handlers/auth'
6
+
7
+ describe "An Leech Auth handler" do
8
+ before do
9
+ @srv = Leech::Server.new(:logger => Logger.new(StringIO.new))
10
+ @srv.use :auth
11
+ end
12
+
13
+ it "should append #authorize and #authorized? methods to server instance" do
14
+ @srv.should respond_to :authorize
15
+ @srv.should respond_to :authorized?
16
+ end
17
+
18
+ it "should define matcher for AUTHORIZE command" do
19
+ Leech::Handlers::Auth.matchers.size.should == 1
20
+ pattern = Leech::Handlers::Auth.matchers.keys.first
21
+ 'AUTHORIZE'.should =~ pattern
22
+ 'AUTHORIZE passcode'.should =~ pattern
23
+ end
24
+
25
+ context "powered server" do
26
+ it "should authorize client with empty passcode when options[:passcode] is not set" do
27
+ @srv.run
28
+ sock = TCPSocket.new(@srv.host, @srv.port)
29
+ sleep 1
30
+ sock.puts("AUTHORIZE\n")
31
+ sock.gets.should == "AUTHORIZED\n"
32
+ sock.close
33
+ end
34
+
35
+ it "should authorize client with valid passcode" do
36
+ @srv.options[:passcode] = 'secret'
37
+ @srv.run
38
+ sock = TCPSocket.new(@srv.host, @srv.port)
39
+ sleep 1
40
+ sock.puts("AUTHORIZE secret\n")
41
+ sock.gets.should == "AUTHORIZED\n"
42
+ sock.close
43
+ end
44
+
45
+ it "should not authorize client with invalid passcode" do
46
+ @srv.options[:passcode] = 'secret'
47
+ @srv.run
48
+ sock = TCPSocket.new(@srv.host, @srv.port)
49
+ sleep 1
50
+ sock.puts("AUTHORIZE not-secret\n")
51
+ sock.gets.should == "UNAUTHORIZED\n"
52
+ sock.close
53
+ end
54
+
55
+ after do
56
+ @srv.stop
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,178 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ require 'socket'
3
+ require 'leech/server'
4
+
5
+ NULL_LOGGER = Logger.new(StringIO.new)
6
+
7
+ describe "An new Leech server" do
8
+ it "should properly store passed arguments" do
9
+ srv = Leech::Server.new(:host => "host", :port => 111, 1 => 2)
10
+ srv.host.should == "host"
11
+ srv.port.should == 111
12
+ srv.options[1] = 2
13
+ end
14
+
15
+ it "should recognize max_workers option in passed arguments" do
16
+ srv = Leech::Server.new(:host => "host.com", :port => 111, :max_workers => 10)
17
+ srv.max_workers.should == 10
18
+ end
19
+
20
+ it "should recognize timeout option in passed arguments" do
21
+ srv = Leech::Server.new(:host => "host.com", :port => 111, :timeout => 100)
22
+ srv.timeout.should == 100
23
+ end
24
+
25
+ it "should recognize max_workers option in passed arguments" do
26
+ srv = Leech::Server.new(:host => "host.com", :port => 111, :max_workers => 10)
27
+ srv.max_workers.should == 10
28
+ end
29
+
30
+ it "should recognize logger option in passed arguments" do
31
+ srv = Leech::Server.new(:host => "host.com", :port => 111, :logger => "logger")
32
+ srv.logger.should == "logger"
33
+ end
34
+
35
+ it "should recognize throttle option in passed arguments" do
36
+ srv = Leech::Server.new(:host => "host.com", :port => 111, :throttle => 10)
37
+ srv.throttle.should == 10 / 100.0
38
+ end
39
+
40
+ it "should allow to pass arguments in block" do
41
+ srv = Leech::Server.new do
42
+ use(Leech::Handler)
43
+ host 'myhost.com'
44
+ port 12345
45
+ end
46
+ srv.host.should == 'myhost.com'
47
+ srv.port.should == 12345
48
+ end
49
+ end
50
+
51
+ describe "An instnace of Leech server" do
52
+ before do
53
+ @srv = Leech::Server.new(:port => 'localhost', :port => 1234, :timeout => 3,
54
+ :max_workers => 5, :logger => NULL_LOGGER)
55
+ end
56
+
57
+ it "should allow to use additional handlers" do
58
+ @srv.use(handler = Class.new(Leech::Handler))
59
+ @srv.instance_variable_get('@handlers').should include(handler)
60
+ end
61
+
62
+ it "should provide inline handler" do
63
+ @srv.handle(/^TEST$/) {|env,params| }
64
+ inline_handler = @srv.instance_variable_get('@inline_handler')
65
+ inline_handler.matchers.keys.size.should == 1
66
+ inline_handler.matchers.should have_key(/^TEST$/)
67
+ end
68
+
69
+ context "on run" do
70
+ before do
71
+ @srv.run
72
+ end
73
+
74
+ it "should freeze handlers list" do
75
+ @srv.instance_variable_get('@handlers').frozen?.should == true
76
+ end
77
+
78
+ it "should change it's running state" do
79
+ @srv.running?.should == true
80
+ end
81
+
82
+ after do
83
+ @srv.stop
84
+ end
85
+ end
86
+
87
+ context "on stop" do
88
+ before do
89
+ @srv.run
90
+ end
91
+
92
+ it "should change it's running state" do
93
+ @srv.stop
94
+ @srv.running?.should == false
95
+ end
96
+
97
+ it "should unfreeze handlers list" do
98
+ frozen = @srv.instance_variable_get('@handlers')
99
+ @srv.stop if @srv.running?
100
+ unfrozen = @srv.instance_variable_get('@handlers')
101
+ unfrozen.frozen?.should == false
102
+ unfrozen.should == frozen
103
+ end
104
+ end
105
+
106
+ context "on client connection" do
107
+ before do
108
+ @srv.handle(/^TEST MESSAGE$/) {|env,params| env.answer("HELLO\n") }
109
+ @srv.run
110
+ @sock = TCPSocket.new(@srv.host, @srv.port)
111
+ end
112
+
113
+ it "should create new worker for it" do
114
+ sleep 1
115
+ @srv.instance_variable_get('@workers').list.size.should == 1
116
+ end
117
+
118
+ it "should get client informations" do
119
+ sleep 1
120
+ info = @srv.instance_variable_get('@workers').list.first[:info]
121
+ info.should be_kind_of(Hash)
122
+ info.should have_key(:host)
123
+ info.should have_key(:port)
124
+ info.should have_key(:addr)
125
+ info.should have_key(:uri)
126
+ end
127
+
128
+ context "when command is received" do
129
+ it "should handle it by matching handler" do
130
+ @sock.puts("TEST MESSAGE\n")
131
+ answer = @sock.gets
132
+ answer.should == "HELLO\n"
133
+ end
134
+ end
135
+
136
+ after do
137
+ @sock.close
138
+ @srv.stop
139
+ end
140
+ end
141
+
142
+ it "should handle multiple connections" do
143
+ @srv.handle(/^MESSAGE FROM (.*)$/) {|env,params| env.answer("HELLO #{params[1]}\n") }
144
+ @srv.run
145
+ sock1 = TCPSocket.new(@srv.host, @srv.port)
146
+ sock2 = TCPSocket.new(@srv.host, @srv.port)
147
+ sock3 = TCPSocket.new(@srv.host, @srv.port)
148
+ sleep 1
149
+ @srv.instance_variable_get('@workers').list.size.should == 3
150
+ sock1.close
151
+ sock2.close
152
+ sock3.close
153
+ @srv.stop
154
+ end
155
+
156
+ it "should handle multiple connections" do
157
+ @srv.run
158
+ sock1 = TCPSocket.new(@srv.host, @srv.port)
159
+ sock2 = TCPSocket.new(@srv.host, @srv.port)
160
+ sock3 = TCPSocket.new(@srv.host, @srv.port)
161
+ sleep 1
162
+ @srv.instance_variable_get('@workers').list.size.should == 3
163
+ sock1.close
164
+ sock2.close
165
+ sock3.close
166
+ @srv.stop
167
+ end
168
+
169
+ it "should respect :max_workers option" do
170
+ @srv.run
171
+ sockets = []
172
+ 8.times { sockets << TCPSocket.new(@srv.host, @srv.port) }
173
+ sleep 1
174
+ @srv.instance_variable_get('@workers').list.size.should == 5
175
+ sockets.each {|s| s.close }
176
+ @srv.stop
177
+ end
178
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'rubygems'
4
+ require 'leech'
5
+ require 'spec'
6
+ require 'spec/autorun'
7
+
8
+ Spec::Runner.configure do |config|
9
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: leech
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Kriss Kowalik
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-07-28 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 2
30
+ - 9
31
+ version: 1.2.9
32
+ type: :development
33
+ version_requirements: *id001
34
+ description: "Leech is simple TCP client/server framework. Server is\n similar to rack. It allows to define own handlers for received text commands. "
35
+ email: kriss.kowalik@gmail.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files:
41
+ - LICENSE
42
+ - README.md
43
+ files:
44
+ - .document
45
+ - .gitignore
46
+ - CHANGELOG.md
47
+ - LICENSE
48
+ - README.md
49
+ - Rakefile
50
+ - TODO.md
51
+ - VERSION
52
+ - lib/leech.rb
53
+ - lib/leech/client.rb
54
+ - lib/leech/handler.rb
55
+ - lib/leech/handlers/auth.rb
56
+ - lib/leech/server.rb
57
+ - spec/handler_spec.rb
58
+ - spec/handlers/auth_spec.rb
59
+ - spec/server_spec.rb
60
+ - spec/spec.opts
61
+ - spec/spec_helper.rb
62
+ has_rdoc: true
63
+ homepage: http://github.com/kriss/leech
64
+ licenses: []
65
+
66
+ post_install_message:
67
+ rdoc_options:
68
+ - --charset=UTF-8
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ requirements: []
86
+
87
+ rubyforge_project:
88
+ rubygems_version: 1.3.6
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: Simple TCP client/server framework with commands handling
92
+ test_files:
93
+ - spec/handler_spec.rb
94
+ - spec/server_spec.rb
95
+ - spec/spec_helper.rb
96
+ - spec/handlers/auth_spec.rb