webby 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- # $Id: resource.rb 17 2007-08-28 04:11:00Z tim_pease $
1
+ # $Id: resource.rb 46 2007-11-27 03:31:29Z tim_pease $
2
2
 
3
3
  module Webby
4
4
 
@@ -54,6 +54,9 @@ class Resource
54
54
  # Resource file modification time
55
55
  attr_reader :mtime
56
56
 
57
+ # Resource page number (if needed)
58
+ attr_reader :number
59
+
57
60
  # call-seq:
58
61
  # Resource.new( filename ) => resource
59
62
  #
@@ -66,6 +69,7 @@ class Resource
66
69
  @ext = ::File.extname(@path).sub(%r/\A\.?/o, '')
67
70
  @mtime = ::File.mtime @path
68
71
 
72
+ @number = nil
69
73
  @rendering = false
70
74
 
71
75
  # deal with the meta-data
@@ -119,7 +123,7 @@ class Resource
119
123
  return @mdata['extension'] if @mdata.has_key? 'extension'
120
124
 
121
125
  if @mdata.has_key? 'layout'
122
- lyt = self.class.layouts.find_by_name @mdata['layout']
126
+ lyt = self.class.layouts.find :filename => @mdata['layout']
123
127
  break if lyt.nil?
124
128
  return lyt.extension
125
129
  end
@@ -138,18 +142,45 @@ class Resource
138
142
  # the 'destination' propery in the resource's meta-data.
139
143
  #
140
144
  def destination
141
- return @dest if defined? @dest
142
- return @dest = ::Webby.config['output_dir'] if is_layout?
145
+ return @dest if defined? @dest and @dest
146
+ return @dest = ::Webby.cairn if is_layout?
143
147
 
144
148
  @dest = if @mdata.has_key? 'destination' then @mdata['destination']
145
149
  else File.join(dir, filename) end
146
150
 
147
151
  @dest = File.join(::Webby.config['output_dir'], @dest)
152
+ @dest << @number.to_s if @number
148
153
  @dest << '.'
149
154
  @dest << extension
150
155
  @dest
151
156
  end
152
157
 
158
+ # call-seq
159
+ # href => string or nil
160
+ #
161
+ # Returns a string suitable for use as an href linking to this page. Nil
162
+ # is returned for layouts.
163
+ #
164
+ def href
165
+ return nil if is_layout?
166
+ return @href if defined? @href and @href
167
+
168
+ @href = destination.sub(::Webby.config['output_dir'], '')
169
+ @href
170
+ end
171
+
172
+ # call-seq:
173
+ # resource.number = Integer
174
+ #
175
+ # Sets the page number for the current resource to the given integer. This
176
+ # number is used to modify the output destination for resources that
177
+ # require pagination.
178
+ #
179
+ def number=( num )
180
+ @number = num
181
+ @dest = nil
182
+ end
183
+
153
184
  # call-seq:
154
185
  # render => string
155
186
  #
@@ -160,11 +191,12 @@ class Resource
160
191
  # Note, this only renders this resource. The returned string does not
161
192
  # include any layout rendering.
162
193
  #
163
- def render
194
+ def render( renderer = nil )
164
195
  raise Error, "page '#@path' is in a rendering loop" if @rendering
165
196
 
166
197
  @rendering = true
167
- content = Renderer.new(self).render_page
198
+ renderer ||= Renderer.new(self)
199
+ content = renderer.render_page
168
200
  @rendering = false
169
201
 
170
202
  return content
@@ -213,23 +245,24 @@ class Resource
213
245
  return @mdata['dirty'] if @mdata.has_key? 'dirty'
214
246
 
215
247
  # if the destination file does not exist, then we are dirty
216
- return @mdata['dirty'] = true unless test ?e, destination
248
+ return true unless test ?e, destination
217
249
 
218
250
  # if this file's mtime is larger than the destination file's
219
251
  # mtime, then we are dirty
220
- @mdata['dirty'] = @mtime > File.mtime(destination)
221
- return @mdata['dirty'] if is_static? or @mdata['dirty']
252
+ dirty = @mtime > File.mtime(destination)
253
+ return dirty if is_static? or dirty
222
254
 
223
255
  # check to see if the layout is dirty, and it it is then we
224
256
  # are dirty, too
225
257
  if @mdata.has_key? 'layout'
226
- lyt = self.class.layouts.find_by_name @mdata['layout']
227
- break if lyt.nil?
228
- return @mdata['dirty'] = true if lyt.dirty?
258
+ lyt = self.class.layouts.find :filename => @mdata['layout']
259
+ unless lyt.nil?
260
+ return true if lyt.dirty?
261
+ end
229
262
  end
230
263
 
231
264
  # if we got here, then we are not dirty
232
- @mdata['dirty'] = false
265
+ false
233
266
  end
234
267
 
235
268
  # call-seq:
@@ -0,0 +1,149 @@
1
+ # This code was originally written by Bruce Williams, and it is available
2
+ # as the Paginator gem. I've added a few helper methods and modifications so
3
+ # it plays a little more nicely with Webby. Specifically, a Webby::Resource
4
+ # can be given to the Page and used to generate links to the previous and
5
+ # next pages.
6
+ #
7
+ # Many thanks to Bruce Williams for letting me use his work. Drop him a note
8
+ # of praise scribbled on the back of a $100 bill. He'd appreciate it.
9
+
10
+ require 'forwardable'
11
+
12
+ module Webby
13
+ class Paginator
14
+
15
+ include Enumerable
16
+
17
+ class ArgumentError < ::ArgumentError; end
18
+ class MissingCountError < ArgumentError; end
19
+ class MissingSelectError < ArgumentError; end
20
+
21
+ attr_reader :per_page, :count, :resource
22
+
23
+ # Instantiate a new Paginator object
24
+ #
25
+ # Provide:
26
+ # * A total count of the number of objects to paginate
27
+ # * The number of objects in each page
28
+ # * A block that returns the array of items
29
+ # * The block is passed the item offset
30
+ # (and the number of items to show per page, for
31
+ # convenience, if the arity is 2)
32
+ def initialize(count, per_page, resource, &select)
33
+ @count, @per_page, @resource = count, per_page, resource
34
+ unless select
35
+ raise MissingSelectError, "Must provide block to select data for each page"
36
+ end
37
+ @select = select
38
+ end
39
+
40
+ # Total number of pages
41
+ def number_of_pages
42
+ (@count / @per_page).to_i + (@count % @per_page > 0 ? 1 : 0)
43
+ end
44
+
45
+ # First page object
46
+ def first
47
+ page 1
48
+ end
49
+
50
+ # Last page object
51
+ def last
52
+ page number_of_pages
53
+ end
54
+
55
+ def each
56
+ 1.upto(number_of_pages) do |number|
57
+ yield page(number)
58
+ end
59
+ end
60
+
61
+ # Retrieve page object by number
62
+ def page(number)
63
+ number = (n = number.to_i) > 0 ? n : 1
64
+ Page.new(self, number, lambda {
65
+ offset = (number - 1) * @per_page
66
+ args = [offset]
67
+ args << @per_page if @select.arity == 2
68
+ @select.call(*args)
69
+ })
70
+ end
71
+
72
+ # Page object
73
+ #
74
+ # Retrieves items for a page and provides metadata about the position
75
+ # of the page in the paginator
76
+ class Page
77
+
78
+ include Enumerable
79
+
80
+ attr_reader :number, :pager
81
+
82
+ def initialize(pager, number, select) #:nodoc:
83
+ @pager, @number = pager, number
84
+ @offset = (number - 1) * pager.per_page
85
+ @select = select
86
+
87
+ @pager.resource.number = number
88
+ end
89
+
90
+ # Retrieve the items for this page
91
+ # * Caches
92
+ def items
93
+ @items ||= @select.call
94
+ end
95
+
96
+ # Checks to see if there's a page before this one
97
+ def prev?
98
+ @number > 1
99
+ end
100
+
101
+ # Get previous page (if possible)
102
+ def prev
103
+ @pager.page(@number - 1) if prev?
104
+ end
105
+
106
+ # Checks to see if there's a page after this one
107
+ def next?
108
+ @number < @pager.number_of_pages
109
+ end
110
+
111
+ # Get next page (if possible)
112
+ def next
113
+ @pager.page(@number + 1) if next?
114
+ end
115
+
116
+ # The "item number" of the first item on this page
117
+ def first_item_number
118
+ 1 + @offset
119
+ end
120
+
121
+ # The "item number" of the last item on this page
122
+ def last_item_number
123
+ if next?
124
+ @offset + @pager.per_page
125
+ else
126
+ @pager.count
127
+ end
128
+ end
129
+
130
+ def ==(other) #:nodoc:
131
+ @pager == other.pager && self.number == other.number
132
+ end
133
+
134
+ def each(&block)
135
+ items.each(&block)
136
+ end
137
+
138
+ def method_missing(meth, *args, &block) #:nodoc:
139
+ if @pager.respond_to?(meth)
140
+ @pager.__send__(meth, *args, &block)
141
+ else
142
+ super
143
+ end
144
+ end
145
+
146
+ end
147
+
148
+ end # class Paginator
149
+ end # module Webby
@@ -0,0 +1,337 @@
1
+ # $Id: spawner.rb 46 2007-11-27 03:31:29Z tim_pease $
2
+
3
+ require 'rbconfig'
4
+ require 'thread'
5
+ require 'tempfile'
6
+
7
+ # == Synopsis
8
+ #
9
+ # A class for spawning child processes and ensuring those children continue
10
+ # running.
11
+ #
12
+ # == Details
13
+ #
14
+ # When a spawner is created it is given the command to run in a child
15
+ # process. This child process has +stdin+, +stdout+, and +stderr+ redirected
16
+ # to +/dev/null+ (this works even on Windows). When the child dies for any
17
+ # reason, the spawner will restart a new child process in the exact same
18
+ # manner as the original.
19
+ #
20
+ class Spawner
21
+
22
+ @dev_null = test(?e, "/dev/null") ? "/dev/null" : "NUL:"
23
+
24
+ c = ::Config::CONFIG
25
+ ruby = File.join(c['bindir'], c['ruby_install_name']) << c['EXEEXT']
26
+ @ruby = if system('%s -e exit' % ruby) then ruby
27
+ elsif system('ruby -e exit') then 'ruby'
28
+ else warn 'no ruby in PATH/CONFIG'
29
+ end
30
+
31
+ class << self
32
+ attr_reader :ruby
33
+ attr_reader :dev_null
34
+
35
+ def finalizer( cids )
36
+ pid = $$
37
+ lambda do
38
+ break unless pid == $$
39
+ cids.kill 'TERM', :all
40
+ end # lambda
41
+ end # finalizer
42
+ end
43
+
44
+ # call-seq:
45
+ # Spawner.new( command, *args, opts = {} )
46
+ #
47
+ # Creates a new spawner that will execute the given external _command_ in
48
+ # a sub-process. The calling semantics of <code>Kernel::exec</code> are
49
+ # used to execute the _command_. Any number of optional _args_ can be
50
+ # passed to the _command_.
51
+ #
52
+ # Available options:
53
+ #
54
+ # :spawn => the number of child processes to spawn
55
+ # :pause => wait time (in seconds) before respawning after termination
56
+ # :ruby => the Ruby interpreter to use when spawning children
57
+ # :env => a hash for the child process environment
58
+ # :stdin => stdin child processes will read from
59
+ # :stdout => stdout child processes will write to
60
+ # :stderr => stderr child processes will write to
61
+ #
62
+ # The <code>:env</code> option is used to add environemnt variables to
63
+ # child processes when they are spawned.
64
+ #
65
+ # *Note:* all spawned child processes will use the same stdin, stdout, and
66
+ # stderr if they are given in the options. Otherwise they all default to
67
+ # <code>/dev/null</code> on *NIX and <code>NUL:</code> on Windows.
68
+ #
69
+ def initialize( *args )
70
+ config = {
71
+ :ruby => self.class.ruby,
72
+ :spawn => 1,
73
+ :pause => 0,
74
+ :stdin => self.class.dev_null,
75
+ :stdout => self.class.dev_null,
76
+ :stderr => self.class.dev_null
77
+ }
78
+ config.merge! args.pop if Hash === args.last
79
+ config[:argv] = args
80
+
81
+ raise ArgumentError, 'wrong number of arguments' if args.empty?
82
+
83
+ @stop = true
84
+ @cids = []
85
+ @group = ThreadGroup.new
86
+
87
+ @spawn = config.delete(:spawn)
88
+ @pause = config.delete(:pause)
89
+ @ruby = config.delete(:ruby)
90
+
91
+ @tmp = child_program(config)
92
+
93
+ class << @cids
94
+ # call-seq:
95
+ # sync {block}
96
+ #
97
+ # Executes the given block in a synchronized fashion -- i.e. only a
98
+ # single thread can execute at a time. Uses Mutex under the hood.
99
+ #
100
+ def sync(&b)
101
+ @mutex ||= Mutex.new
102
+ @mutex.synchronize(&b)
103
+ end
104
+
105
+ # call-seq:
106
+ # kill( signal, num ) => number killed
107
+ # kill( signal, :all ) => number killed
108
+ #
109
+ # Send the _signal_ to a given _num_ of child processes or all child
110
+ # processes if <code>:all</code> is given instead of a number. Returns
111
+ # the number of child processes killed.
112
+ #
113
+ def kill( signal, arg )
114
+ return if empty?
115
+
116
+ ary = sync do
117
+ case arg
118
+ when :all: self.dup
119
+ when Integer: self.slice(0,arg)
120
+ else raise ArgumentError end
121
+ end
122
+
123
+ ary.each do |cid|
124
+ begin
125
+ Process.kill(signal, cid)
126
+ rescue SystemCallError
127
+ sync {delete cid}
128
+ end
129
+ end
130
+ ary.length
131
+ end # def kill
132
+ end # class << @cids
133
+
134
+ end # def initialize
135
+
136
+ attr_reader :spawn
137
+ attr_accessor :pause
138
+
139
+ # call-seq:
140
+ # spawner.spawn = num
141
+ #
142
+ # Set the number of child processes to spawn. If the new spawn number is
143
+ # less than the current number, then spawner threads will die
144
+ #
145
+ def spawn=( num )
146
+ num = num.abs
147
+ diff, @spawn = num - @spawn, num
148
+ return unless running?
149
+
150
+ if diff > 0
151
+ diff.times {_spawn}
152
+ elsif diff < 0
153
+ @cids.kill 'TERM', diff.abs
154
+ end
155
+ end
156
+
157
+ # call-seq:
158
+ # start => self
159
+ #
160
+ # Spawn the sub-processes.
161
+ #
162
+ def start
163
+ return self if running?
164
+ @stop = false
165
+
166
+ @cleanup = Spawner.finalizer(@cids)
167
+ ObjectSpace.define_finalizer(self, @cleanup)
168
+
169
+ @spawn.times {_spawn}
170
+ self
171
+ end
172
+
173
+ # call-seq:
174
+ # stop( timeout = 5 ) => self
175
+ #
176
+ # Stop any spawned sub-processes.
177
+ #
178
+ def stop( timeout = 5 )
179
+ return self unless running?
180
+ @stop = true
181
+
182
+ @cleanup.call
183
+ ObjectSpace.undefine_finalizer(self)
184
+
185
+ # the cleanup call sends SIGTERM to all the child processes
186
+ # however, some might still be hanging around, so we are going to wait
187
+ # for a timeout interval and then send a SIGKILL to any remaining child
188
+ # processes
189
+ nap_time = 0.05 * timeout # sleep for 5% of the timeout interval
190
+ timeout = Time.now + timeout
191
+
192
+ until @cids.empty?
193
+ sleep nap_time
194
+ unless Time.now < timeout
195
+ @cids.kill 'KILL', :all
196
+ @cids.clear
197
+ @group.list.each {|t| t.kill}
198
+ break
199
+ end
200
+ end
201
+
202
+ self
203
+ end
204
+
205
+ # call-seq:
206
+ # restart( timeout = 5 )
207
+ #
208
+ def restart( timeout = 5 )
209
+ stop( timeout )
210
+ start
211
+ end
212
+
213
+ # call-seq:
214
+ # running?
215
+ #
216
+ # Returns +true+ if the spawner is currently running; returns +false+
217
+ # otherwise.
218
+ #
219
+ def running?
220
+ !@stop
221
+ end
222
+
223
+ # call-seq:
224
+ # join( timeout = nil ) => spawner or nil
225
+ #
226
+ # The calling thread will suspend execution until all child processes have
227
+ # been stopped. Does not return until all spawner threads have exited (the
228
+ # child processes have been stopped) or until _timeout seconds have
229
+ # passed. If the timeout expires +nil+ will be returned; otherwise the
230
+ # spawner is returned.
231
+ #
232
+ def join( limit = nil )
233
+ loop do
234
+ t = @group.list.first
235
+ break if t.nil?
236
+ return nil unless t.join(limit)
237
+ end
238
+ self
239
+ end
240
+
241
+
242
+ private
243
+
244
+ # call-seq:
245
+ # _spawn => thread
246
+ #
247
+ # Creates a thread that will spawn the sub-process via
248
+ # <code>IO::popen</code>. If the sub-process terminates, it will be
249
+ # respawned until the +stop+ message is sent to this spawner.
250
+ #
251
+ # If an Exception is encountered during the spawning process, a message
252
+ # will be printed to stderr and the thread will exit.
253
+ #
254
+ def _spawn
255
+ t = Thread.new do
256
+ catch(:die) do
257
+ loop do
258
+ begin
259
+ io = IO.popen("#{@ruby} #{@tmp.path}", 'r')
260
+ cid = io.gets.to_i
261
+
262
+ @cids.sync {@cids << cid} if cid > 0
263
+ Process.wait cid
264
+ rescue Exception => e
265
+ STDERR.puts e.inspect
266
+ STDERR.puts e.backtrace.join("\n")
267
+ throw :die
268
+ ensure
269
+ io.close rescue nil
270
+ @cids.sync {
271
+ @cids.delete cid
272
+ throw :die unless @cids.length < @spawn
273
+ }
274
+ end
275
+
276
+ throw :die if @stop
277
+ sleep @pause
278
+
279
+ end # loop
280
+ end # catch(:die)
281
+ end # Thread.new
282
+
283
+ @group.add t
284
+ t
285
+ end
286
+
287
+ # call-seq:
288
+ # child_program( config ) => tempfile
289
+ #
290
+ # Creates a child Ruby program based on the given _config_ hash. The
291
+ # following hash keys are used:
292
+ #
293
+ # :argv => command and arguments passed to <code>Kernel::exec</code>
294
+ # :env => environment variables for the child process
295
+ # :cwd => the current working directory to use for the child process
296
+ # :stdin => stdin the child process will read from
297
+ # :stdout => stdout the child process will write to
298
+ # :stderr => stderr the child process will write to
299
+ #
300
+ def child_program( config )
301
+ config = Marshal.dump(config)
302
+
303
+ tmp = Tempfile.new(self.class.name.downcase)
304
+ tmp.write <<-PROG
305
+ begin
306
+ config = Marshal.load(#{config.inspect})
307
+
308
+ argv = config[:argv]
309
+ env = config[:env]
310
+ cwd = config[:cwd]
311
+ stdin = config[:stdin]
312
+ stdout = config[:stdout]
313
+ stderr = config[:stderr]
314
+
315
+ Dir.chdir cwd if cwd
316
+ env.each {|k,v| ENV[k.to_s] = v.to_s} if env
317
+ rescue Exception => e
318
+ STDERR.warn e
319
+ abort
320
+ end
321
+
322
+ STDOUT.puts Process.pid
323
+ STDOUT.flush
324
+
325
+ STDIN.reopen stdin
326
+ STDOUT.reopen stdout
327
+ STDERR.reopen stderr
328
+
329
+ exec *argv
330
+ PROG
331
+
332
+ tmp.close
333
+ tmp
334
+ end
335
+ end # class Spawner
336
+
337
+ # EOF