diff options
author | Jari Vetoniemi <jari.vetoniemi@indooratlas.com> | 2020-03-16 18:49:26 +0900 |
---|---|---|
committer | Jari Vetoniemi <jari.vetoniemi@indooratlas.com> | 2020-03-30 00:39:06 +0900 |
commit | fcbf63e62c627deae76c1b8cb8c0876c536ed811 (patch) | |
tree | 64cb17de3f41a2b6fef2368028fbd00349946994 /jni/ruby/lib/rubygems/server.rb |
Fresh start
Diffstat (limited to 'jni/ruby/lib/rubygems/server.rb')
-rw-r--r-- | jni/ruby/lib/rubygems/server.rb | 868 |
1 files changed, 868 insertions, 0 deletions
diff --git a/jni/ruby/lib/rubygems/server.rb b/jni/ruby/lib/rubygems/server.rb new file mode 100644 index 0000000..7655be2 --- /dev/null +++ b/jni/ruby/lib/rubygems/server.rb @@ -0,0 +1,868 @@ +require 'webrick' +require 'zlib' +require 'erb' + +require 'rubygems' +require 'rubygems/rdoc' + +## +# 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 +# +#-- +# TODO Refactor into a real WEBrick servlet to remove code duplication. + +class Gem::Server + + attr_reader :spec_dirs + + include ERB::Util + include Gem::UserInteraction + + 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["ri_installed"] then %> + <a href="<%=spec["doc_path"]%>">[rdoc]</a> + <% elsif 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 + + # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108 + 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 + + 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 + + def self.run(options) + new(options[:gemdir], options[:port], options[:daemon], + options[:launch], options[:addresses]).run + end + + def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil) + 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 + + def add_date res + res['date'] = @spec_dirs.map do |spec_dir| + File.stat(spec_dir).mtime + end.max + end + + def doc_root gem_name + if have_rdoc_4_plus? then + "/doc_root/#{gem_name}/" + else + "/doc_root/#{gem_name}/rdoc/index.html" + end + end + + def have_rdoc_4_plus? + @have_rdoc_4_plus ||= + Gem::Requirement.new('>= 4.0.0.preview2').satisfied_by? Gem::RDoc.rdoc_version + end + + 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$/ then + specs = Gem.gzip specs + res['content-type'] = 'application/x-gzip' + else + res['content-type'] = 'application/octet-stream' + end + + if req.request_method == 'HEAD' then + res['content-length'] = specs.length + else + res.body << specs + end + end + + ## + # Creates server sockets based on the addresses option. If no addresses + # were given a server socket for all interfaces is created. + + 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? then + say "Unable to start a server." + say "Check for running servers or your --bind and --port arguments" + terminate_interaction 1 + end + end + + 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$/ then + specs = Gem.gzip specs + res['content-type'] = 'application/x-gzip' + else + res['content-type'] = 'application/octet-stream' + end + + if req.request_method == 'HEAD' then + res['content-length'] = specs.length + else + res.body << specs + end + end + + 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}/)?(.*?)-([0-9.]+[^-]*?)(-.*?)?\.gemspec\.rz$| then + marshal_format, name, version, platform = $1, $2, $3, $4 + specs = Gem::Specification.find_all_by_name name, version + + selector = [name, version, platform].map(&:inspect).join ' ' + + platform = if platform then + Gem::Platform.new platform.sub(/^-/, '') + else + Gem::Platform::RUBY + end + + specs = specs.select { |s| s.platform == platform } + + if specs.empty? then + res.status = 404 + res.body = "No gems found matching #{selector}" + elsif specs.length > 1 then + res.status = 500 + res.body = "Multiple gems found matching #{selector}" + elsif marshal_format then + 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 + + 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 { |dep| + { + "name" => dep.name, + "type" => dep.type, + "version" => dep.requirement.to_s, + } + } + + 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 + + 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" => spec.homepage, + "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" => "http://docs.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 + + ## + # 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 http://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 + + 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 + + ## + # Updates the server to use the latest installed gems. + + def reset_gems # :nodoc: + Gem::Specification.dirs = @gem_dirs + 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. + + def show_rdoc_for_pattern(pattern, res) + found_gems = Dir.glob("{#{@gem_dirs.join ','}}/doc/#{pattern}").select {|path| + File.exist? File.join(path, 'rdoc/index.html') + } + 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 + + 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? then + @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 + + 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$/ then + specs = Gem.gzip specs + res['content-type'] = 'application/x-gzip' + else + res['content-type'] = 'application/octet-stream' + end + + if req.request_method == 'HEAD' then + res['content-length'] = specs.length + else + res.body << specs + end + end + + 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 +end |