leech 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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