servolux 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,274 @@
1
+
2
+ # == Synopsis
3
+ # A Piper is used to fork a child proces and then establish a communication
4
+ # pipe between the parent and child. This communication pipe is used to pass
5
+ # Ruby objects between the two.
6
+ #
7
+ # == Details
8
+ # When a new piper instance is created, the Ruby process is forked into two
9
+ # porcesses - the parent and the child. Each continues execution from the
10
+ # point of the fork. The piper establishes a pipe for communication between
11
+ # the parent and the child. This communication pipe can be opened as read /
12
+ # write / read-write (from the perspective of the parent).
13
+ #
14
+ # Communication over the pipe is handled by marshalling Ruby objects through
15
+ # the pipe. This means that nearly any Ruby object can be passed between the
16
+ # two processes. For example, exceptions from the child process can be
17
+ # marshalled back to the parent and raised there.
18
+ #
19
+ # Object passing is handled by use of the +puts+ and +gets+ methods defined
20
+ # on the Piper. These methods use a +timeout+ and the Kernel#select method
21
+ # to ensure a timely return.
22
+ #
23
+ # == Examples
24
+ #
25
+ # piper = Servolux::Piper.new('r', :timeout => 5)
26
+ #
27
+ # piper.parent {
28
+ # $stdout.puts "parent pid #{Process.pid}"
29
+ # $stdout.puts "child pid #{piper.pid} [from fork]"
30
+ #
31
+ # child_pid = piper.gets
32
+ # $stdout.puts "child pid #{child_pid} [from child]"
33
+ #
34
+ # msg = piper.gets
35
+ # $stdout.puts "message from child #{msg.inspect}"
36
+ # }
37
+ #
38
+ # piper.child {
39
+ # sleep 2
40
+ # piper.puts Process.pid
41
+ # sleep 3
42
+ # piper.puts "The time is #{Time.now}"
43
+ # }
44
+ #
45
+ # piper.close
46
+ #
47
+ class Servolux::Piper
48
+
49
+ # :stopdoc:
50
+ SEPERATOR = [0xDEAD, 0xBEEF].pack('n*').freeze
51
+ # :startdoc:
52
+
53
+ # call-seq:
54
+ # Piper.daemon( nochdir = false, noclose = false )
55
+ #
56
+ # Creates a new Piper with the child process configured as a daemon. The
57
+ # +pid+ method of the piper returns the PID of the daemon process.
58
+ #
59
+ # Be default a daemon process will release its current working directory
60
+ # and the stdout/stderr/stdin file descriptors. This allows the parent
61
+ # process to exit cleanly. This behavior can be overridden by setting the
62
+ # _nochdir_ and _noclose_ flags to true. The first will keep the current
63
+ # working directory; the second will keep stdout/stderr/stdin open.
64
+ #
65
+ def self.daemon( nochdir = false, noclose = false )
66
+ piper = self.new(:timeout => 1)
67
+ piper.parent {
68
+ pid = piper.gets
69
+ piper.instance_variable_set(:@child_pid, pid)
70
+ }
71
+ piper.child {
72
+ Process.setsid # Become session leader.
73
+ exit!(0) if fork # Zap session leader.
74
+
75
+ Dir.chdir '/' unless nochdir # Release old working directory.
76
+ File.umask 0000 # Ensure sensible umask.
77
+
78
+ unless noclose
79
+ STDIN.reopen '/dev/null' # Free file descriptors and
80
+ STDOUT.reopen '/dev/null', 'a' # point them somewhere sensible.
81
+ STDERR.reopen '/dev/null', 'a'
82
+ end
83
+
84
+ piper.puts Process.pid
85
+ }
86
+ piper
87
+ end
88
+
89
+ # The timeout in seconds to wait for puts / gets commands.
90
+ attr_accessor :timeout
91
+
92
+ # The read end of the pipe.
93
+ attr_reader :read_io
94
+
95
+ # The write end of the pipe.
96
+ attr_reader :write_io
97
+
98
+ # call-seq:
99
+ # Piper.new( mode = 'r', opts = {} )
100
+ #
101
+ # Creates a new Piper instance with the communication pipe configured
102
+ # using the provided _mode_. The default mode is read-only (from the
103
+ # parent, and write-only from the child). The supported modes are as
104
+ # follows:
105
+ #
106
+ # Mode | Parent View | Child View
107
+ # -----+-------------+-----------
108
+ # r read-only write-only
109
+ # w write-only read-only
110
+ # rw read-write read-write
111
+ #
112
+ # The communication timeout can be provided as an option. This is the
113
+ # number of seconds to wait for a +puts+ or +gets+ to succeed.
114
+ #
115
+ def initialize( *args )
116
+ opts = args.last.is_a?(Hash) ? args.pop : {}
117
+ mode = args.first || 'r'
118
+
119
+ unless %w[r w rw].include? mode
120
+ raise ArgumentError, "Unsupported mode #{mode.inspect}"
121
+ end
122
+
123
+ @timeout = opts.getopt(:timeout, 0)
124
+ @read_io, @write_io = IO.pipe
125
+ @child_pid = Kernel.fork
126
+
127
+ if child?
128
+ case mode
129
+ when 'r'; close_read
130
+ when 'w'; close_write
131
+ end
132
+ else
133
+ case mode
134
+ when 'r'; close_write
135
+ when 'w'; close_read
136
+ end
137
+ end
138
+ end
139
+
140
+ # Close both the read and write ends of the communications pipe. This only
141
+ # affects the process from which it was called -- the parent or the child.
142
+ #
143
+ def close
144
+ @read_io.close rescue nil
145
+ @write_io.close rescue nil
146
+ end
147
+
148
+ # Close the read end of the communications pipe. This only affects the
149
+ # process from which it was called -- the parent or the child.
150
+ #
151
+ def close_read
152
+ @read_io.close rescue nil
153
+ end
154
+
155
+ # Close the write end of the communications pipe. This only affects the
156
+ # process from which it was called -- the parent or the child.
157
+ #
158
+ def close_write
159
+ @write_io.close rescue nil
160
+ end
161
+
162
+ # Returns +true+ if the communications pipe is readable from the process
163
+ # and there is data waiting to be read.
164
+ #
165
+ def readable?
166
+ return false if @read_io.closed?
167
+ r,w,e = Kernel.select([@read_io], nil, nil, @timeout)
168
+ return !(r.nil? or r.empty?)
169
+ end
170
+
171
+ # Returns +true+ if the communications pipe is writeable from the process
172
+ # and the write buffer can accept more data.
173
+ #
174
+ def writeable?
175
+ return false if @write_io.closed?
176
+ r,w,e = Kernel.select(nil, [@write_io], nil, @timeout)
177
+ return !(w.nil? or w.empty?)
178
+ end
179
+
180
+ # call-seq:
181
+ # child { block }
182
+ # child {|piper| block }
183
+ #
184
+ # Execute the _block_ only in the child process. This method returns
185
+ # immediately when called from the parent process.
186
+ #
187
+ def child( &block )
188
+ return unless child?
189
+ raise ArgumentError, "A block must be supplied" if block.nil?
190
+
191
+ if block.arity > 0
192
+ block.call(self)
193
+ else
194
+ block.call
195
+ end
196
+ end
197
+
198
+ # Returns +true+ if this is the child prcoess and +false+ otherwise.
199
+ #
200
+ def child?
201
+ @child_pid.nil?
202
+ end
203
+
204
+ # call-seq:
205
+ # parent { block }
206
+ # parent {|piper| block }
207
+ #
208
+ # Execute the _block_ only in the parent process. This method returns
209
+ # immediately when called from the child process.
210
+ #
211
+ def parent( &block )
212
+ return unless parent?
213
+ raise ArgumentError, "A block must be supplied" if block.nil?
214
+
215
+ if block.arity > 0
216
+ block.call(self)
217
+ else
218
+ block.call
219
+ end
220
+ end
221
+
222
+ # Returns +true+ if this is the parent prcoess and +false+ otherwise.
223
+ #
224
+ def parent?
225
+ !@child_pid.nil?
226
+ end
227
+
228
+ # Returns the PID of the child process when called from the parent.
229
+ # Returns +nil+ when called from the child.
230
+ #
231
+ def pid
232
+ @child_pid
233
+ end
234
+
235
+ # Read an object from the communication pipe. Returns +nil+ if the pipe is
236
+ # closed for reading or if no data is available before the timeout
237
+ # expires. If data is available then it is un-marshalled and returned as a
238
+ # Ruby object.
239
+ #
240
+ # This method will block until the +timeout+ is reached or data can be
241
+ # read from the pipe.
242
+ #
243
+ def gets
244
+ return unless readable?
245
+
246
+ data = @read_io.gets SEPERATOR
247
+ return if data.nil?
248
+
249
+ data.chomp! SEPERATOR
250
+ Marshal.load(data) rescue data
251
+ end
252
+
253
+ # Write an object to the communication pipe. Returns +nil+ if the pipe is
254
+ # closed for writing or if the write buffer is full. The _obj_ is
255
+ # marshalled and written to the pipe (therefore, procs and other
256
+ # un-marshallable Ruby objects cannot be passed through the pipe).
257
+ #
258
+ # If the write is successful, then the number of bytes written to the pipe
259
+ # is returned. If this number is zero it means that the _obj_ was
260
+ # unsuccessfully communicated (sorry).
261
+ #
262
+ def puts( obj )
263
+ return unless writeable?
264
+
265
+ bytes = @write_io.write Marshal.dump(obj)
266
+ @write_io.write SEPERATOR if bytes > 0
267
+ @write_io.flush
268
+
269
+ bytes
270
+ end
271
+
272
+ end # class Servolux::Piper
273
+
274
+ # EOF
@@ -0,0 +1,226 @@
1
+
2
+ # == Synopsis
3
+ # The Server class makes it simple to create a server-type application in
4
+ # Ruby. A server in this context is any process that should run for a long
5
+ # period of time either in the foreground or as a daemon.
6
+ #
7
+ # == Details
8
+ # The Server class provides for standard server features: process ID file
9
+ # management, signal handling, run loop, logging, etc. All that you need to
10
+ # provide is a +run+ method that will be called by the server's run loop.
11
+ # Optionally, you can provide a block to the +new+ method and it will be
12
+ # called within the run loop instead of a run method.
13
+ #
14
+ # SIGINT and SIGTERM are handled by default. These signals will gracefully
15
+ # shutdown the server by calling the +shutdown+ method (provided by default,
16
+ # too). A few other signals can be handled by defining a few methods on your
17
+ # server instance. For example, SIGINT is hanlded by the +int+ method (an
18
+ # alias for +shutdown+). Likewise, SIGTERM is handled by the +term+ method
19
+ # (another alias for +shutdown+). The following signal methods are
20
+ # recognized by the Server class:
21
+ #
22
+ # Method | Signal | Default Action
23
+ # --------+----------+----------------
24
+ # hup SIGHUP none
25
+ # int SIGINT shutdown
26
+ # term SIGTERM shutdown
27
+ # usr1 SIGUSR1 none
28
+ # usr2 SIGUSR2 none
29
+ #
30
+ # In order to handle SIGUSR1 you would define a <tt>usr1</tt> method for your
31
+ # server.
32
+ #
33
+ # There are a few other methods that are useful and should be mentioned. Two
34
+ # methods are called before and after the run loop starts: +before_starting+
35
+ # and +after_starting+. The first is called just before the run loop thread
36
+ # is created and started. The second is called just after the run loop
37
+ # thread has been created (no guarantee is made that the run loop thread has
38
+ # actually been scheduled).
39
+ #
40
+ # Likewise, two other methods are called before and after the run loop is
41
+ # shutdown: +before_stopping+ and +after_stopping+. The first is called just
42
+ # before the run loop thread is signaled for shutdown. The second is called
43
+ # just after the run loop thread has died; the +after_stopping+ method is
44
+ # guarnteed to NOT be called till after the run loop thread is well and
45
+ # truly dead.
46
+ #
47
+ # == Usage
48
+ # For simple, quick and dirty servers just pass a block to the Server
49
+ # initializer. This block will be used as the run method.
50
+ #
51
+ # server = Servolux::Server.new('Basic', :interval => 1) {
52
+ # puts "I'm alive and well @ #{Time.now}"
53
+ # }
54
+ # server.startup
55
+ #
56
+ # For more complex services you will need to define your own server methods:
57
+ # the +run+ method, signal handlers, and before/after methods. Any pattern
58
+ # that Ruby provides for defining methods on objects can be used to define
59
+ # these methods. In a nutshell:
60
+ #
61
+ # Inheritance
62
+ #
63
+ # class MyServer < Servolux::Server
64
+ # def run
65
+ # puts "I'm alive and well @ #{Time.now}"
66
+ # end
67
+ # end
68
+ # server = MyServer.new('MyServer', :interval => 1)
69
+ # server.startup
70
+ #
71
+ # Extension
72
+ #
73
+ # module MyServer
74
+ # def run
75
+ # puts "I'm alive and well @ #{Time.now}"
76
+ # end
77
+ # end
78
+ # server = Servolux::Server.new('Module', :interval => 1)
79
+ # server.extend MyServer
80
+ # server.startup
81
+ #
82
+ # Singleton Class
83
+ #
84
+ # server = Servolux::Server.new('Singleton', :interval => 1)
85
+ # class << server
86
+ # def run
87
+ # puts "I'm alive and well @ #{Time.now}"
88
+ # end
89
+ # end
90
+ # server.startup
91
+ #
92
+ # == Examples
93
+ #
94
+ # === Signals
95
+ # This example shows how to change the log level of the server when SIGUSR1
96
+ # is sent to the process. The log level toggles between "debug" and the
97
+ # original log level each time SIGUSR1 is sent to the server process. Since
98
+ # this is a module, it can be used with any Servolux::Server instance.
99
+ #
100
+ # module DebugSignal
101
+ # def usr1
102
+ # if @old_log_level
103
+ # logger.level = @old_log_level
104
+ # @old_log_level = nil
105
+ # else
106
+ # @old_log_level = logger.level
107
+ # logger.level = :debug
108
+ # end
109
+ # end
110
+ # end
111
+ #
112
+ # server = Servolux::Server.new('Debugger', :interval => 2) {
113
+ # logger.info "Running @ #{Time.now}"
114
+ # logger.debug "hey look - a debug message"
115
+ # }
116
+ # server.extend DebugSignal
117
+ # server.startup
118
+ #
119
+ class Servolux::Server
120
+ include ::Servolux::Threaded
121
+
122
+ # :stopdoc:
123
+ SIGNALS = %w[HUP INT TERM USR1 USR2] & Signal.list.keys
124
+ SIGNALS.each {|sig| sig.freeze}.freeze
125
+ # :startdoc:
126
+
127
+ Error = Class.new(::Servolux::Error)
128
+
129
+ attr_reader :name
130
+ attr_writer :logger
131
+ attr_writer :pid_file
132
+
133
+ # call-seq:
134
+ # Server.new( name, options = {} ) { block }
135
+ #
136
+ # Creates a new server identified by _name_ and configured from the
137
+ # _options_ hash. The _block_ is run inside a separate thread that will
138
+ # loop at the configured interval.
139
+ #
140
+ # ==== Options
141
+ # * logger <Logger> :: The logger instance this server will use
142
+ # * pid_file <String> :: Location of the PID file
143
+ # * interval <Numeric> :: Sleep interval between invocations of the _block_
144
+ #
145
+ def initialize( name, opts = {}, &block )
146
+ @name = name
147
+
148
+ self.logger = opts.getopt :logger
149
+ self.pid_file = opts.getopt :pid_file
150
+ self.interval = opts.getopt :interval, 0
151
+
152
+ if block
153
+ eg = class << self; self; end
154
+ eg.__send__(:define_method, :run, &block)
155
+ end
156
+
157
+ ary = %w[name logger pid_file].map { |var|
158
+ self.send(var).nil? ? var : nil
159
+ }.compact
160
+ raise Error, "These variables are required: #{ary.join(', ')}." unless ary.empty?
161
+ end
162
+
163
+ # Start the server running using it's own internal thread. This method
164
+ # will not return until the server is shutdown.
165
+ #
166
+ # Startup involves creating a PID file, registering signal handlers to
167
+ # shutdown the server, starting and joining the server thread. The PID
168
+ # file is deleted when this method returns.
169
+ #
170
+ def startup
171
+ return self if running?
172
+ begin
173
+ create_pid_file
174
+ trap_signals
175
+ start
176
+ join
177
+ ensure
178
+ delete_pid_file
179
+ end
180
+ return self
181
+ end
182
+
183
+ alias :shutdown :stop # for symmetry with the startup method
184
+ alias :int :stop # handles the INT signal
185
+ alias :term :stop # handles the TERM signal
186
+ private :start, :stop
187
+
188
+ # Returns the logger instance used by the server. If none was given, then
189
+ # a logger is created from the Logging framework (see the Logging rubygem
190
+ # for more information).
191
+ #
192
+ def logger
193
+ @logger ||= Logging.logger[self]
194
+ end
195
+
196
+ # Returns the PID file name used by the server. If none was given, then
197
+ # the server name is used to create a PID file name.
198
+ #
199
+ def pid_file
200
+ @pid_file ||= name.downcase.tr(' ','_') + '.pid'
201
+ end
202
+
203
+ private
204
+
205
+ def create_pid_file
206
+ logger.debug "Server #{name.inspect} creating pid file #{pid_file.inspect}"
207
+ File.open(pid_file, 'w') {|fd| fd.write(Process.pid.to_s)}
208
+ end
209
+
210
+ def delete_pid_file
211
+ if test(?f, pid_file)
212
+ logger.debug "Server #{name.inspect} removing pid file #{pid_file.inspect}"
213
+ File.delete(pid_file)
214
+ end
215
+ end
216
+
217
+ def trap_signals
218
+ SIGNALS.each do |sig|
219
+ m = sig.downcase.to_sym
220
+ Signal.trap(sig) { self.send(m) rescue nil } if self.respond_to? m
221
+ end
222
+ end
223
+
224
+ end # class Servolux::Server
225
+
226
+ # EOF
@@ -0,0 +1,133 @@
1
+
2
+ # == Synopsis
3
+ # The Threaded module is used to peform some activity at a specified
4
+ # interval.
5
+ #
6
+ # == Details
7
+ # Sometimes it is useful for an object to have its own thread of execution
8
+ # to perform a task at a recurring interval. The Threaded module
9
+ # encapsulates this functionality so you don't have to write it yourself. It
10
+ # can be used with any object that responds to the +run+ method.
11
+ #
12
+ # The threaded object is run by calling the +start+ method. This will create
13
+ # a new thread that will invoke the +run+ method at the desired interval.
14
+ # Just before the thread is created the +before_starting+ method will be
15
+ # called (if it is defined by the threaded object). Likewise, after the
16
+ # thread is created the +after_starting+ method will be called (if it is
17
+ # defeined by the threaded object).
18
+ #
19
+ # The threaded object is stopped by calling the +stop+ method. This sets an
20
+ # internal flag and then wakes up the thread. The thread gracefully exits
21
+ # after checking the flag. Like the start method, before and after methods
22
+ # are defined for stopping as well. Just before the thread is stopped the
23
+ # +before_stopping+ method will be called (if it is defined by the threaded
24
+ # object). Likewise, after the thread has died the +after_stopping+ method
25
+ # will be called (if it is defeined by the threaded object).
26
+ #
27
+ # Calling the +join+ method on a threaded object will cause the calling
28
+ # thread to wait until the threaded object has stopped. An optional timeout
29
+ # parameter can be given.
30
+ #
31
+ # == Examples
32
+ # Take a look at the Servolux::Server class for an example of a threaded
33
+ # object.
34
+ #
35
+ module Servolux::Threaded
36
+
37
+ # This method will be called by the activity thread at the desired
38
+ # interval. Implementing classes are exptect to provide this
39
+ # functionality.
40
+ #
41
+ def run
42
+ raise NotImplementedError,
43
+ 'This method must be defined by the threaded object.'
44
+ end
45
+
46
+ # Start the activity thread. If already started this method will return
47
+ # without taking any action.
48
+ #
49
+ # If the including class defines a 'before_starting' method, it will be
50
+ # called before the thread is created and run. Likewise, if the
51
+ # including class defines an 'after_starting' method, it will be called
52
+ # after the thread is created.
53
+ #
54
+ def start
55
+ return self if running?
56
+ logger.debug "Starting"
57
+
58
+ before_starting if self.respond_to?(:before_starting)
59
+ @activity_thread_running = true
60
+ @activity_thread = Thread.new {
61
+ begin
62
+ loop {
63
+ sleep interval
64
+ break unless running?
65
+ run
66
+ }
67
+ rescue Exception => e
68
+ logger.fatal e
69
+ end
70
+ }
71
+ after_starting if self.respond_to?(:after_starting)
72
+ self
73
+ end
74
+
75
+ # Stop the activity thread. If already stopped this method will return
76
+ # without taking any action.
77
+ #
78
+ # If the including class defines a 'before_stopping' method, it will be
79
+ # called before the thread is stopped. Likewise, if the including class
80
+ # defines an 'after_stopping' method, it will be called after the thread
81
+ # has stopped.
82
+ #
83
+ def stop
84
+ return self unless running?
85
+ logger.debug "Stopping"
86
+
87
+ before_stopping if self.respond_to?(:before_stopping)
88
+ @activity_thread_running = false
89
+ @activity_thread.wakeup
90
+ @activity_thread.join
91
+ @activity_thread = nil
92
+ after_stopping if self.respond_to?(:after_stopping)
93
+ self
94
+ end
95
+
96
+ # If the activity thread is running, the calling thread will suspend
97
+ # execution and run the activity thread. This method does not return until
98
+ # the activity thread is stopped or until _limit_ seconds have passed.
99
+ #
100
+ # If the activity thread is not running, this method returns immediately
101
+ # with +nil+.
102
+ #
103
+ def join( limit = nil )
104
+ @activity_thread.join limit
105
+ self
106
+ rescue NoMethodError
107
+ return self
108
+ end
109
+
110
+ # Returns +true+ if the activity thread is running. Returns +false+
111
+ # otherwise.
112
+ #
113
+ def running?
114
+ @activity_thread_running
115
+ end
116
+
117
+ # Sets the number of seconds to sleep between invocations of the
118
+ # threaded object's 'run' method.
119
+ #
120
+ def interval=( value )
121
+ @activity_thread_interval = value
122
+ end
123
+
124
+ # Returns the number of seconds to sleep between invocations of the
125
+ # threaded object's 'run' method.
126
+ #
127
+ def interval
128
+ @activity_thread_interval
129
+ end
130
+
131
+ end # module Servolux::Threaded
132
+
133
+ # EOF
data/lib/servolux.rb ADDED
@@ -0,0 +1,49 @@
1
+
2
+ require 'logging'
3
+
4
+ module Servolux
5
+
6
+ # :stopdoc:
7
+ VERSION = '0.1.0'
8
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
9
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
10
+ # :startdoc:
11
+
12
+ # Generic Servolux Error class.
13
+ Error = Class.new(StandardError)
14
+
15
+ # Returns the version string for the library.
16
+ #
17
+ def self.version
18
+ VERSION
19
+ end
20
+
21
+ # Returns the library path for the module. If any arguments are given,
22
+ # they will be joined to the end of the libray path using
23
+ # <tt>File.join</tt>.
24
+ #
25
+ def self.libpath( *args )
26
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
27
+ end
28
+
29
+ # Returns the lpath for the module. If any arguments are given,
30
+ # they will be joined to the end of the path using
31
+ # <tt>File.join</tt>.
32
+ #
33
+ def self.path( *args )
34
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
35
+ end
36
+
37
+ # Returns +true+ if the execution platform supports fork.
38
+ #
39
+ def self.fork?
40
+ RUBY_PLATFORM != 'java' and test(?e, '/dev/null')
41
+ end
42
+
43
+ end # module Servolux
44
+
45
+ %w[threaded server piper daemon].each do |lib|
46
+ require Servolux.libpath('servolux', lib)
47
+ end
48
+
49
+ # EOF
@@ -0,0 +1,20 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe Servolux do
5
+
6
+ before :all do
7
+ @root_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
8
+ end
9
+
10
+ it "finds things releative to 'lib'" do
11
+ Servolux.libpath(%w[servolux threaded]).should == File.join(@root_dir, %w[lib servolux threaded])
12
+ end
13
+
14
+ it "finds things releative to 'root'" do
15
+ Servolux.path('Rakefile').should == File.join(@root_dir, 'Rakefile')
16
+ end
17
+
18
+ end
19
+
20
+ # EOF