PageRenderTime 434ms CodeModel.GetById 389ms app.highlight 40ms RepoModel.GetById 1ms app.codeStats 0ms

/tools/Ruby/lib/ruby/site_ruby/1.8/rubygems/server.rb

http://github.com/agross/netopenspace
Ruby | 832 lines | 608 code | 151 blank | 73 comment | 35 complexity | c1d4e4c5c134fc87ec3adf14db094dea MD5 | raw file
  1require 'webrick'
  2require 'zlib'
  3require 'erb'
  4
  5require 'rubygems'
  6require 'rubygems/doc_manager'
  7
  8##
  9# Gem::Server and allows users to serve gems for consumption by
 10# `gem --remote-install`.
 11#
 12# gem_server starts an HTTP server on the given port and serves the following:
 13# * "/" - Browsing of gem spec files for installed gems
 14# * "/specs.#{Gem.marshal_version}.gz" - specs name/version/platform index
 15# * "/latest_specs.#{Gem.marshal_version}.gz" - latest specs
 16#   name/version/platform index
 17# * "/quick/" - Individual gemspecs
 18# * "/gems" - Direct access to download the installable gems
 19# * "/rdoc?q=" - Search for installed rdoc documentation
 20# * legacy indexes:
 21#   * "/Marshal.#{Gem.marshal_version}" - Full SourceIndex dump of metadata
 22#     for installed gems
 23#
 24# == Usage
 25#
 26#   gem_server = Gem::Server.new Gem.dir, 8089, false
 27#   gem_server.run
 28#
 29#--
 30# TODO Refactor into a real WEBrick servlet to remove code duplication.
 31
 32class Gem::Server
 33
 34  attr_reader :spec_dirs
 35
 36  include ERB::Util
 37  include Gem::UserInteraction
 38
 39  SEARCH = <<-SEARCH
 40      <form class="headerSearch" name="headerSearchForm" method="get" action="/rdoc">
 41        <div id="search" style="float:right">
 42          <label for="q">Filter/Search</label>
 43          <input id="q" type="text" style="width:10em" name="q">
 44          <button type="submit" style="display:none"></button>
 45        </div>
 46      </form>
 47  SEARCH
 48
 49  DOC_TEMPLATE = <<-'DOC_TEMPLATE'
 50  <?xml version="1.0" encoding="iso-8859-1"?>
 51  <!DOCTYPE html
 52       PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 53       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 54
 55  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 56  <head>
 57    <title>RubyGems Documentation Index</title>
 58    <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
 59  </head>
 60  <body>
 61    <div id="fileHeader">
 62<%= SEARCH %>
 63      <h1>RubyGems Documentation Index</h1>
 64    </div>
 65    <!-- banner header -->
 66
 67  <div id="bodyContent">
 68    <div id="contextContent">
 69      <div id="description">
 70        <h1>Summary</h1>
 71  <p>There are <%=values["gem_count"]%> gems installed:</p>
 72  <p>
 73  <%= values["specs"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>.
 74  <h1>Gems</h1>
 75
 76  <dl>
 77  <% values["specs"].each do |spec| %>
 78  	<dt>
 79  	<% if spec["first_name_entry"] then %>
 80  	  <a name="<%=spec["name"]%>"></a>
 81  	<% end  %>
 82
 83  	<b><%=spec["name"]%> <%=spec["version"]%></b>
 84
 85  	<% if  spec["rdoc_installed"] then %>
 86  	  <a href="<%=spec["doc_path"]%>">[rdoc]</a>
 87  	<% else %>
 88  	  <span title="rdoc not installed">[rdoc]</span>
 89  	<% end %>
 90
 91  	<% if spec["homepage"] then %>
 92  		<a href="<%=spec["homepage"]%>" title="<%=spec["homepage"]%>">[www]</a>
 93  	<% else %>
 94  		<span title="no homepage available">[www]</span>
 95  	<% end  %>
 96
 97  	<% if spec["has_deps"] then %>
 98  	 - depends on
 99  		<%= spec["dependencies"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>.
100  	<% end %>
101  	</dt>
102  	<dd>
103  	<%=spec["summary"]%>
104  	<% if spec["executables"] then %>
105  	  <br/>
106
107  		<% if  spec["only_one_executable"] then %>
108  		    Executable is
109  		<% else %>
110  		    Executables are
111  		<%end%>
112
113  		<%= spec["executables"].map { |v| "<span class=\"context-item-name\">#{v["executable"]}</span>"}.join ', ' %>.
114
115  	<%end%>
116  	<br/>
117  	<br/>
118  	</dd>
119  <% end %>
120  </dl>
121
122      </div>
123     </div>
124    </div>
125  <div id="validator-badges">
126    <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
127  </div>
128  </body>
129  </html>
130  DOC_TEMPLATE
131
132  # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108
133  RDOC_CSS = <<-RDOC_CSS
134body {
135    font-family: Verdana,Arial,Helvetica,sans-serif;
136    font-size:   90%;
137    margin: 0;
138    margin-left: 40px;
139    padding: 0;
140    background: white;
141}
142
143h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; }
144h1 { font-size: 150%; }
145h2,h3,h4 { margin-top: 1em; }
146
147a { background: #eef; color: #039; text-decoration: none; }
148a:hover { background: #039; color: #eef; }
149
150/* Override the base stylesheets Anchor inside a table cell */
151td > a {
152  background: transparent;
153  color: #039;
154  text-decoration: none;
155}
156
157/* and inside a section title */
158.section-title > a {
159  background: transparent;
160  color: #eee;
161  text-decoration: none;
162}
163
164/* === Structural elements =================================== */
165
166div#index {
167    margin: 0;
168    margin-left: -40px;
169    padding: 0;
170    font-size: 90%;
171}
172
173
174div#index a {
175    margin-left: 0.7em;
176}
177
178div#index .section-bar {
179   margin-left: 0px;
180   padding-left: 0.7em;
181   background: #ccc;
182   font-size: small;
183}
184
185
186div#classHeader, div#fileHeader {
187    width: auto;
188    color: white;
189    padding: 0.5em 1.5em 0.5em 1.5em;
190    margin: 0;
191    margin-left: -40px;
192    border-bottom: 3px solid #006;
193}
194
195div#classHeader a, div#fileHeader a {
196    background: inherit;
197    color: white;
198}
199
200div#classHeader td, div#fileHeader td {
201    background: inherit;
202    color: white;
203}
204
205
206div#fileHeader {
207    background: #057;
208}
209
210div#classHeader {
211    background: #048;
212}
213
214
215.class-name-in-header {
216  font-size:  180%;
217  font-weight: bold;
218}
219
220
221div#bodyContent {
222    padding: 0 1.5em 0 1.5em;
223}
224
225div#description {
226    padding: 0.5em 1.5em;
227    background: #efefef;
228    border: 1px dotted #999;
229}
230
231div#description h1,h2,h3,h4,h5,h6 {
232    color: #125;;
233    background: transparent;
234}
235
236div#validator-badges {
237    text-align: center;
238}
239div#validator-badges img { border: 0; }
240
241div#copyright {
242    color: #333;
243    background: #efefef;
244    font: 0.75em sans-serif;
245    margin-top: 5em;
246    margin-bottom: 0;
247    padding: 0.5em 2em;
248}
249
250
251/* === Classes =================================== */
252
253table.header-table {
254    color: white;
255    font-size: small;
256}
257
258.type-note {
259    font-size: small;
260    color: #DEDEDE;
261}
262
263.xxsection-bar {
264    background: #eee;
265    color: #333;
266    padding: 3px;
267}
268
269.section-bar {
270   color: #333;
271   border-bottom: 1px solid #999;
272    margin-left: -20px;
273}
274
275
276.section-title {
277    background: #79a;
278    color: #eee;
279    padding: 3px;
280    margin-top: 2em;
281    margin-left: -30px;
282    border: 1px solid #999;
283}
284
285.top-aligned-row {  vertical-align: top }
286.bottom-aligned-row { vertical-align: bottom }
287
288/* --- Context section classes ----------------------- */
289
290.context-row { }
291.context-item-name { font-family: monospace; font-weight: bold; color: black; }
292.context-item-value { font-size: small; color: #448; }
293.context-item-desc { color: #333; padding-left: 2em; }
294
295/* --- Method classes -------------------------- */
296.method-detail {
297    background: #efefef;
298    padding: 0;
299    margin-top: 0.5em;
300    margin-bottom: 1em;
301    border: 1px dotted #ccc;
302}
303.method-heading {
304  color: black;
305  background: #ccc;
306  border-bottom: 1px solid #666;
307  padding: 0.2em 0.5em 0 0.5em;
308}
309.method-signature { color: black; background: inherit; }
310.method-name { font-weight: bold; }
311.method-args { font-style: italic; }
312.method-description { padding: 0 0.5em 0 0.5em; }
313
314/* --- Source code sections -------------------- */
315
316a.source-toggle { font-size: 90%; }
317div.method-source-code {
318    background: #262626;
319    color: #ffdead;
320    margin: 1em;
321    padding: 0.5em;
322    border: 1px dashed #999;
323    overflow: hidden;
324}
325
326div.method-source-code pre { color: #ffdead; overflow: hidden; }
327
328/* --- Ruby keyword styles --------------------- */
329
330.standalone-code { background: #221111; color: #ffdead; overflow: hidden; }
331
332.ruby-constant  { color: #7fffd4; background: transparent; }
333.ruby-keyword { color: #00ffff; background: transparent; }
334.ruby-ivar    { color: #eedd82; background: transparent; }
335.ruby-operator  { color: #00ffee; background: transparent; }
336.ruby-identifier { color: #ffdead; background: transparent; }
337.ruby-node    { color: #ffa07a; background: transparent; }
338.ruby-comment { color: #b22222; font-weight: bold; background: transparent; }
339.ruby-regexp  { color: #ffa07a; background: transparent; }
340.ruby-value   { color: #7fffd4; background: transparent; }
341  RDOC_CSS
342
343  RDOC_NO_DOCUMENTATION = <<-'NO_DOC'
344<?xml version="1.0" encoding="iso-8859-1"?>
345<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
346          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
347<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
348  <head>
349    <title>Found documentation</title>
350    <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
351  </head>
352  <body>
353    <div id="fileHeader">
354<%= SEARCH %>
355      <h1>No documentation found</h1>
356    </div>
357
358    <div id="bodyContent">
359      <div id="contextContent">
360        <div id="description">
361          <p>No gems matched <%= h query.inspect %></p>
362
363          <p>
364            Back to <a href="/">complete gem index</a>
365          </p>
366
367        </div>
368      </div>
369    </div>
370    <div id="validator-badges">
371      <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
372    </div>
373  </body>
374</html>
375  NO_DOC
376
377  RDOC_SEARCH_TEMPLATE = <<-'RDOC_SEARCH'
378<?xml version="1.0" encoding="iso-8859-1"?>
379<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
380          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
381<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
382  <head>
383    <title>Found documentation</title>
384    <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
385  </head>
386  <body>
387    <div id="fileHeader">
388<%= SEARCH %>
389      <h1>Found documentation</h1>
390    </div>
391    <!-- banner header -->
392
393    <div id="bodyContent">
394      <div id="contextContent">
395        <div id="description">
396          <h1>Summary</h1>
397          <p><%=doc_items.length%> documentation topics found.</p>
398          <h1>Topics</h1>
399
400          <dl>
401          <% doc_items.each do |doc_item| %>
402            <dt>
403              <b><%=doc_item[:name]%></b>
404              <a href="<%=doc_item[:url]%>">[rdoc]</a>
405            </dt>
406            <dd>
407              <%=doc_item[:summary]%>
408              <br/>
409              <br/>
410            </dd>
411          <% end %>
412          </dl>
413
414          <p>
415            Back to <a href="/">complete gem index</a>
416          </p>
417
418        </div>
419      </div>
420    </div>
421    <div id="validator-badges">
422      <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
423    </div>
424  </body>
425</html>
426  RDOC_SEARCH
427
428  def self.run(options)
429    new(options[:gemdir], options[:port], options[:daemon],
430        options[:launch], options[:addresses]).run
431  end
432
433  ##
434  # Only the first directory in gem_dirs is used for serving gems
435
436  def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil)
437    Socket.do_not_reverse_lookup = true
438
439    @gem_dirs = Array gem_dirs
440    @port = port
441    @daemon = daemon
442    @launch = launch
443    @addresses = addresses
444    logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL
445    @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger
446
447    @spec_dirs = @gem_dirs.map do |gem_dir|
448      spec_dir = File.join gem_dir, 'specifications'
449
450      unless File.directory? spec_dir then
451        raise ArgumentError, "#{gem_dir} does not appear to be a gem repository"
452      end
453
454      spec_dir
455    end
456
457    @source_index = Gem::SourceIndex.new(@spec_dirs)
458  end
459
460  def Marshal(req, res)
461    @source_index.refresh!
462
463    add_date res
464
465    index = Marshal.dump @source_index
466
467    if req.request_method == 'HEAD' then
468      res['content-length'] = index.length
469      return
470    end
471
472    if req.path =~ /Z$/ then
473      res['content-type'] = 'application/x-deflate'
474      index = Gem.deflate index
475    else
476      res['content-type'] = 'application/octet-stream'
477    end
478
479    res.body << index
480  end
481
482  def add_date res
483    res['date'] = @spec_dirs.map do |spec_dir|
484      File.stat(spec_dir).mtime
485    end.max
486  end
487
488  def latest_specs(req, res)
489    @source_index.refresh!
490
491    res['content-type'] = 'application/x-gzip'
492
493    add_date res
494
495    specs = @source_index.latest_specs.sort.map do |spec|
496      platform = spec.original_platform
497      platform = Gem::Platform::RUBY if platform.nil?
498      [spec.name, spec.version, platform]
499    end
500
501    specs = Marshal.dump specs
502
503    if req.path =~ /\.gz$/ then
504      specs = Gem.gzip specs
505      res['content-type'] = 'application/x-gzip'
506    else
507      res['content-type'] = 'application/octet-stream'
508    end
509
510    if req.request_method == 'HEAD' then
511      res['content-length'] = specs.length
512    else
513      res.body << specs
514    end
515  end
516
517  ##
518  # Creates server sockets based on the addresses option.  If no addresses
519  # were given a server socket for all interfaces is created.
520
521  def listen addresses = @addresses
522    addresses = [nil] unless addresses
523
524    listeners = 0
525
526    addresses.each do |address|
527      begin
528        @server.listen address, @port
529        @server.listeners[listeners..-1].each do |listener|
530          host, port = listener.addr.values_at 2, 1
531          host = "[#{host}]" if host =~ /:/ # we don't reverse lookup
532          say "Server started at http://#{host}:#{port}"
533        end
534
535        listeners = @server.listeners.length
536      rescue SystemCallError
537        next
538      end
539    end
540
541    if @server.listeners.empty? then
542      say "Unable to start a server."
543      say "Check for running servers or your --bind and --port arguments"
544      terminate_interaction 1
545    end
546  end
547
548  def quick(req, res)
549    @source_index.refresh!
550
551    res['content-type'] = 'text/plain'
552    add_date res
553
554    case req.request_uri.path
555    when %r|^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)-([0-9.]+)(-.*?)?\.gemspec\.rz$| then
556      dep = Gem::Dependency.new $2, $3
557      specs = @source_index.search dep
558      marshal_format = $1
559
560      selector = [$2, $3, $4].map { |s| s.inspect }.join ' '
561
562      platform = if $4 then
563                   Gem::Platform.new $4.sub(/^-/, '')
564                 else
565                   Gem::Platform::RUBY
566                 end
567
568      specs = specs.select { |s| s.platform == platform }
569
570      if specs.empty? then
571        res.status = 404
572        res.body = "No gems found matching #{selector}"
573      elsif specs.length > 1 then
574        res.status = 500
575        res.body = "Multiple gems found matching #{selector}"
576      elsif marshal_format then
577        res['content-type'] = 'application/x-deflate'
578        res.body << Gem.deflate(Marshal.dump(specs.first))
579      end
580    else
581      raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
582    end
583  end
584
585  def root(req, res)
586    @source_index.refresh!
587    add_date res
588
589    raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
590      req.path == '/'
591
592    specs = []
593    total_file_count = 0
594
595    @source_index.each do |path, spec|
596      total_file_count += spec.files.size
597      deps = spec.dependencies.map do |dep|
598        { "name"    => dep.name,
599          "type"    => dep.type,
600          "version" => dep.requirement.to_s, }
601      end
602
603      deps = deps.sort_by { |dep| [dep["name"].downcase, dep["version"]] }
604      deps.last["is_last"] = true unless deps.empty?
605
606      # executables
607      executables = spec.executables.sort.collect { |exec| {"executable" => exec} }
608      executables = nil if executables.empty?
609      executables.last["is_last"] = true if executables
610
611      specs << {
612        "authors"             => spec.authors.sort.join(", "),
613        "date"                => spec.date.to_s,
614        "dependencies"        => deps,
615        "doc_path"            => "/doc_root/#{spec.full_name}/rdoc/index.html",
616        "executables"         => executables,
617        "only_one_executable" => (executables && executables.size == 1),
618        "full_name"           => spec.full_name,
619        "has_deps"            => !deps.empty?,
620        "homepage"            => spec.homepage,
621        "name"                => spec.name,
622        "rdoc_installed"      => Gem::DocManager.new(spec).rdoc_installed?,
623        "summary"             => spec.summary,
624        "version"             => spec.version.to_s,
625      }
626    end
627
628    specs << {
629      "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others",
630      "dependencies" => [],
631      "doc_path" => "/doc_root/rubygems-#{Gem::VERSION}/rdoc/index.html",
632      "executables" => [{"executable" => 'gem', "is_last" => true}],
633      "only_one_executable" => true,
634      "full_name" => "rubygems-#{Gem::VERSION}",
635      "has_deps" => false,
636      "homepage" => "http://docs.rubygems.org/",
637      "name" => 'rubygems',
638      "rdoc_installed" => true,
639      "summary" => "RubyGems itself",
640      "version" => Gem::VERSION,
641    }
642
643    specs = specs.sort_by { |spec| [spec["name"].downcase, spec["version"]] }
644    specs.last["is_last"] = true
645
646    # tag all specs with first_name_entry
647    last_spec = nil
648    specs.each do |spec|
649      is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase)
650      spec["first_name_entry"] = is_first
651      last_spec = spec
652    end
653
654    # create page from template
655    template = ERB.new(DOC_TEMPLATE)
656    res['content-type'] = 'text/html'
657
658    values = { "gem_count" => specs.size.to_s, "specs" => specs,
659               "total_file_count" => total_file_count.to_s }
660
661    # suppress 1.9.3dev warning about unused variable
662    values = values
663
664    result = template.result binding
665    res.body = result
666  end
667
668  ##
669  # Can be used for quick navigation to the rdoc documentation.  You can then
670  # define a search shortcut for your browser.  E.g. in Firefox connect
671  # 'shortcut:rdoc' to http://localhost:8808/rdoc?q=%s template. Then you can
672  # directly open the ActionPack documentation by typing 'rdoc actionp'. If
673  # there are multiple hits for the search term, they are presented as a list
674  # with links.
675  #
676  # Search algorithm aims for an intuitive search:
677  # 1. first try to find the gems and documentation folders which name
678  #    starts with the search term
679  # 2. search for entries, that *contain* the search term
680  # 3. show all the gems
681  #
682  # If there is only one search hit, user is immediately redirected to the
683  # documentation for the particular gem, otherwise a list with results is
684  # shown.
685  #
686  # === Additional trick - install documentation for ruby core
687  #
688  # Note: please adjust paths accordingly use for example 'locate yaml.rb' and
689  # 'gem environment' to identify directories, that are specific for your
690  # local installation
691  #
692  # 1. install ruby sources
693  #      cd /usr/src
694  #      sudo apt-get source ruby
695  #
696  # 2. generate documentation
697  #      rdoc -o /usr/lib/ruby/gems/1.8/doc/core/rdoc \
698  #        /usr/lib/ruby/1.8 ruby1.8-1.8.7.72
699  #
700  # By typing 'rdoc core' you can now access the core documentation
701
702  def rdoc(req, res)
703    query = req.query['q']
704    show_rdoc_for_pattern("#{query}*", res) && return
705    show_rdoc_for_pattern("*#{query}*", res) && return
706
707    template = ERB.new RDOC_NO_DOCUMENTATION
708
709    res['content-type'] = 'text/html'
710    res.body = template.result binding
711  end
712
713  ##
714  # Returns true and prepares http response, if rdoc for the requested gem
715  # name pattern was found.
716  #
717  # The search is based on the file system content, not on the gems metadata.
718  # This allows additional documentation folders like 'core' for the ruby core
719  # documentation - just put it underneath the main doc folder.
720
721  def show_rdoc_for_pattern(pattern, res)
722    found_gems = Dir.glob("{#{@gem_dirs.join ','}}/doc/#{pattern}").select {|path|
723      File.exist? File.join(path, 'rdoc/index.html')
724    }
725    case found_gems.length
726    when 0
727      return false
728    when 1
729      new_path = File.basename(found_gems[0])
730      res.status = 302
731      res['Location'] = "/doc_root/#{new_path}/rdoc/index.html"
732      return true
733    else
734      doc_items = []
735      found_gems.each do |file_name|
736        base_name = File.basename(file_name)
737        doc_items << {
738          :name => base_name,
739          :url => "/doc_root/#{base_name}/rdoc/index.html",
740          :summary => ''
741        }
742      end
743
744      template = ERB.new(RDOC_SEARCH_TEMPLATE)
745      res['content-type'] = 'text/html'
746      result = template.result binding
747      res.body = result
748      return true
749    end
750  end
751
752  def run
753    listen
754
755    WEBrick::Daemon.start if @daemon
756
757    @server.mount_proc "/Marshal.#{Gem.marshal_version}", method(:Marshal)
758    @server.mount_proc "/Marshal.#{Gem.marshal_version}.Z", method(:Marshal)
759
760    @server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs)
761    @server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs)
762
763    @server.mount_proc "/latest_specs.#{Gem.marshal_version}",
764                       method(:latest_specs)
765    @server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz",
766                       method(:latest_specs)
767
768    @server.mount_proc "/quick/", method(:quick)
769
770    @server.mount_proc("/gem-server-rdoc-style.css") do |req, res|
771      res['content-type'] = 'text/css'
772      add_date res
773      res.body << RDOC_CSS
774    end
775
776    @server.mount_proc "/", method(:root)
777
778    @server.mount_proc "/rdoc", method(:rdoc)
779
780    paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" }
781    paths.each do |mount_point, mount_dir|
782      @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler,
783                    File.join(@gem_dirs.first, mount_dir), true)
784    end
785
786    trap("INT") { @server.shutdown; exit! }
787    trap("TERM") { @server.shutdown; exit! }
788
789    launch if @launch
790
791    @server.start
792  end
793
794  def specs(req, res)
795    @source_index.refresh!
796
797    add_date res
798
799    specs = @source_index.sort.map do |_, spec|
800      platform = spec.original_platform
801      platform = Gem::Platform::RUBY if platform.nil?
802      [spec.name, spec.version, platform]
803    end
804
805    specs = Marshal.dump specs
806
807    if req.path =~ /\.gz$/ then
808      specs = Gem.gzip specs
809      res['content-type'] = 'application/x-gzip'
810    else
811      res['content-type'] = 'application/octet-stream'
812    end
813
814    if req.request_method == 'HEAD' then
815      res['content-length'] = specs.length
816    else
817      res.body << specs
818    end
819  end
820
821  def launch
822    listeners = @server.listeners.map{|l| l.addr[2] }
823
824    host = listeners.any?{|l| l == '0.0.0.0'} ? 'localhost' : listeners.first
825
826    say "Launching browser to http://#{host}:#{@port}"
827
828    system("#{@launch} http://#{host}:#{@port}")
829  end
830
831end
832