webby 0.4.0 → 0.5.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.
@@ -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