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:
-
“/” - Browsing of gem spec files for installed gems
-
“/specs.#{Gem.marshal_version}.gz” - specs name/version/platform index
-
“/latest_specs.#{Gem.marshal_version}.gz” - latest specs name/version/platform index
-
“/quick/” - Individual gemspecs
-
“/gems” - Direct access to download the installable gems
-
“/rdoc?q=” - Search for installed rdoc documentation
Usage
gem_server = Gem::Server.new Gem.dir, 8089, false gem_server.run
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 430
def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil)
begin
require 'webrick'
rescue LoadError
abort "webrick is not found. You may need to `gem install webrick` to install webrick."
end
Gem::RDoc.load_rdoc
Socket.do_not_reverse_lookup = true
@gem_dirs = Array gem_dirs
@port = port
@daemon = daemon
@launch = launch
@addresses = addresses
logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL
@server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger
@spec_dirs = @gem_dirs.map {|gem_dir| File.join gem_dir, 'specifications' }
@spec_dirs.reject! {|spec_dir| !File.directory? spec_dir }
reset_gems
@have_rdoc_4_plus = nil
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 425
def self.run(options)
new(options[:gemdir], options[:port], options[:daemon],
options[:launch], options[:addresses]).run
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 457
def add_date(res)
res['date'] = @spec_dirs.map do |spec_dir|
File.stat(spec_dir).mtime
end.max
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 469
def doc_root(gem_name)
if have_rdoc_4_plus?
"/doc_root/#{u gem_name}/"
else
"/doc_root/#{u gem_name}/rdoc/index.html"
end
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 477
def have_rdoc_4_plus?
@have_rdoc_4_plus ||=
Gem::Requirement.new('>= 4.0.0.preview2').satisfied_by? Gem::RDoc.rdoc_version
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 482
def latest_specs(req, res)
reset_gems
res['content-type'] = 'application/x-gzip'
add_date res
latest_specs = Gem::Specification.latest_specs
specs = latest_specs.sort.map do |spec|
platform = spec.original_platform || Gem::Platform::RUBY
[spec.name, spec.version, platform]
end
specs = Marshal.dump specs
if req.path =~ /\.gz$/
specs = Gem::Util.gzip specs
res['content-type'] = 'application/x-gzip'
else
res['content-type'] = 'application/octet-stream'
end
if req.request_method == 'HEAD'
res['content-length'] = specs.length
else
res.body << specs
end
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 872
def launch
listeners = @server.listeners.map{|l| l.addr[2] }
# TODO: 0.0.0.0 == any, not localhost.
host = listeners.any?{|l| l == '0.0.0.0' } ? 'localhost' : listeners.first
say "Launching browser to http://#{host}:#{@port}"
system("#{@launch} http://#{host}:#{@port}")
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 516
def listen(addresses = @addresses)
addresses = [nil] unless addresses
listeners = 0
addresses.each do |address|
begin
@server.listen address, @port
@server.listeners[listeners..-1].each do |listener|
host, port = listener.addr.values_at 2, 1
host = "[#{host}]" if host =~ /:/ # we don't reverse lookup
say "Server started at http://#{host}:#{port}"
end
listeners = @server.listeners.length
rescue SystemCallError
next
end
end
if @server.listeners.empty?
say "Unable to start a server."
say "Check for running servers or your --bind and --port arguments"
terminate_interaction 1
end
end
Creates server sockets based on the addresses option. If no addresses were given a server socket for all interfaces is created.
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 543
def prerelease_specs(req, res)
reset_gems
res['content-type'] = 'application/x-gzip'
add_date res
specs = Gem::Specification.select do |spec|
spec.version.prerelease?
end.sort.map do |spec|
platform = spec.original_platform || Gem::Platform::RUBY
[spec.name, spec.version, platform]
end
specs = Marshal.dump specs
if req.path =~ /\.gz$/
specs = Gem::Util.gzip specs
res['content-type'] = 'application/x-gzip'
else
res['content-type'] = 'application/octet-stream'
end
if req.request_method == 'HEAD'
res['content-length'] = specs.length
else
res.body << specs
end
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 573
def quick(req, res)
reset_gems
res['content-type'] = 'text/plain'
add_date res
case req.request_uri.path
when %r{^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)\.gemspec\.rz$} then
marshal_format, full_name = $1, $2
specs = Gem::Specification.find_all_by_full_name(full_name)
selector = full_name.inspect
if specs.empty?
res.status = 404
res.body = "No gems found matching #{selector}"
elsif specs.length > 1
res.status = 500
res.body = "Multiple gems found matching #{selector}"
elsif marshal_format
res['content-type'] = 'application/x-deflate'
res.body << Gem.deflate(Marshal.dump(specs.first))
end
else
raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
end
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 734
def rdoc(req, res)
query = req.query['q']
show_rdoc_for_pattern("#{query}*", res) && return
show_rdoc_for_pattern("*#{query}*", res) && return
template = ERB.new RDOC_NO_DOCUMENTATION
res['content-type'] = 'text/html'
res.body = template.result binding
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:
-
first try to find the gems and documentation folders which name starts with the search term
-
search for entries, that contain the search term
-
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
-
install Ruby sources
cd /usr/src sudo apt-get source ruby
-
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
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 601
def root(req, res)
reset_gems
add_date res
raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
req.path == '/'
specs = []
total_file_count = 0
Gem::Specification.each do |spec|
total_file_count += spec.files.size
deps = spec.dependencies.map do |dep|
{
"name" => dep.name,
"type" => dep.type,
"version" => dep.requirement.to_s,
}
end
deps = deps.sort_by {|dep| [dep["name"].downcase, dep["version"]] }
deps.last["is_last"] = true unless deps.empty?
# executables
executables = spec.executables.sort.collect {|exec| {"executable" => exec} }
executables = nil if executables.empty?
executables.last["is_last"] = true if executables
# Pre-process spec homepage for safety reasons
begin
homepage_uri = URI.parse(spec.homepage)
if [URI::HTTP, URI::HTTPS].member? homepage_uri.class
homepage_uri = spec.homepage
else
homepage_uri = "."
end
rescue URI::InvalidURIError
homepage_uri = "."
end
specs << {
"authors" => spec.authors.sort.join(", "),
"date" => spec.date.to_s,
"dependencies" => deps,
"doc_path" => doc_root(spec.full_name),
"executables" => executables,
"only_one_executable" => (executables && executables.size == 1),
"full_name" => spec.full_name,
"has_deps" => !deps.empty?,
"homepage" => homepage_uri,
"name" => spec.name,
"rdoc_installed" => Gem::RDoc.new(spec).rdoc_installed?,
"ri_installed" => Gem::RDoc.new(spec).ri_installed?,
"summary" => spec.summary,
"version" => spec.version.to_s,
}
end
specs << {
"authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others",
"dependencies" => [],
"doc_path" => doc_root("rubygems-#{Gem::VERSION}"),
"executables" => [{"executable" => 'gem', "is_last" => true}],
"only_one_executable" => true,
"full_name" => "rubygems-#{Gem::VERSION}",
"has_deps" => false,
"homepage" => "https://guides.rubygems.org/",
"name" => 'rubygems',
"ri_installed" => true,
"summary" => "RubyGems itself",
"version" => Gem::VERSION,
}
specs = specs.sort_by {|spec| [spec["name"].downcase, spec["version"]] }
specs.last["is_last"] = true
# tag all specs with first_name_entry
last_spec = nil
specs.each do |spec|
is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase)
spec["first_name_entry"] = is_first
last_spec = spec
end
# create page from template
template = ERB.new(DOC_TEMPLATE)
res['content-type'] = 'text/html'
values = { "gem_count" => specs.size.to_s, "specs" => specs,
"total_file_count" => total_file_count.to_s }
# suppress 1.9.3dev warning about unused variable
values = values
result = template.result binding
res.body = result
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 791
def run
listen
WEBrick::Daemon.start if @daemon
@server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs)
@server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs)
@server.mount_proc "/latest_specs.#{Gem.marshal_version}",
method(:latest_specs)
@server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz",
method(:latest_specs)
@server.mount_proc "/prerelease_specs.#{Gem.marshal_version}",
method(:prerelease_specs)
@server.mount_proc "/prerelease_specs.#{Gem.marshal_version}.gz",
method(:prerelease_specs)
@server.mount_proc "/quick/", method(:quick)
@server.mount_proc("/gem-server-rdoc-style.css") do |req, res|
res['content-type'] = 'text/css'
add_date res
res.body << RDOC_CSS
end
@server.mount_proc "/", method(:root)
@server.mount_proc "/rdoc", method(:rdoc)
file_handlers = {
'/gems' => '/cache/',
}
if have_rdoc_4_plus?
@server.mount '/doc_root', RDoc::Servlet, '/doc_root'
else
file_handlers['/doc_root'] = '/doc/'
end
@gem_dirs.each do |gem_dir|
file_handlers.each do |mount_point, mount_dir|
@server.mount(mount_point, WEBrick::HTTPServlet::FileHandler,
File.join(gem_dir, mount_dir), true)
end
end
trap("INT") { @server.shutdown; exit! }
trap("TERM") { @server.shutdown; exit! }
launch if @launch
@server.start
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 760
def show_rdoc_for_pattern(pattern, res)
found_gems = Dir.glob("{#{@gem_dirs.join ','}}/doc/#{pattern}").select do |path|
File.exist? File.join(path, 'rdoc/index.html')
end
case found_gems.length
when 0
return false
when 1
new_path = File.basename(found_gems[0])
res.status = 302
res['Location'] = doc_root new_path
return true
else
doc_items = []
found_gems.each do |file_name|
base_name = File.basename(file_name)
doc_items << {
:name => base_name,
:url => doc_root(new_path),
:summary => '',
}
end
template = ERB.new(RDOC_SEARCH_TEMPLATE)
res['content-type'] = 'text/html'
result = template.result binding
res.body = result
return true
end
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.
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 846
def specs(req, res)
reset_gems
add_date res
specs = Gem::Specification.sort_by(&:sort_obj).map do |spec|
platform = spec.original_platform || Gem::Platform::RUBY
[spec.name, spec.version, platform]
end
specs = Marshal.dump specs
if req.path =~ /\.gz$/
specs = Gem::Util.gzip specs
res['content-type'] = 'application/x-gzip'
else
res['content-type'] = 'application/octet-stream'
end
if req.request_method == 'HEAD'
res['content-length'] = specs.length
else
res.body << specs
end
end
# File tmp/rubies/ruby-3.0.5/lib/rubygems/server.rb, line 463
def uri_encode(str)
str.gsub(URI::UNSAFE) do |match|
match.each_byte.map {|c| sprintf('%%%02X', c.ord) }.join
end
end