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