Class Gem::Server
In: lib/rubygems/server.rb
Parent: Object

Gem::Server and allows users to serve gems for consumption by `gem —remote-install`.

gem_server starts an HTTP server on the given port and serves the following:

Usage

  gem_server = Gem::Server.new Gem.dir, 8089, false
  gem_server.run

Methods

Marshal   add_date   latest_specs   launch   listen   new   quick   rdoc   root   run   run   show_rdoc_for_pattern   specs  

Included Modules

ERB::Util Gem::UserInteraction

Constants

SEARCH = <<-SEARCH <form class="headerSearch" name="headerSearchForm" method="get" action="/rdoc"> <div id="search" style="float:right"> <label for="q">Filter/Search</label> <input id="q" type="text" style="width:10em" name="q"> <button type="submit" style="display:none"></button> </div> </form> SEARCH
DOC_TEMPLATE = <<-'DOC_TEMPLATE' <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>RubyGems Documentation Index</title> <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> </head> <body> <div id="fileHeader"> <%= SEARCH %> <h1>RubyGems Documentation Index</h1> </div> <!-- banner header --> <div id="bodyContent"> <div id="contextContent"> <div id="description"> <h1>Summary</h1> <p>There are <%=values["gem_count"]%> gems installed:</p> <p> <%= values["specs"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>. <h1>Gems</h1> <dl> <% values["specs"].each do |spec| %> <dt> <% if spec["first_name_entry"] then %> <a name="<%=spec["name"]%>"></a> <% end %> <b><%=spec["name"]%> <%=spec["version"]%></b> <% if spec["rdoc_installed"] then %> <a href="<%=spec["doc_path"]%>">[rdoc]</a> <% else %> <span title="rdoc not installed">[rdoc]</span> <% end %> <% if spec["homepage"] then %> <a href="<%=spec["homepage"]%>" title="<%=spec["homepage"]%>">[www]</a> <% else %> <span title="no homepage available">[www]</span> <% end %> <% if spec["has_deps"] then %> - depends on <%= spec["dependencies"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>. <% end %> </dt> <dd> <%=spec["summary"]%> <% if spec["executables"] then %> <br/> <% if spec["only_one_executable"] then %> Executable is <% else %> Executables are <%end%> <%= spec["executables"].map { |v| "<span class=\"context-item-name\">#{v["executable"]}</span>"}.join ', ' %>. <%end%> <br/> <br/> </dd> <% end %> </dl> </div> </div> </div> <div id="validator-badges"> <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> </div> </body> </html> DOC_TEMPLATE
RDOC_CSS = <<-RDOC_CSS body { font-family: Verdana,Arial,Helvetica,sans-serif; font-size: 90%; margin: 0; margin-left: 40px; padding: 0; background: white; } h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; } h1 { font-size: 150%; } h2,h3,h4 { margin-top: 1em; } a { background: #eef; color: #039; text-decoration: none; } a:hover { background: #039; color: #eef; } /* Override the base stylesheets Anchor inside a table cell */ td > a { background: transparent; color: #039; text-decoration: none; } /* and inside a section title */ .section-title > a { background: transparent; color: #eee; text-decoration: none; } /* === Structural elements =================================== */ div#index { margin: 0; margin-left: -40px; padding: 0; font-size: 90%; } div#index a { margin-left: 0.7em; } div#index .section-bar { margin-left: 0px; padding-left: 0.7em; background: #ccc; font-size: small; } div#classHeader, div#fileHeader { width: auto; color: white; padding: 0.5em 1.5em 0.5em 1.5em; margin: 0; margin-left: -40px; border-bottom: 3px solid #006; } div#classHeader a, div#fileHeader a { background: inherit; color: white; } div#classHeader td, div#fileHeader td { background: inherit; color: white; } div#fileHeader { background: #057; } div#classHeader { background: #048; } .class-name-in-header { font-size: 180%; font-weight: bold; } div#bodyContent { padding: 0 1.5em 0 1.5em; } div#description { padding: 0.5em 1.5em; background: #efefef; border: 1px dotted #999; } div#description h1,h2,h3,h4,h5,h6 { color: #125;; background: transparent; } div#validator-badges { text-align: center; } div#validator-badges img { border: 0; } div#copyright { color: #333; background: #efefef; font: 0.75em sans-serif; margin-top: 5em; margin-bottom: 0; padding: 0.5em 2em; } /* === Classes =================================== */ table.header-table { color: white; font-size: small; } .type-note { font-size: small; color: #DEDEDE; } .xxsection-bar { background: #eee; color: #333; padding: 3px; } .section-bar { color: #333; border-bottom: 1px solid #999; margin-left: -20px; } .section-title { background: #79a; color: #eee; padding: 3px; margin-top: 2em; margin-left: -30px; border: 1px solid #999; } .top-aligned-row { vertical-align: top } .bottom-aligned-row { vertical-align: bottom } /* --- Context section classes ----------------------- */ .context-row { } .context-item-name { font-family: monospace; font-weight: bold; color: black; } .context-item-value { font-size: small; color: #448; } .context-item-desc { color: #333; padding-left: 2em; } /* --- Method classes -------------------------- */ .method-detail { background: #efefef; padding: 0; margin-top: 0.5em; margin-bottom: 1em; border: 1px dotted #ccc; } .method-heading { color: black; background: #ccc; border-bottom: 1px solid #666; padding: 0.2em 0.5em 0 0.5em; } .method-signature { color: black; background: inherit; } .method-name { font-weight: bold; } .method-args { font-style: italic; } .method-description { padding: 0 0.5em 0 0.5em; } /* --- Source code sections -------------------- */ a.source-toggle { font-size: 90%; } div.method-source-code { background: #262626; color: #ffdead; margin: 1em; padding: 0.5em; border: 1px dashed #999; overflow: hidden; } div.method-source-code pre { color: #ffdead; overflow: hidden; } /* --- Ruby keyword styles --------------------- */ .standalone-code { background: #221111; color: #ffdead; overflow: hidden; } .ruby-constant { color: #7fffd4; background: transparent; } .ruby-keyword { color: #00ffff; background: transparent; } .ruby-ivar { color: #eedd82; background: transparent; } .ruby-operator { color: #00ffee; background: transparent; } .ruby-identifier { color: #ffdead; background: transparent; } .ruby-node { color: #ffa07a; background: transparent; } .ruby-comment { color: #b22222; font-weight: bold; background: transparent; } .ruby-regexp { color: #ffa07a; background: transparent; } .ruby-value { color: #7fffd4; background: transparent; } RDOC_CSS   CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108
RDOC_NO_DOCUMENTATION = <<-'NO_DOC' <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Found documentation</title> <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> </head> <body> <div id="fileHeader"> <%= SEARCH %> <h1>No documentation found</h1> </div> <div id="bodyContent"> <div id="contextContent"> <div id="description"> <p>No gems matched <%= h query.inspect %></p> <p> Back to <a href="/">complete gem index</a> </p> </div> </div> </div> <div id="validator-badges"> <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> </div> </body> </html> NO_DOC
RDOC_SEARCH_TEMPLATE = <<-'RDOC_SEARCH' <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Found documentation</title> <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> </head> <body> <div id="fileHeader"> <%= SEARCH %> <h1>Found documentation</h1> </div> <!-- banner header --> <div id="bodyContent"> <div id="contextContent"> <div id="description"> <h1>Summary</h1> <p><%=doc_items.length%> documentation topics found.</p> <h1>Topics</h1> <dl> <% doc_items.each do |doc_item| %> <dt> <b><%=doc_item[:name]%></b> <a href="<%=doc_item[:url]%>">[rdoc]</a> </dt> <dd> <%=doc_item[:summary]%> <br/> <br/> </dd> <% end %> </dl> <p> Back to <a href="/">complete gem index</a> </p> </div> </div> </div> <div id="validator-badges"> <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> </div> </body> </html> RDOC_SEARCH

Attributes

spec_dirs  [R] 

Public Class methods

Only the first directory in gem_dirs is used for serving gems

[Source]

     # File lib/rubygems/server.rb, line 441
441:   def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil)
442:     Socket.do_not_reverse_lookup = true
443: 
444:     @gem_dirs = Array gem_dirs
445:     @port = port
446:     @daemon = daemon
447:     @launch = launch
448:     @addresses = addresses
449:     logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL
450:     @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger
451: 
452:     @spec_dirs = @gem_dirs.map do |gem_dir|
453:       spec_dir = File.join gem_dir, 'specifications'
454: 
455:       unless File.directory? spec_dir then
456:         raise ArgumentError, "#{gem_dir} does not appear to be a gem repository"
457:       end
458: 
459:       spec_dir
460:     end
461: 
462:     @source_index = Gem::SourceIndex.new(@spec_dirs)
463:   end

[Source]

     # File lib/rubygems/server.rb, line 433
433:   def self.run(options)
434:     new(options[:gemdir], options[:port], options[:daemon],
435:         options[:launch], options[:addresses]).run
436:   end

Public Instance methods

[Source]

     # File lib/rubygems/server.rb, line 465
465:   def Marshal(req, res)
466:     @source_index.refresh!
467: 
468:     add_date res
469: 
470:     index = Marshal.dump @source_index
471: 
472:     if req.request_method == 'HEAD' then
473:       res['content-length'] = index.length
474:       return
475:     end
476: 
477:     if req.path =~ /Z$/ then
478:       res['content-type'] = 'application/x-deflate'
479:       index = Gem.deflate index
480:     else
481:       res['content-type'] = 'application/octet-stream'
482:     end
483: 
484:     res.body << index
485:   end

[Source]

     # File lib/rubygems/server.rb, line 487
487:   def add_date res
488:     res['date'] = @spec_dirs.map do |spec_dir|
489:       File.stat(spec_dir).mtime
490:     end.max
491:   end

[Source]

     # File lib/rubygems/server.rb, line 493
493:   def latest_specs(req, res)
494:     @source_index.refresh!
495: 
496:     res['content-type'] = 'application/x-gzip'
497: 
498:     add_date res
499: 
500:     specs = @source_index.latest_specs.sort.map do |spec|
501:       platform = spec.original_platform
502:       platform = Gem::Platform::RUBY if platform.nil?
503:       [spec.name, spec.version, platform]
504:     end
505: 
506:     specs = Marshal.dump specs
507: 
508:     if req.path =~ /\.gz$/ then
509:       specs = Gem.gzip specs
510:       res['content-type'] = 'application/x-gzip'
511:     else
512:       res['content-type'] = 'application/octet-stream'
513:     end
514: 
515:     if req.request_method == 'HEAD' then
516:       res['content-length'] = specs.length
517:     else
518:       res.body << specs
519:     end
520:   end

[Source]

     # File lib/rubygems/server.rb, line 826
826:   def launch
827:     listeners = @server.listeners.map{|l| l.addr[2] }
828: 
829:     host = listeners.any?{|l| l == '0.0.0.0'} ? 'localhost' : listeners.first
830: 
831:     say "Launching browser to http://#{host}:#{@port}"
832: 
833:     system("#{@launch} http://#{host}:#{@port}")
834:   end

Creates server sockets based on the addresses option. If no addresses were given a server socket for all interfaces is created.

[Source]

     # File lib/rubygems/server.rb, line 526
526:   def listen addresses = @addresses
527:     addresses = [nil] unless addresses
528: 
529:     listeners = 0
530: 
531:     addresses.each do |address|
532:       begin
533:         @server.listen address, @port
534:         @server.listeners[listeners..-1].each do |listener|
535:           host, port = listener.addr.values_at 2, 1
536:           host = "[#{host}]" if host =~ /:/ # we don't reverse lookup
537:           say "Server started at http://#{host}:#{port}"
538:         end
539: 
540:         listeners = @server.listeners.length
541:       rescue SystemCallError
542:         next
543:       end
544:     end
545: 
546:     if @server.listeners.empty? then
547:       say "Unable to start a server."
548:       say "Check for running servers or your --bind and --port arguments"
549:       terminate_interaction 1
550:     end
551:   end

[Source]

     # File lib/rubygems/server.rb, line 553
553:   def quick(req, res)
554:     @source_index.refresh!
555: 
556:     res['content-type'] = 'text/plain'
557:     add_date res
558: 
559:     case req.request_uri.path
560:     when %r|^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)-([0-9.]+)(-.*?)?\.gemspec\.rz$| then
561:       dep = Gem::Dependency.new $2, $3
562:       specs = @source_index.search dep
563:       marshal_format = $1
564: 
565:       selector = [$2, $3, $4].map { |s| s.inspect }.join ' '
566: 
567:       platform = if $4 then
568:                    Gem::Platform.new $4.sub(/^-/, '')
569:                  else
570:                    Gem::Platform::RUBY
571:                  end
572: 
573:       specs = specs.select { |s| s.platform == platform }
574: 
575:       if specs.empty? then
576:         res.status = 404
577:         res.body = "No gems found matching #{selector}"
578:       elsif specs.length > 1 then
579:         res.status = 500
580:         res.body = "Multiple gems found matching #{selector}"
581:       elsif marshal_format then
582:         res['content-type'] = 'application/x-deflate'
583:         res.body << Gem.deflate(Marshal.dump(specs.first))
584:       end
585:     else
586:       raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
587:     end
588:   end

Can be used for quick navigation to the rdoc documentation. You can then define a search shortcut for your browser. E.g. in Firefox connect ‘shortcut:rdoc’ to localhost:8808/rdoc?q=%s template. Then you can directly open the ActionPack documentation by typing ‘rdoc actionp’. If there are multiple hits for the search term, they are presented as a list with links.

Search algorithm aims for an intuitive search:

  1. first try to find the gems and documentation folders which name starts with the search term
  2. search for entries, that contain the search term
  3. show all the gems

If there is only one search hit, user is immediately redirected to the documentation for the particular gem, otherwise a list with results is shown.

Additional trick - install documentation for ruby core

Note: please adjust paths accordingly use for example ‘locate yaml.rb’ and ‘gem environment’ to identify directories, that are specific for your local installation

  1. install ruby sources
      cd /usr/src
      sudo apt-get source ruby
    
  2. generate documentation
      rdoc -o /usr/lib/ruby/gems/1.8/doc/core/rdoc    #        /usr/lib/ruby/1.8 ruby1.8-1.8.7.72
    

By typing ‘rdoc core’ you can now access the core documentation

[Source]

     # File lib/rubygems/server.rb, line 707
707:   def rdoc(req, res)
708:     query = req.query['q']
709:     show_rdoc_for_pattern("#{query}*", res) && return
710:     show_rdoc_for_pattern("*#{query}*", res) && return
711: 
712:     template = ERB.new RDOC_NO_DOCUMENTATION
713: 
714:     res['content-type'] = 'text/html'
715:     res.body = template.result binding
716:   end

[Source]

     # File lib/rubygems/server.rb, line 590
590:   def root(req, res)
591:     @source_index.refresh!
592:     add_date res
593: 
594:     raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
595:       req.path == '/'
596: 
597:     specs = []
598:     total_file_count = 0
599: 
600:     @source_index.each do |path, spec|
601:       total_file_count += spec.files.size
602:       deps = spec.dependencies.map do |dep|
603:         { "name"    => dep.name,
604:           "type"    => dep.type,
605:           "version" => dep.requirement.to_s, }
606:       end
607: 
608:       deps = deps.sort_by { |dep| [dep["name"].downcase, dep["version"]] }
609:       deps.last["is_last"] = true unless deps.empty?
610: 
611:       # executables
612:       executables = spec.executables.sort.collect { |exec| {"executable" => exec} }
613:       executables = nil if executables.empty?
614:       executables.last["is_last"] = true if executables
615: 
616:       specs << {
617:         "authors"             => spec.authors.sort.join(", "),
618:         "date"                => spec.date.to_s,
619:         "dependencies"        => deps,
620:         "doc_path"            => "/doc_root/#{spec.full_name}/rdoc/index.html",
621:         "executables"         => executables,
622:         "only_one_executable" => (executables && executables.size == 1),
623:         "full_name"           => spec.full_name,
624:         "has_deps"            => !deps.empty?,
625:         "homepage"            => spec.homepage,
626:         "name"                => spec.name,
627:         "rdoc_installed"      => Gem::DocManager.new(spec).rdoc_installed?,
628:         "summary"             => spec.summary,
629:         "version"             => spec.version.to_s,
630:       }
631:     end
632: 
633:     specs << {
634:       "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others",
635:       "dependencies" => [],
636:       "doc_path" => "/doc_root/rubygems-#{Gem::VERSION}/rdoc/index.html",
637:       "executables" => [{"executable" => 'gem', "is_last" => true}],
638:       "only_one_executable" => true,
639:       "full_name" => "rubygems-#{Gem::VERSION}",
640:       "has_deps" => false,
641:       "homepage" => "http://docs.rubygems.org/",
642:       "name" => 'rubygems',
643:       "rdoc_installed" => true,
644:       "summary" => "RubyGems itself",
645:       "version" => Gem::VERSION,
646:     }
647: 
648:     specs = specs.sort_by { |spec| [spec["name"].downcase, spec["version"]] }
649:     specs.last["is_last"] = true
650: 
651:     # tag all specs with first_name_entry
652:     last_spec = nil
653:     specs.each do |spec|
654:       is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase)
655:       spec["first_name_entry"] = is_first
656:       last_spec = spec
657:     end
658: 
659:     # create page from template
660:     template = ERB.new(DOC_TEMPLATE)
661:     res['content-type'] = 'text/html'
662: 
663:     values = { "gem_count" => specs.size.to_s, "specs" => specs,
664:                "total_file_count" => total_file_count.to_s }
665: 
666:     # suppress 1.9.3dev warning about unused variable
667:     values = values
668: 
669:     result = template.result binding
670:     res.body = result
671:   end

[Source]

     # File lib/rubygems/server.rb, line 757
757:   def run
758:     listen
759: 
760:     WEBrick::Daemon.start if @daemon
761: 
762:     @server.mount_proc "/Marshal.#{Gem.marshal_version}", method(:Marshal)
763:     @server.mount_proc "/Marshal.#{Gem.marshal_version}.Z", method(:Marshal)
764: 
765:     @server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs)
766:     @server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs)
767: 
768:     @server.mount_proc "/latest_specs.#{Gem.marshal_version}",
769:                        method(:latest_specs)
770:     @server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz",
771:                        method(:latest_specs)
772: 
773:     @server.mount_proc "/quick/", method(:quick)
774: 
775:     @server.mount_proc("/gem-server-rdoc-style.css") do |req, res|
776:       res['content-type'] = 'text/css'
777:       add_date res
778:       res.body << RDOC_CSS
779:     end
780: 
781:     @server.mount_proc "/", method(:root)
782: 
783:     @server.mount_proc "/rdoc", method(:rdoc)
784: 
785:     paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" }
786:     paths.each do |mount_point, mount_dir|
787:       @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler,
788:                     File.join(@gem_dirs.first, mount_dir), true)
789:     end
790: 
791:     trap("INT") { @server.shutdown; exit! }
792:     trap("TERM") { @server.shutdown; exit! }
793: 
794:     launch if @launch
795: 
796:     @server.start
797:   end

Returns true and prepares http response, if rdoc for the requested gem name pattern was found.

The search is based on the file system content, not on the gems metadata. This allows additional documentation folders like ‘core’ for the ruby core documentation - just put it underneath the main doc folder.

[Source]

     # File lib/rubygems/server.rb, line 726
726:   def show_rdoc_for_pattern(pattern, res)
727:     found_gems = Dir.glob("{#{@gem_dirs.join ','}}/doc/#{pattern}").select {|path|
728:       File.exist? File.join(path, 'rdoc/index.html')
729:     }
730:     case found_gems.length
731:     when 0
732:       return false
733:     when 1
734:       new_path = File.basename(found_gems[0])
735:       res.status = 302
736:       res['Location'] = "/doc_root/#{new_path}/rdoc/index.html"
737:       return true
738:     else
739:       doc_items = []
740:       found_gems.each do |file_name|
741:         base_name = File.basename(file_name)
742:         doc_items << {
743:           :name => base_name,
744:           :url => "/doc_root/#{base_name}/rdoc/index.html",
745:           :summary => ''
746:         }
747:       end
748: 
749:       template = ERB.new(RDOC_SEARCH_TEMPLATE)
750:       res['content-type'] = 'text/html'
751:       result = template.result binding
752:       res.body = result
753:       return true
754:     end
755:   end

[Source]

     # File lib/rubygems/server.rb, line 799
799:   def specs(req, res)
800:     @source_index.refresh!
801: 
802:     add_date res
803: 
804:     specs = @source_index.sort.map do |_, spec|
805:       platform = spec.original_platform
806:       platform = Gem::Platform::RUBY if platform.nil?
807:       [spec.name, spec.version, platform]
808:     end
809: 
810:     specs = Marshal.dump specs
811: 
812:     if req.path =~ /\.gz$/ then
813:       specs = Gem.gzip specs
814:       res['content-type'] = 'application/x-gzip'
815:     else
816:       res['content-type'] = 'application/octet-stream'
817:     end
818: 
819:     if req.request_method == 'HEAD' then
820:       res['content-length'] = specs.length
821:     else
822:       res.body << specs
823:     end
824:   end

[Validate]