├── History.txt ├── bin └── starling ├── test ├── test_helper.rb ├── test_persistent_queue.rb └── test_starling.rb ├── Rakefile ├── tasks ├── environment.rake ├── website.rake └── deployment.rake ├── sample-config.yml ├── config ├── requirements.rb └── hoe.rb ├── script ├── destroy ├── generate └── txt2html ├── Manifest.txt ├── License.txt ├── README.txt ├── website ├── template.rhtml ├── index.txt ├── stylesheets │ └── screen.css ├── index.html └── javascripts │ └── rounded_corners_lite.inc.js ├── lib ├── starling.rb └── starling │ ├── server.rb │ ├── queue_collection.rb │ ├── persistent_queue.rb │ ├── handler.rb │ └── runner.rb └── setup.rb /History.txt: -------------------------------------------------------------------------------- 1 | == 1.0.0 2007-11-02 2 | 3 | * Initial release 4 | -------------------------------------------------------------------------------- /bin/starling: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'starling/runner' 4 | StarlingServer::Runner.run 5 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | $:.unshift(File.join(File.dirname(__FILE__), "..", "lib")) 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'config/requirements' 2 | require 'config/hoe' # setup Hoe + all gem configuration 3 | 4 | Dir['tasks/**/*.rake'].each { |rake| load rake } -------------------------------------------------------------------------------- /tasks/environment.rake: -------------------------------------------------------------------------------- 1 | task :ruby_env do 2 | RUBY_APP = if RUBY_PLATFORM =~ /java/ 3 | "jruby" 4 | else 5 | "ruby" 6 | end unless defined? RUBY_APP 7 | end 8 | -------------------------------------------------------------------------------- /sample-config.yml: -------------------------------------------------------------------------------- 1 | 2 | starling: 3 | port: 22122 4 | pid_file: /tmp/starling.pid 5 | queue_path: /tmp 6 | timeout: 0 7 | syslog_channel: starling-tampopo 8 | log_level: DEBUG 9 | daemonize: true 10 | 11 | -------------------------------------------------------------------------------- /config/requirements.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | include FileUtils 3 | 4 | require 'rubygems' 5 | %w[rake hoe newgem rubigen].each do |req_gem| 6 | begin 7 | require req_gem 8 | rescue LoadError 9 | puts "This Rakefile requires the '#{req_gem}' RubyGem." 10 | puts "Installation: gem install #{req_gem} -y" 11 | exit 12 | end 13 | end 14 | 15 | $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib])) 16 | -------------------------------------------------------------------------------- /script/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.join(File.dirname(__FILE__), '..') 3 | 4 | begin 5 | require 'rubigen' 6 | rescue LoadError 7 | require 'rubygems' 8 | require 'rubigen' 9 | end 10 | require 'rubigen/scripts/destroy' 11 | 12 | ARGV.shift if ['--help', '-h'].include?(ARGV[0]) 13 | RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme] 14 | RubiGen::Scripts::Destroy.new.run(ARGV) 15 | -------------------------------------------------------------------------------- /script/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.join(File.dirname(__FILE__), '..') 3 | 4 | begin 5 | require 'rubigen' 6 | rescue LoadError 7 | require 'rubygems' 8 | require 'rubigen' 9 | end 10 | require 'rubigen/scripts/generate' 11 | 12 | ARGV.shift if ['--help', '-h'].include?(ARGV[0]) 13 | RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme] 14 | RubiGen::Scripts::Generate.new.run(ARGV) 15 | -------------------------------------------------------------------------------- /test/test_persistent_queue.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper.rb') 2 | 3 | require 'digest/md5' 4 | require 'starling/server' 5 | 6 | class TestQueueCollection < Test::Unit::TestCase 7 | def test_creating_queue_collection_with_invalid_path_throws_inaccessible_queue_exception 8 | invalid_path = nil 9 | while invalid_path.nil? || File.exist?(invalid_path) 10 | invalid_path = File.join('/', Digest::MD5.hexdigest(rand(2**32-1).to_s)[0,8]) 11 | end 12 | 13 | assert_raises(StarlingServer::InaccessibleQueuePath) { 14 | StarlingServer::QueueCollection.new(invalid_path) 15 | } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /tasks/website.rake: -------------------------------------------------------------------------------- 1 | desc 'Generate website files' 2 | task :website_generate => :ruby_env do 3 | (Dir['website/**/*.txt'] - Dir['website/version*.txt']).each do |txt| 4 | sh %{ #{RUBY_APP} script/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} } 5 | end 6 | end 7 | 8 | desc 'Upload website files to rubyforge' 9 | task :website_upload do 10 | host = "#{rubyforge_username}@rubyforge.org" 11 | remote_dir = "/var/www/gforge-projects/#{PATH}/" 12 | local_dir = 'website' 13 | sh %{rsync -aCv #{local_dir}/ #{host}:#{remote_dir}} 14 | end 15 | 16 | desc 'Generate and upload website files' 17 | task :website => [:website_generate, :website_upload, :publish_docs] 18 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | History.txt 2 | License.txt 3 | Manifest.txt 4 | README.txt 5 | Rakefile 6 | bin/starling 7 | config/hoe.rb 8 | config/requirements.rb 9 | lib/starling.rb 10 | lib/starling/handler.rb 11 | lib/starling/persistent_queue.rb 12 | lib/starling/queue_collection.rb 13 | lib/starling/runner.rb 14 | lib/starling/server.rb 15 | script/destroy 16 | script/generate 17 | script/txt2html 18 | setup.rb 19 | tasks/deployment.rake 20 | tasks/environment.rake 21 | tasks/website.rake 22 | test/test_helper.rb 23 | test/test_persistent_queue.rb 24 | test/test_starling.rb 25 | website/index.html 26 | website/index.txt 27 | website/javascripts/rounded_corners_lite.inc.js 28 | website/stylesheets/screen.css 29 | website/template.rhtml 30 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 FIXME full name 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tasks/deployment.rake: -------------------------------------------------------------------------------- 1 | desc 'Release the website and new gem version' 2 | task :deploy => [:check_version, :website, :release] do 3 | puts "Remember to create SVN tag:" 4 | puts "svn copy svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/trunk " + 5 | "svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} " 6 | puts "Suggested comment:" 7 | puts "Tagging release #{CHANGES}" 8 | end 9 | 10 | desc 'Runs tasks website_generate and install_gem as a local deployment of the gem' 11 | task :local_deploy => [:website_generate, :install_gem] 12 | 13 | task :check_version do 14 | unless ENV['VERSION'] 15 | puts 'Must pass a VERSION=x.y.z release version' 16 | exit 17 | end 18 | unless ENV['VERSION'] == VERS 19 | puts "Please update your version.rb to match the release version, currently #{VERS}" 20 | exit 21 | end 22 | end 23 | 24 | desc 'Install the package as a gem, without generating documentation(ri/rdoc)' 25 | task :install_gem_no_doc => [:clean, :package] do 26 | sh "#{'sudo ' unless Hoe::WINDOZE }gem install pkg/*.gem --no-rdoc --no-ri" 27 | end 28 | 29 | namespace :manifest do 30 | desc 'Recreate Manifest.txt to include ALL files' 31 | task :refresh do 32 | `rake check_manifest | patch -p0 > Manifest.txt` 33 | end 34 | end -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | = Name 2 | 3 | Starling - a light weight server for reliable distributed message passing. 4 | 5 | = Synopsis 6 | 7 | # Start the Starling server as a daemonized process: 8 | starling -h 192.168.1.1 -d 9 | 10 | # Put messages onto a queue: 11 | require 'memcache' 12 | starling = MemCache.new('192.168.1.1:22122') 13 | starling.set('my_queue', 12345) 14 | 15 | # Get messages from the queue: 16 | require 'memcache' 17 | starling = MemCache.new('192.168.1.1:22122') 18 | loop { puts starling.get('my_queue') } 19 | 20 | # See the Starling documentation for more information. 21 | 22 | = Description 23 | 24 | Starling is a powerful but simple messaging server that enables reliable 25 | distributed queuing with an absolutely minimal overhead. It speaks the 26 | MemCache protocol for maximum cross-platform compatibility. Any language 27 | that speaks MemCache can take advantage of Starling's queue facilities. 28 | 29 | = Known Issues 30 | 31 | * Starling is "slow" as far as messaging systems are concerned. In practice, 32 | it's fast enough. 33 | 34 | = Authors 35 | 36 | Blaine Cook 37 | 38 | = Copyright 39 | 40 | Starling - a light-weight server for reliable distributed message passing. 41 | Copyright 2007 Blaine Cook , Twitter Inc. 42 | 43 | <> 44 | -------------------------------------------------------------------------------- /website/template.rhtml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | <%= title %> 9 | 10 | 11 | 14 | 29 | 30 | 31 |
32 | 33 |

<%= title %>

34 |
35 |

Get Version

36 | <%= version %> 37 |
38 | <%= body %> 39 |

40 | FIXME full name, <%= modified.pretty %>
41 | Theme extended from Paul Battley 42 |

43 |
44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /website/index.txt: -------------------------------------------------------------------------------- 1 | h1. Starling 2 | 3 | h2. What 4 | 5 | Starling is a light-weight server for distributed message passing. 6 | 7 | h2. Installing 8 | 9 |
sudo gem install starling
10 | 11 | h2. The basics 12 | 13 | # Start the Starling server. 14 | # Put messages onto a queue from a client whose work you'd like to offload to a deferred process. 15 | # Run a daemon process to take messages off the queue, and process as time allows. 16 | 17 | h2. Demonstration of usage 18 | 19 | # Start the Starling server: 20 | 21 |
22 | 
23 |     starling -h 192.168.1.1 -d
24 | 
25 | 
26 | 27 | # Queue messages for later processing: 28 | 29 |
30 | 
31 |     require 'memcache'
32 |     starling = MemCache.new('192.168.1.1:22122')
33 |     starling.set('my_queue', 12345)
34 | 
35 | 
36 | 37 | # Fetch messages from the queue and do something with them: 38 | 39 |
40 | 
41 |     require 'memcache'
42 |     starling = MemCache.new('192.168.1.1:22122')
43 |     loop { puts starling.get('my_queue') }
44 | 
45 | 
46 | 47 | h2. Forum 48 | 49 | "http://groups.google.com/group/starling":http://groups.google.com/group/starling 50 | 51 | TODO - create Google Group - starling 52 | 53 | h2. How to submit patches 54 | 55 | Read the "8 steps for fixing other people's code":http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/ and for section "8b: Submit patch to Google Groups":http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/#8b-google-groups, use the Google Group above. 56 | 57 | The trunk repository is svn://rubyforge.org/var/svn/starling/trunk for anonymous access. 58 | 59 | h2. License 60 | 61 | This code is free to use under the terms of the MIT license. 62 | 63 | h2. Contact 64 | 65 | Comments are welcome. Send an email to "FIXME full name":mailto:FIXME email via the "forum":http://groups.google.com/group/starling 66 | 67 | -------------------------------------------------------------------------------- /script/txt2html: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | begin 5 | require 'newgem' 6 | rescue LoadError 7 | puts "\n\nGenerating the website requires the newgem RubyGem" 8 | puts "Install: gem install newgem\n\n" 9 | exit(1) 10 | end 11 | require 'redcloth' 12 | require 'syntax/convertors/html' 13 | require 'erb' 14 | require File.join(File.dirname(__FILE__), '..', 'lib', 'starling', 'server') 15 | 16 | version = Starling::VERSION 17 | download = 'http://rubyforge.org/projects/starling' 18 | 19 | class Fixnum 20 | def ordinal 21 | # teens 22 | return 'th' if (10..19).include?(self % 100) 23 | # others 24 | case self % 10 25 | when 1: return 'st' 26 | when 2: return 'nd' 27 | when 3: return 'rd' 28 | else return 'th' 29 | end 30 | end 31 | end 32 | 33 | class Time 34 | def pretty 35 | return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}" 36 | end 37 | end 38 | 39 | def convert_syntax(syntax, source) 40 | return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^
|
$!,'') 41 | end 42 | 43 | if ARGV.length >= 1 44 | src, template = ARGV 45 | template ||= File.join(File.dirname(__FILE__), '/../website/template.rhtml') 46 | 47 | else 48 | puts("Usage: #{File.split($0).last} source.txt [template.rhtml] > output.html") 49 | exit! 50 | end 51 | 52 | template = ERB.new(File.open(template).read) 53 | 54 | title = nil 55 | body = nil 56 | File.open(src) do |fsrc| 57 | title_text = fsrc.readline 58 | body_text = fsrc.read 59 | syntax_items = [] 60 | body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)!m){ 61 | ident = syntax_items.length 62 | element, syntax, source = $1, $2, $3 63 | syntax_items << "<#{element} class='syntax'>#{convert_syntax(syntax, source)}" 64 | "syntax-temp-#{ident}" 65 | } 66 | title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip 67 | body = RedCloth.new(body_text).to_html 68 | body.gsub!(%r!(?:
)?syntax-temp-(\d+)(?:
)?!){ syntax_items[$1.to_i] } 69 | end 70 | stat = File.stat(src) 71 | created = stat.ctime 72 | modified = stat.mtime 73 | 74 | $stdout << template.result(binding) 75 | -------------------------------------------------------------------------------- /config/hoe.rb: -------------------------------------------------------------------------------- 1 | require 'starling/server' 2 | 3 | AUTHOR = 'Blaine Cook' # can also be an array of Authors 4 | EMAIL = "blaine@twitter.com" 5 | DESCRIPTION = "Starling is a lightweight, transactional, distributed queue server" 6 | GEM_NAME = 'starling' # what ppl will type to install your gem 7 | RUBYFORGE_PROJECT = 'starling' # The unix name for your project 8 | HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org" 9 | DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}" 10 | 11 | @config_file = "~/.rubyforge/user-config.yml" 12 | @config = nil 13 | RUBYFORGE_USERNAME = "unknown" 14 | def rubyforge_username 15 | unless @config 16 | begin 17 | @config = YAML.load(File.read(File.expand_path(@config_file))) 18 | rescue 19 | puts <<-EOS 20 | ERROR: No rubyforge config file found: #{@config_file} 21 | Run 'rubyforge setup' to prepare your env for access to Rubyforge 22 | - See http://newgem.rubyforge.org/rubyforge.html for more details 23 | EOS 24 | exit 25 | end 26 | end 27 | RUBYFORGE_USERNAME.replace @config["username"] 28 | end 29 | 30 | 31 | REV = nil 32 | # UNCOMMENT IF REQUIRED: 33 | # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil 34 | VERS = StarlingServer::VERSION + (REV ? ".#{REV}" : "") 35 | RDOC_OPTS = ['--quiet', '--title', 'starling documentation', 36 | "--opname", "index.html", 37 | "--line-numbers", 38 | "--main", "README", 39 | "--inline-source"] 40 | 41 | class Hoe 42 | def extra_deps 43 | @extra_deps.reject! { |x| Array(x).first == 'hoe' } 44 | @extra_deps 45 | end 46 | end 47 | 48 | # Generate all the Rake tasks 49 | # Run 'rake -T' to see list of generated tasks (from gem root directory) 50 | hoe = Hoe.new(GEM_NAME, VERS) do |p| 51 | p.author = AUTHOR 52 | p.description = DESCRIPTION 53 | p.email = EMAIL 54 | p.summary = DESCRIPTION 55 | p.url = HOMEPATH 56 | p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT 57 | p.test_globs = ["test/**/test_*.rb"] 58 | p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean. 59 | 60 | # == Optional 61 | p.changes = p.paragraphs_of("History.txt", 0..1).join("\\n\\n") 62 | #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ] 63 | 64 | #p.spec_extras = {} # A hash of extra values to set in the gemspec. 65 | 66 | end 67 | 68 | CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n") 69 | PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}" 70 | hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc') 71 | hoe.rsync_args = '-av --delete --ignore-errors' 72 | -------------------------------------------------------------------------------- /lib/starling.rb: -------------------------------------------------------------------------------- 1 | require 'memcache' 2 | 3 | class Starling < MemCache 4 | 5 | WAIT_TIME = 0.25 6 | 7 | ## 8 | # fetch an item from a queue. 9 | 10 | def get(*args) 11 | loop do 12 | response = super(*args) 13 | return response unless response.nil? 14 | sleep WAIT_TIME 15 | end 16 | end 17 | 18 | ## 19 | # insert +value+ into +queue+. 20 | # 21 | # +expiry+ is expressed as a UNIX timestamp 22 | # 23 | # If +raw+ is true, +value+ will not be Marshalled. If +raw+ = :yaml, +value+ 24 | # will be serialized with YAML, instead. 25 | 26 | def set(queue, value, expiry = 0, raw = false) 27 | retries = 0 28 | begin 29 | if raw == :yaml 30 | value = YAML.dump(value) 31 | raw = true 32 | end 33 | 34 | super(queue, value, expiry, raw) 35 | rescue MemCache::MemCacheError => e 36 | retries += 1 37 | sleep WAIT_TIME 38 | retry unless retries > 3 39 | raise e 40 | end 41 | end 42 | 43 | ## 44 | # returns the number of items in +queue+. If +queue+ is +:all+, a hash of all 45 | # queue sizes will be returned. 46 | 47 | def sizeof(queue, statistics = nil) 48 | statistics ||= stats 49 | 50 | if queue == :all 51 | queue_sizes = {} 52 | available_queues(statistics).each do |queue| 53 | queue_sizes[queue] = sizeof(queue, statistics) 54 | end 55 | return queue_sizes 56 | end 57 | 58 | statistics.inject(0) { |m,(k,v)| m + v["queue_#{queue}_items"].to_i } 59 | end 60 | 61 | ## 62 | # returns a list of available (currently allocated) queues. 63 | 64 | def available_queues(statistics = nil) 65 | statistics ||= stats 66 | 67 | statistics.map { |k,v| 68 | v.keys 69 | }.flatten.uniq.grep(/^queue_(.*)_items/).map { |v| 70 | v.gsub(/^queue_/, '').gsub(/_items$/, '') 71 | }.reject { |v| 72 | v =~ /_total$/ || v =~ /_expired$/ 73 | } 74 | end 75 | 76 | ## 77 | # iterator to flush +queue+. Each element will be passed to the provided 78 | # +block+ 79 | 80 | def flush(queue) 81 | sizeof(queue).times do 82 | v = get(queue) 83 | yield v if block_given? 84 | end 85 | end 86 | 87 | private 88 | 89 | def get_server_for_key(key) 90 | raise ArgumentError, "illegal character in key #{key.inspect}" if key =~ /\s/ 91 | raise ArgumentError, "key too long #{key.inspect}" if key.length > 250 92 | raise MemCacheError, "No servers available" if @servers.empty? 93 | 94 | bukkits = @buckets.dup 95 | bukkits.nitems.times do |try| 96 | n = rand(bukkits.nitems) 97 | server = bukkits[n] 98 | return server if server.alive? 99 | bukkits.delete_at(n) 100 | end 101 | 102 | raise MemCacheError, "No servers available (all dead)" 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /website/stylesheets/screen.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E1D1F1; 3 | font-family: "Georgia", sans-serif; 4 | font-size: 16px; 5 | line-height: 1.6em; 6 | padding: 1.6em 0 0 0; 7 | color: #333; 8 | } 9 | h1, h2, h3, h4, h5, h6 { 10 | color: #444; 11 | } 12 | h1 { 13 | font-family: sans-serif; 14 | font-weight: normal; 15 | font-size: 4em; 16 | line-height: 0.8em; 17 | letter-spacing: -0.1ex; 18 | margin: 5px; 19 | } 20 | li { 21 | padding: 0; 22 | margin: 0; 23 | list-style-type: square; 24 | } 25 | a { 26 | color: #5E5AFF; 27 | background-color: #DAC; 28 | font-weight: normal; 29 | text-decoration: underline; 30 | } 31 | blockquote { 32 | font-size: 90%; 33 | font-style: italic; 34 | border-left: 1px solid #111; 35 | padding-left: 1em; 36 | } 37 | .caps { 38 | font-size: 80%; 39 | } 40 | 41 | #main { 42 | width: 45em; 43 | padding: 0; 44 | margin: 0 auto; 45 | } 46 | .coda { 47 | text-align: right; 48 | color: #77f; 49 | font-size: smaller; 50 | } 51 | 52 | table { 53 | font-size: 90%; 54 | line-height: 1.4em; 55 | color: #ff8; 56 | background-color: #111; 57 | padding: 2px 10px 2px 10px; 58 | border-style: dashed; 59 | } 60 | 61 | th { 62 | color: #fff; 63 | } 64 | 65 | td { 66 | padding: 2px 10px 2px 10px; 67 | } 68 | 69 | .success { 70 | color: #0CC52B; 71 | } 72 | 73 | .failed { 74 | color: #E90A1B; 75 | } 76 | 77 | .unknown { 78 | color: #995000; 79 | } 80 | pre, code { 81 | font-family: monospace; 82 | font-size: 90%; 83 | line-height: 1.4em; 84 | color: #ff8; 85 | background-color: #111; 86 | padding: 2px 10px 2px 10px; 87 | } 88 | .comment { color: #aaa; font-style: italic; } 89 | .keyword { color: #eff; font-weight: bold; } 90 | .punct { color: #eee; font-weight: bold; } 91 | .symbol { color: #0bb; } 92 | .string { color: #6b4; } 93 | .ident { color: #ff8; } 94 | .constant { color: #66f; } 95 | .regex { color: #ec6; } 96 | .number { color: #F99; } 97 | .expr { color: #227; } 98 | 99 | #version { 100 | float: right; 101 | text-align: right; 102 | font-family: sans-serif; 103 | font-weight: normal; 104 | background-color: #B3ABFF; 105 | color: #141331; 106 | padding: 15px 20px 10px 20px; 107 | margin: 0 auto; 108 | margin-top: 15px; 109 | border: 3px solid #141331; 110 | } 111 | 112 | #version .numbers { 113 | display: block; 114 | font-size: 4em; 115 | line-height: 0.8em; 116 | letter-spacing: -0.1ex; 117 | margin-bottom: 15px; 118 | } 119 | 120 | #version p { 121 | text-decoration: none; 122 | color: #141331; 123 | background-color: #B3ABFF; 124 | margin: 0; 125 | padding: 0; 126 | } 127 | 128 | #version a { 129 | text-decoration: none; 130 | color: #141331; 131 | background-color: #B3ABFF; 132 | } 133 | 134 | .clickable { 135 | cursor: pointer; 136 | cursor: hand; 137 | } 138 | 139 | -------------------------------------------------------------------------------- /lib/starling/server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'logger' 3 | require 'rubygems' 4 | require 'eventmachine' 5 | require 'analyzer_tools/syslog_logger' 6 | 7 | here = File.dirname(__FILE__) 8 | 9 | require File.join(here, 'queue_collection') 10 | require File.join(here, 'handler') 11 | 12 | module StarlingServer 13 | 14 | VERSION = "0.9.6" 15 | 16 | class Base 17 | attr_reader :logger 18 | 19 | DEFAULT_HOST = '127.0.0.1' 20 | DEFAULT_PORT = 22122 21 | DEFAULT_PATH = "/tmp/starling/" 22 | DEFAULT_TIMEOUT = 60 23 | 24 | ## 25 | # Initialize a new Starling server and immediately start processing 26 | # requests. 27 | # 28 | # +opts+ is an optional hash, whose valid options are: 29 | # 30 | # [:host] Host on which to listen (default is 127.0.0.1). 31 | # [:port] Port on which to listen (default is 22122). 32 | # [:path] Path to Starling queue logs. Default is /tmp/starling/ 33 | # [:timeout] Time in seconds to wait before closing connections. 34 | # [:logger] A Logger object, an IO handle, or a path to the log. 35 | # [:loglevel] Logger verbosity. Default is Logger::ERROR. 36 | # 37 | # Other options are ignored. 38 | 39 | def self.start(opts = {}) 40 | server = self.new(opts) 41 | server.run 42 | end 43 | 44 | ## 45 | # Initialize a new Starling server, but do not accept connections or 46 | # process requests. 47 | # 48 | # +opts+ is as for +start+ 49 | 50 | def initialize(opts = {}) 51 | @opts = { 52 | :host => DEFAULT_HOST, 53 | :port => DEFAULT_PORT, 54 | :path => DEFAULT_PATH, 55 | :timeout => DEFAULT_TIMEOUT, 56 | :server => self 57 | }.merge(opts) 58 | 59 | @stats = Hash.new(0) 60 | end 61 | 62 | ## 63 | # Start listening and processing requests. 64 | 65 | def run 66 | @stats[:start_time] = Time.now 67 | 68 | @@logger = case @opts[:logger] 69 | when IO, String; Logger.new(@opts[:logger]) 70 | when Logger; @opts[:logger] 71 | else; Logger.new(STDERR) 72 | end 73 | @@logger = SyslogLogger.new(@opts[:syslog_channel]) if @opts[:syslog_channel] 74 | 75 | @opts[:queue] = QueueCollection.new(@opts[:path]) 76 | @@logger.level = @opts[:log_level] || Logger::ERROR 77 | 78 | @@logger.error "Starling STARTUP on #{@opts[:host]}:#{@opts[:port]}" 79 | 80 | EventMachine.epoll 81 | EventMachine.set_descriptor_table_size(4096) 82 | EventMachine.run do 83 | EventMachine.start_server(@opts[:host], @opts[:port], Handler, @opts) 84 | end 85 | 86 | # code here will get executed on shutdown: 87 | @opts[:queue].close 88 | end 89 | 90 | def self.logger 91 | @@logger 92 | end 93 | 94 | 95 | ## 96 | # Stop accepting new connections and shutdown gracefully. 97 | 98 | def stop 99 | EventMachine.stop_event_loop 100 | end 101 | 102 | def stats(stat = nil) #:nodoc: 103 | case stat 104 | when nil; @stats 105 | when :connections; 1 106 | else; @stats[stat] 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/starling/queue_collection.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'starling/persistent_queue' 3 | 4 | module StarlingServer 5 | class InaccessibleQueuePath < Exception #:nodoc: 6 | end 7 | 8 | ## 9 | # QueueCollection is a proxy to a collection of PersistentQueue instances. 10 | 11 | class QueueCollection 12 | 13 | ## 14 | # Create a new QueueCollection at +path+ 15 | 16 | def initialize(path) 17 | unless File.directory?(path) && File.writable?(path) 18 | raise InaccessibleQueuePath.new(path) 19 | end 20 | 21 | @shutdown_mutex = Mutex.new 22 | 23 | @path = path 24 | @logger = StarlingServer::Base.logger 25 | 26 | @queues = {} 27 | @queue_init_mutexes = {} 28 | 29 | @stats = Hash.new(0) 30 | end 31 | 32 | ## 33 | # Puts +data+ onto the queue named +key+ 34 | 35 | def put(key, data) 36 | queue = queues(key) 37 | return nil unless queue 38 | 39 | @stats[:current_bytes] += data.size 40 | @stats[:total_items] += 1 41 | 42 | queue.push(data) 43 | 44 | return true 45 | end 46 | 47 | ## 48 | # Retrieves data from the queue named +key+ 49 | 50 | def take(key) 51 | queue = queues(key) 52 | if queue.nil? || queue.length == 0 53 | @stats[:get_misses] += 1 54 | return nil 55 | else 56 | @stats[:get_hits] += 1 57 | end 58 | result = queue.pop 59 | @stats[:current_bytes] -= result.size 60 | result 61 | end 62 | 63 | ## 64 | # Returns all active queues. 65 | 66 | def queues(key=nil) 67 | return nil if @shutdown_mutex.locked? 68 | 69 | return @queues if key.nil? 70 | 71 | # First try to return the queue named 'key' if it's available. 72 | return @queues[key] if @queues[key] 73 | 74 | # If the queue wasn't available, create or get the mutex that will 75 | # wrap creation of the Queue. 76 | @queue_init_mutexes[key] ||= Mutex.new 77 | 78 | # Otherwise, check to see if another process is already loading 79 | # the queue named 'key'. 80 | if @queue_init_mutexes[key].locked? 81 | # return an empty/false result if we're waiting for the queue 82 | # to be loaded and we're not the first process to request the queue 83 | return nil 84 | else 85 | begin 86 | @queue_init_mutexes[key].lock 87 | # we've locked the mutex, but only go ahead if the queue hasn't 88 | # been loaded. There's a race condition otherwise, and we could 89 | # end up loading the queue multiple times. 90 | if @queues[key].nil? 91 | @queues[key] = PersistentQueue.new(@path, key) 92 | @stats[:current_bytes] += @queues[key].initial_bytes 93 | end 94 | rescue Object => exc 95 | puts "ZOMG There was an exception reading back the queue. That totally sucks." 96 | puts "The exception was: #{exc}. Backtrace: #{exc.backtrace.join("\n")}" 97 | ensure 98 | @queue_init_mutexes[key].unlock 99 | end 100 | end 101 | 102 | return @queues[key] 103 | end 104 | 105 | ## 106 | # Returns statistic +stat_name+ for the QueueCollection. 107 | # 108 | # Valid statistics are: 109 | # 110 | # [:get_misses] Total number of get requests with empty responses 111 | # [:get_hits] Total number of get requests that returned data 112 | # [:current_bytes] Current size in bytes of items in the queues 113 | # [:current_size] Current number of items across all queues 114 | # [:total_items] Total number of items stored in queues. 115 | 116 | def stats(stat_name) 117 | case stat_name 118 | when nil; @stats 119 | when :current_size; current_size 120 | else; @stats[stat_name] 121 | end 122 | end 123 | 124 | ## 125 | # Safely close all queues. 126 | 127 | def close 128 | @shutdown_mutex.lock 129 | @queues.each_pair do |name,queue| 130 | queue.close 131 | @queues.delete(name) 132 | end 133 | end 134 | 135 | private 136 | 137 | def current_size #:nodoc: 138 | @queues.inject(0) { |m, (k,v)| m + v.length } 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Starling 9 | 10 | 11 | 14 | 29 | 30 | 31 |
32 | 33 |

Starling

34 |
35 |

Get Version

36 | 1.0.0 37 |
38 |

What

39 | 40 | 41 |

Starling is a light-weight server for distributed message passing.

42 | 43 | 44 |

Installing

45 | 46 | 47 |

sudo gem install starling

48 | 49 | 50 |

The basics

51 | 52 | 53 |
    54 |
  1. Start the Starling server.
  2. 55 |
  3. Put messages onto a queue from a client whose work you’d like to offload to a deferred process.
  4. 56 |
  5. Run a daemon process to take messages off the queue, and process as time allows.
  6. 57 |
58 | 59 | 60 |

Demonstration of usage

61 | 62 | 63 |
    64 |
  1. Start the Starling server:
  2. 65 |
66 | 67 | 68 |
 69 | 
 70 |     starling -h 192.168.1.1 -d
 71 | 
 72 | 
73 | 74 |
    75 |
  1. Queue messages for later processing:
  2. 76 |
77 | 78 | 79 |
 80 | 
 81 |     require 'memcache'
 82 |     starling = MemCache.new('192.168.1.1:22122')
 83 |     starling.set('my_queue', 12345)
 84 | 
 85 | 
86 | 87 |
    88 |
  1. Fetch messages from the queue and do something with them:
  2. 89 |
90 | 91 | 92 |
 93 | 
 94 |     require 'memcache'
 95 |     starling = MemCache.new('192.168.1.1:22122')
 96 |     loop { puts starling.get('my_queue') }
 97 | 
 98 | 
99 | 100 |

Forum

101 | 102 | 103 |

http://groups.google.com/group/starling

104 | 105 | 106 |

TODO – create Google Group – starling

107 | 108 | 109 |

How to submit patches

110 | 111 | 112 |

Read the 8 steps for fixing other people’s code and for section 8b: Submit patch to Google Groups, use the Google Group above.

113 | 114 | 115 |

The trunk repository is svn://rubyforge.org/var/svn/starling/trunk for anonymous access.

116 | 117 | 118 |

License

119 | 120 | 121 |

This code is free to use under the terms of the MIT license.

122 | 123 | 124 |

Contact

125 | 126 | 127 |

Comments are welcome. Send an email to FIXME full name email via the forum

128 |

129 | FIXME full name, 4th November 2007
130 | Theme extended from Paul Battley 131 |

132 |
133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /lib/starling/persistent_queue.rb: -------------------------------------------------------------------------------- 1 | module StarlingServer 2 | 3 | ## 4 | # PersistentQueue is a subclass of Ruby's thread-safe Queue class. It adds a 5 | # transactional log to the in-memory Queue, which enables quickly rebuilding 6 | # the Queue in the event of a sever outage. 7 | 8 | class PersistentQueue < Queue 9 | 10 | ## 11 | # When a log reaches the SOFT_LOG_MAX_SIZE, the Queue will wait until 12 | # it is empty, and will then rotate the log file. 13 | 14 | SOFT_LOG_MAX_SIZE = 16 * (1024**2) # 16 MB 15 | 16 | TRX_CMD_PUSH = "\000".freeze 17 | TRX_CMD_POP = "\001".freeze 18 | 19 | TRX_PUSH = "\000%s%s".freeze 20 | TRX_POP = "\001".freeze 21 | 22 | attr_reader :initial_bytes 23 | attr_reader :total_items 24 | attr_reader :logsize 25 | attr_reader :current_age 26 | 27 | ## 28 | # Create a new PersistentQueue at +persistence_path+/+queue_name+. 29 | # If a queue log exists at that path, the Queue will be loaded from 30 | # disk before being available for use. 31 | 32 | def initialize(persistence_path, queue_name, debug = false) 33 | @persistence_path = persistence_path 34 | @queue_name = queue_name 35 | @total_items = 0 36 | super() 37 | @initial_bytes = replay_transaction_log(debug) 38 | @current_age = 0 39 | end 40 | 41 | ## 42 | # Pushes +value+ to the queue. By default, +push+ will write to the 43 | # transactional log. Set +log_trx=false+ to override this behaviour. 44 | 45 | def push(value, log_trx = true) 46 | if log_trx 47 | raise NoTransactionLog unless @trx 48 | size = [value.size].pack("I") 49 | transaction sprintf(TRX_PUSH, size, value) 50 | end 51 | 52 | @total_items += 1 53 | super([now_usec, value]) 54 | end 55 | 56 | ## 57 | # Retrieves data from the queue. 58 | 59 | def pop(log_trx = true) 60 | raise NoTransactionLog if log_trx && !@trx 61 | 62 | begin 63 | rv = super(!log_trx) 64 | rescue ThreadError 65 | puts "WARNING: The queue was empty when trying to pop(). Technically this shouldn't ever happen. Probably a bug in the transactional underpinnings. Or maybe shutdown didn't happen cleanly at some point. Ignoring." 66 | rv = [now_usec, ''] 67 | end 68 | transaction "\001" if log_trx 69 | @current_age = (now_usec - rv[0]) / 1000 70 | rv[1] 71 | end 72 | 73 | ## 74 | # Safely closes the transactional queue. 75 | 76 | def close 77 | # Ok, yeah, this is lame, and is *technically* a race condition. HOWEVER, 78 | # the QueueCollection *should* have stopped processing requests, and I don't 79 | # want to add yet another Mutex around all the push and pop methods. So we 80 | # do the next simplest thing, and minimize the time we'll stick around before 81 | # @trx is nil. 82 | @not_trx = @trx 83 | @trx = nil 84 | @not_trx.close 85 | end 86 | 87 | private 88 | 89 | def log_path #:nodoc: 90 | File.join(@persistence_path, @queue_name) 91 | end 92 | 93 | def reopen_log #:nodoc: 94 | @trx = File.new(log_path, File::CREAT|File::RDWR) 95 | @logsize = File.size(log_path) 96 | end 97 | 98 | def rotate_log #:nodoc: 99 | @trx.close 100 | backup_logfile = "#{log_path}.#{Time.now.to_i}" 101 | File.rename(log_path, backup_logfile) 102 | reopen_log 103 | File.unlink(backup_logfile) 104 | end 105 | 106 | def replay_transaction_log(debug) #:nodoc: 107 | reopen_log 108 | bytes_read = 0 109 | 110 | print "Reading back transaction log for #{@queue_name} " if debug 111 | 112 | while !@trx.eof? 113 | cmd = @trx.read(1) 114 | case cmd 115 | when TRX_CMD_PUSH 116 | print ">" if debug 117 | raw_size = @trx.read(4) 118 | next unless raw_size 119 | size = raw_size.unpack("I").first 120 | data = @trx.read(size) 121 | next unless data 122 | push(data, false) 123 | bytes_read += data.size 124 | when TRX_CMD_POP 125 | print "<" if debug 126 | bytes_read -= pop(false).size 127 | else 128 | puts "Error reading transaction log: " + 129 | "I don't understand '#{cmd}' (skipping)." if debug 130 | end 131 | end 132 | 133 | print " done.\n" if debug 134 | 135 | return bytes_read 136 | end 137 | 138 | def transaction(data) #:nodoc: 139 | raise "no transaction log handle. that totally sucks." unless @trx 140 | 141 | @trx.write_nonblock data 142 | @logsize += data.size 143 | rotate_log if @logsize > SOFT_LOG_MAX_SIZE && self.length == 0 144 | end 145 | 146 | def now_usec 147 | now = Time.now 148 | now.to_i * 1000000 + now.usec 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /test/test_starling.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper.rb') 2 | 3 | require 'starling/server' 4 | 5 | require 'fileutils' 6 | require 'rubygems' 7 | require 'memcache' 8 | 9 | class StarlingServer::PersistentQueue 10 | remove_const :SOFT_LOG_MAX_SIZE 11 | SOFT_LOG_MAX_SIZE = 16 * 1024 # 16 KB 12 | end 13 | 14 | def safely_fork(&block) 15 | # anti-race juice: 16 | blocking = true 17 | Signal.trap("USR1") { blocking = false } 18 | 19 | pid = Process.fork(&block) 20 | 21 | while blocking 22 | sleep 0.1 23 | end 24 | 25 | pid 26 | end 27 | 28 | 29 | class TestStarling < Test::Unit::TestCase 30 | 31 | def setup 32 | begin 33 | Dir::mkdir(tmp_path) 34 | rescue Errno::EEXIST 35 | end 36 | 37 | @server_pid = safely_fork do 38 | server = StarlingServer::Base.new(:host => '127.0.0.1', 39 | :port => 22133, 40 | :path => tmp_path, 41 | :logger => Logger.new(STDERR), 42 | :log_level => Logger::FATAL) 43 | Signal.trap("INT") { server.stop } 44 | Process.kill("USR1", Process.ppid) 45 | server.run 46 | end 47 | 48 | @client = MemCache.new('127.0.0.1:22133') 49 | end 50 | 51 | def teardown 52 | Process.kill("INT", @server_pid) 53 | Process.wait(@server_pid) 54 | @client.reset 55 | FileUtils.rm(Dir.glob(File.join(tmp_path, '*'))) 56 | end 57 | 58 | def test_temporary_path_exists_and_is_writable 59 | assert File.exist?(tmp_path) 60 | assert File.directory?(tmp_path) 61 | assert File.writable?(tmp_path) 62 | end 63 | 64 | def test_set_and_get_one_entry 65 | v = rand((2**32)-1) 66 | assert_equal nil, @client.get('test_set_and_get_one_entry') 67 | @client.set('test_set_and_get_one_entry', v) 68 | assert_equal v, @client.get('test_set_and_get_one_entry') 69 | end 70 | 71 | def test_set_with_expiry 72 | v = rand((2**32)-1) 73 | assert_equal nil, @client.get('test_set_with_expiry') 74 | now = Time.now.to_i 75 | @client.set('test_set_with_expiry', v + 2, now) 76 | @client.set('test_set_with_expiry', v) 77 | sleep(1.0) 78 | assert_equal v, @client.get('test_set_with_expiry') 79 | end 80 | 81 | def test_age 82 | now = Time.now.to_i 83 | @client.set('test_age', 'nibbler') 84 | sleep(1.0) 85 | assert_equal 'nibbler', @client.get('test_age') 86 | 87 | stats = @client.stats['127.0.0.1:22133'] 88 | assert stats.has_key?('queue_test_age_age') 89 | assert stats['queue_test_age_age'] >= 1000 90 | end 91 | 92 | def test_log_rotation 93 | log_rotation_path = File.join(tmp_path, 'test_log_rotation') 94 | 95 | Dir.glob("#{log_rotation_path}*").each do |file| 96 | File.unlink(file) rescue nil 97 | end 98 | assert_equal nil, @client.get('test_log_rotation') 99 | 100 | v = 'x' * 8192 101 | 102 | @client.set('test_log_rotation', v) 103 | assert_equal 8207, File.size(log_rotation_path) 104 | @client.get('test_log_rotation') 105 | 106 | assert_equal nil, @client.get('test_log_rotation') 107 | 108 | @client.set('test_log_rotation', v) 109 | assert_equal v, @client.get('test_log_rotation') 110 | 111 | assert_equal 1, File.size(log_rotation_path) 112 | # rotated log should be erased after a successful roll. 113 | assert_equal 1, Dir.glob("#{log_rotation_path}*").size 114 | end 115 | 116 | def test_stats 117 | stats = @client.stats 118 | assert_kind_of Hash, stats 119 | assert stats.has_key?('127.0.0.1:22133') 120 | 121 | server_stats = stats['127.0.0.1:22133'] 122 | 123 | basic_stats = %w( bytes pid time limit_maxbytes cmd_get version 124 | bytes_written cmd_set get_misses total_connections 125 | curr_connections curr_items uptime get_hits total_items 126 | rusage_system rusage_user bytes_read ) 127 | 128 | basic_stats.each do |stat| 129 | assert server_stats.has_key?(stat) 130 | end 131 | end 132 | 133 | def test_unknown_command_returns_valid_result 134 | response = @client.add('blah', 1) 135 | assert_match 'CLIENT_ERROR', response 136 | end 137 | 138 | def test_that_disconnecting_and_reconnecting_works 139 | v = rand(2**32-1) 140 | @client.set('test_that_disconnecting_and_reconnecting_works', v) 141 | @client.reset 142 | assert_equal v, @client.get('test_that_disconnecting_and_reconnecting_works') 143 | end 144 | 145 | def test_epoll 146 | # this may take a few seconds. 147 | # the point is to make sure that we're using epoll on Linux, so we can 148 | # handle more than 1024 connections. 149 | 150 | unless IO::popen("uname").read.chomp == "Linux" 151 | puts "(Skipping epoll test: not on Linux)" 152 | return 153 | end 154 | fd_limit = IO::popen("bash -c 'ulimit -n'").read.chomp.to_i 155 | unless fd_limit > 1024 156 | puts "(Skipping epoll test: 'ulimit -n' = #{fd_limit}, need > 1024)" 157 | return 158 | end 159 | 160 | v = rand(2**32 - 1) 161 | @client.set('test_epoll', v) 162 | 163 | # we can't open 1024 connections to memcache from within this process, 164 | # because we will hit ruby's 1024 fd limit ourselves! 165 | pid1 = safely_fork do 166 | unused_sockets = [] 167 | 600.times do 168 | unused_sockets << TCPSocket.new("127.0.0.1", 22133) 169 | end 170 | Process.kill("USR1", Process.ppid) 171 | sleep 90 172 | end 173 | pid2 = safely_fork do 174 | unused_sockets = [] 175 | 600.times do 176 | unused_sockets << TCPSocket.new("127.0.0.1", 22133) 177 | end 178 | Process.kill("USR1", Process.ppid) 179 | sleep 90 180 | end 181 | 182 | begin 183 | client = MemCache.new('127.0.0.1:22133') 184 | assert_equal v, client.get('test_epoll') 185 | ensure 186 | Process.kill("TERM", pid1) 187 | Process.kill("TERM", pid2) 188 | end 189 | end 190 | 191 | 192 | private 193 | 194 | def tmp_path 195 | File.join(File.dirname(__FILE__), "..", "tmp") 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /lib/starling/handler.rb: -------------------------------------------------------------------------------- 1 | module StarlingServer 2 | 3 | ## 4 | # This is an internal class that's used by Starling::Server to handle the 5 | # MemCache protocol and act as an interface between the Server and the 6 | # QueueCollection. 7 | 8 | class Handler < EventMachine::Connection 9 | 10 | DATA_PACK_FMT = "Ia*".freeze 11 | 12 | # ERROR responses 13 | ERR_UNKNOWN_COMMAND = "CLIENT_ERROR bad command line format\r\n".freeze 14 | 15 | # GET Responses 16 | GET_COMMAND = /\Aget (.{1,250})\s*\r\n/m 17 | GET_RESPONSE = "VALUE %s %s %s\r\n%s\r\nEND\r\n".freeze 18 | GET_RESPONSE_EMPTY = "END\r\n".freeze 19 | 20 | # SET Responses 21 | SET_COMMAND = /\Aset (.{1,250}) ([0-9]+) ([0-9]+) ([0-9]+)\r\n/m 22 | SET_RESPONSE_SUCCESS = "STORED\r\n".freeze 23 | SET_RESPONSE_FAILURE = "NOT STORED\r\n".freeze 24 | SET_CLIENT_DATA_ERROR = "CLIENT_ERROR bad data chunk\r\nERROR\r\n".freeze 25 | 26 | # STAT Response 27 | STATS_COMMAND = /\Astats\r\n/m 28 | STATS_RESPONSE = "STAT pid %d 29 | STAT uptime %d 30 | STAT time %d 31 | STAT version %s 32 | STAT rusage_user %0.6f 33 | STAT rusage_system %0.6f 34 | STAT curr_items %d 35 | STAT total_items %d 36 | STAT bytes %d 37 | STAT curr_connections %d 38 | STAT total_connections %d 39 | STAT cmd_get %d 40 | STAT cmd_set %d 41 | STAT get_hits %d 42 | STAT get_misses %d 43 | STAT bytes_read %d 44 | STAT bytes_written %d 45 | STAT limit_maxbytes %d 46 | %sEND\r\n".freeze 47 | QUEUE_STATS_RESPONSE = "STAT queue_%s_items %d 48 | STAT queue_%s_total_items %d 49 | STAT queue_%s_logsize %d 50 | STAT queue_%s_expired_items %d 51 | STAT queue_%s_age %d\n".freeze 52 | 53 | SHUTDOWN_COMMAND = /\Ashutdown\r\n/m 54 | 55 | 56 | @@next_session_id = 1 57 | 58 | ## 59 | # Creates a new handler for the MemCache protocol that communicates with a 60 | # given client. 61 | 62 | def initialize(options = {}) 63 | @opts = options 64 | end 65 | 66 | ## 67 | # Process incoming commands from the attached client. 68 | 69 | def post_init 70 | @stash = [] 71 | @data = "" 72 | @data_buf = "" 73 | @server = @opts[:server] 74 | @logger = StarlingServer::Base.logger 75 | @expiry_stats = Hash.new(0) 76 | @expected_length = nil 77 | @server.stats[:total_connections] += 1 78 | set_comm_inactivity_timeout @opts[:timeout] 79 | @queue_collection = @opts[:queue] 80 | 81 | @session_id = @@next_session_id 82 | @@next_session_id += 1 83 | 84 | peer = Socket.unpack_sockaddr_in(get_peername) 85 | #@logger.debug "(#{@session_id}) New session from #{peer[1]}:#{peer[0]}" 86 | end 87 | 88 | def receive_data(incoming) 89 | @server.stats[:bytes_read] += incoming.size 90 | @data << incoming 91 | 92 | while data = @data.slice!(/.*?\r\n/m) 93 | response = process(data) 94 | end 95 | 96 | send_data response if response 97 | end 98 | 99 | def process(data) 100 | data = @data_buf + data if @data_buf.size > 0 101 | # our only non-normal state is consuming an object's data 102 | # when @expected_length is present 103 | if @expected_length && data.size == @expected_length 104 | response = set_data(data) 105 | @data_buf = "" 106 | return response 107 | elsif @expected_length 108 | @data_buf = data 109 | return 110 | end 111 | 112 | case data 113 | when SET_COMMAND 114 | @server.stats[:set_requests] += 1 115 | set($1, $2, $3, $4.to_i) 116 | when GET_COMMAND 117 | @server.stats[:get_requests] += 1 118 | get($1) 119 | when STATS_COMMAND 120 | stats 121 | when SHUTDOWN_COMMAND 122 | # no point in responding, they'll never get it. 123 | Runner::shutdown 124 | else 125 | logger.warn "Unknown command: #{data}." 126 | respond ERR_UNKNOWN_COMMAND 127 | end 128 | rescue => e 129 | logger.error "Error handling request: #{e}." 130 | logger.debug e.backtrace.join("\n") 131 | respond GET_RESPONSE_EMPTY 132 | end 133 | 134 | def unbind 135 | #@logger.debug "(#{@session_id}) connection ends" 136 | end 137 | 138 | private 139 | def respond(str, *args) 140 | response = sprintf(str, *args) 141 | @server.stats[:bytes_written] += response.length 142 | response 143 | end 144 | 145 | def set(key, flags, expiry, len) 146 | @expected_length = len + 2 147 | @stash = [ key, flags, expiry ] 148 | nil 149 | end 150 | 151 | def set_data(incoming) 152 | key, flags, expiry = @stash 153 | data = incoming.slice(0...@expected_length-2) 154 | @stash = [] 155 | @expected_length = nil 156 | 157 | internal_data = [expiry.to_i, data].pack(DATA_PACK_FMT) 158 | if @queue_collection.put(key, internal_data) 159 | respond SET_RESPONSE_SUCCESS 160 | else 161 | respond SET_RESPONSE_FAILURE 162 | end 163 | end 164 | 165 | def get(key) 166 | now = Time.now.to_i 167 | 168 | while response = @queue_collection.take(key) 169 | expiry, data = response.unpack(DATA_PACK_FMT) 170 | 171 | break if expiry == 0 || expiry >= now 172 | 173 | @expiry_stats[key] += 1 174 | expiry, data = nil 175 | end 176 | 177 | if data 178 | respond GET_RESPONSE, key, 0, data.size, data 179 | else 180 | respond GET_RESPONSE_EMPTY 181 | end 182 | end 183 | 184 | def stats 185 | respond STATS_RESPONSE, 186 | Process.pid, # pid 187 | Time.now - @server.stats(:start_time), # uptime 188 | Time.now.to_i, # time 189 | StarlingServer::VERSION, # version 190 | Process.times.utime, # rusage_user 191 | Process.times.stime, # rusage_system 192 | @queue_collection.stats(:current_size), # curr_items 193 | @queue_collection.stats(:total_items), # total_items 194 | @queue_collection.stats(:current_bytes), # bytes 195 | @server.stats(:connections), # curr_connections 196 | @server.stats(:total_connections), # total_connections 197 | @server.stats(:get_requests), # get count 198 | @server.stats(:set_requests), # set count 199 | @queue_collection.stats(:get_hits), 200 | @queue_collection.stats(:get_misses), 201 | @server.stats(:bytes_read), # total bytes read 202 | @server.stats(:bytes_written), # total bytes written 203 | 0, # limit_maxbytes 204 | queue_stats 205 | end 206 | 207 | def queue_stats 208 | @queue_collection.queues.inject("") do |m,(k,v)| 209 | m + sprintf(QUEUE_STATS_RESPONSE, 210 | k, v.length, 211 | k, v.total_items, 212 | k, v.logsize, 213 | k, @expiry_stats[k], 214 | k, v.current_age) 215 | end 216 | end 217 | 218 | def logger 219 | @logger 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /lib/starling/runner.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'server') 2 | require 'optparse' 3 | require 'yaml' 4 | 5 | module StarlingServer 6 | class Runner 7 | 8 | attr_accessor :options 9 | private :options, :options= 10 | 11 | def self.run 12 | new 13 | end 14 | 15 | def self.shutdown 16 | @@instance.shutdown 17 | end 18 | 19 | def initialize 20 | @@instance = self 21 | parse_options 22 | 23 | @process = ProcessHelper.new(options[:logger], options[:pid_file], options[:user], options[:group]) 24 | 25 | pid = @process.running? 26 | if pid 27 | STDERR.puts "There is already a Starling process running (pid #{pid}), exiting." 28 | exit(1) 29 | elsif pid.nil? 30 | STDERR.puts "Cleaning up stale pidfile at #{options[:pid_file]}." 31 | end 32 | 33 | start 34 | end 35 | 36 | def load_config_file(filename) 37 | YAML.load(File.open(filename))['starling'].each do |key, value| 38 | # alias some keys 39 | case key 40 | when "queue_path" then key = "path" 41 | when "log_file" then key = "logger" 42 | end 43 | options[key.to_sym] = value 44 | 45 | if options[:log_level].instance_of?(String) 46 | options[:log_level] = Logger.const_get(options[:log_level]) 47 | end 48 | end 49 | end 50 | 51 | def parse_options 52 | self.options = { :host => '127.0.0.1', 53 | :port => 22122, 54 | :path => File.join(%w( / var spool starling )), 55 | :log_level => Logger::ERROR, 56 | :daemonize => false, 57 | :timeout => 0, 58 | :pid_file => File.join(%w( / var run starling.pid )) } 59 | 60 | OptionParser.new do |opts| 61 | opts.summary_width = 25 62 | 63 | opts.banner = "Starling (#{StarlingServer::VERSION})\n\n", 64 | "usage: starling [options...]\n", 65 | " starling --help\n", 66 | " starling --version\n" 67 | 68 | opts.separator "" 69 | opts.separator "Configuration:" 70 | 71 | opts.on("-f", "--config FILENAME", 72 | "Config file (yaml) to load") do |filename| 73 | load_config_file(filename) 74 | end 75 | 76 | opts.on("-q", "--queue_path PATH", 77 | :REQUIRED, 78 | "Path to store Starling queue logs", "(default: #{options[:path]})") do |queue_path| 79 | options[:path] = queue_path 80 | end 81 | 82 | opts.separator ""; opts.separator "Network:" 83 | 84 | opts.on("-hHOST", "--host HOST", "Interface on which to listen (default: #{options[:host]})") do |host| 85 | options[:host] = host 86 | end 87 | 88 | opts.on("-pHOST", "--port PORT", Integer, "TCP port on which to listen (default: #{options[:port]})") do |port| 89 | options[:port] = port 90 | end 91 | 92 | opts.separator ""; opts.separator "Process:" 93 | 94 | opts.on("-d", "Run as a daemon.") do 95 | options[:daemonize] = true 96 | end 97 | 98 | opts.on("-PFILE", "--pid FILENAME", "save PID in FILENAME when using -d option.", "(default: #{options[:pid_file]})") do |pid_file| 99 | options[:pid_file] = pid_file 100 | end 101 | 102 | opts.on("-u", "--user USER", Integer, "User to run as") do |user| 103 | options[:user] = user 104 | end 105 | 106 | opts.on("-gGROUP", "--group GROUP", "Group to run as") do |group| 107 | options[:group] = group 108 | end 109 | 110 | opts.separator ""; opts.separator "Logging:" 111 | 112 | opts.on("-L", "--log [FILE]", "Path to print debugging information.") do |log_path| 113 | options[:logger] = log_path 114 | end 115 | 116 | opts.on("-l", "--syslog CHANNEL", "Write logs to the syslog instead of a log file.") do |channel| 117 | options[:syslog_channel] = channel 118 | end 119 | 120 | opts.on("-v", "Increase logging verbosity (may be used multiple times).") do 121 | options[:log_level] -= 1 122 | end 123 | 124 | opts.on("-t", "--timeout [SECONDS]", Integer, 125 | "Time in seconds before disconnecting inactive clients (0 to disable).", 126 | "(default: #{options[:timeout]})") do |timeout| 127 | options[:timeout] = timeout 128 | end 129 | 130 | opts.separator ""; opts.separator "Miscellaneous:" 131 | 132 | opts.on_tail("-?", "--help", "Display this usage information.") do 133 | puts "#{opts}\n" 134 | exit 135 | end 136 | 137 | opts.on_tail("-V", "--version", "Print version number and exit.") do 138 | puts "Starling #{StarlingServer::VERSION}\n\n" 139 | exit 140 | end 141 | end.parse! 142 | end 143 | 144 | def start 145 | drop_privileges 146 | 147 | @process.daemonize if options[:daemonize] 148 | 149 | setup_signal_traps 150 | @process.write_pid_file 151 | 152 | STDOUT.puts "Starting at #{options[:host]}:#{options[:port]}." 153 | @server = StarlingServer::Base.new(options) 154 | @server.run 155 | 156 | @process.remove_pid_file 157 | end 158 | 159 | def drop_privileges 160 | Process.euid = options[:user] if options[:user] 161 | Process.egid = options[:group] if options[:group] 162 | end 163 | 164 | def shutdown 165 | begin 166 | STDOUT.puts "Shutting down." 167 | StarlingServer::Base.logger.info "Shutting down." 168 | @server.stop 169 | rescue Object => e 170 | STDERR.puts "There was an error shutting down: #{e}" 171 | exit(70) 172 | end 173 | end 174 | 175 | def setup_signal_traps 176 | Signal.trap("INT") { shutdown } 177 | Signal.trap("TERM") { shutdown } 178 | end 179 | end 180 | 181 | class ProcessHelper 182 | 183 | def initialize(log_file = nil, pid_file = nil, user = nil, group = nil) 184 | @log_file = log_file 185 | @pid_file = pid_file 186 | @user = user 187 | @group = group 188 | end 189 | 190 | def safefork 191 | begin 192 | if pid = fork 193 | return pid 194 | end 195 | rescue Errno::EWOULDBLOCK 196 | sleep 5 197 | retry 198 | end 199 | end 200 | 201 | def daemonize 202 | sess_id = detach_from_terminal 203 | exit if pid = safefork 204 | 205 | Dir.chdir("/") 206 | File.umask 0000 207 | 208 | close_io_handles 209 | redirect_io 210 | 211 | return sess_id 212 | end 213 | 214 | def detach_from_terminal 215 | srand 216 | safefork and exit 217 | 218 | unless sess_id = Process.setsid 219 | raise "Couldn't detache from controlling terminal." 220 | end 221 | 222 | trap 'SIGHUP', 'IGNORE' 223 | 224 | sess_id 225 | end 226 | 227 | def close_io_handles 228 | ObjectSpace.each_object(IO) do |io| 229 | unless [STDIN, STDOUT, STDERR].include?(io) 230 | begin 231 | io.close unless io.closed? 232 | rescue Exception 233 | end 234 | end 235 | end 236 | end 237 | 238 | def redirect_io 239 | begin; STDIN.reopen('/dev/null'); rescue Exception; end 240 | 241 | if @log_file 242 | begin 243 | STDOUT.reopen(@log_file, "a") 244 | STDOUT.sync = true 245 | rescue Exception 246 | begin; STDOUT.reopen('/dev/null'); rescue Exception; end 247 | end 248 | else 249 | begin; STDOUT.reopen('/dev/null'); rescue Exception; end 250 | end 251 | 252 | begin; STDERR.reopen(STDOUT); rescue Exception; end 253 | STDERR.sync = true 254 | end 255 | 256 | def rescue_exception 257 | begin 258 | yield 259 | rescue Exception 260 | end 261 | end 262 | 263 | def write_pid_file 264 | return unless @pid_file 265 | File.open(@pid_file, "w") { |f| f.write(Process.pid) } 266 | File.chmod(0644, @pid_file) 267 | end 268 | 269 | def remove_pid_file 270 | return unless @pid_file 271 | File.unlink(@pid_file) if File.exists?(@pid_file) 272 | end 273 | 274 | def running? 275 | return false unless @pid_file 276 | 277 | pid = File.read(@pid_file).chomp.to_i rescue nil 278 | pid = nil if pid == 0 279 | return false unless pid 280 | 281 | begin 282 | Process.kill(0, pid) 283 | return pid 284 | rescue Errno::ESRCH 285 | return nil 286 | rescue Errno::EPERM 287 | return pid 288 | end 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /website/javascripts/rounded_corners_lite.inc.js: -------------------------------------------------------------------------------- 1 | 2 | /**************************************************************** 3 | * * 4 | * curvyCorners * 5 | * ------------ * 6 | * * 7 | * This script generates rounded corners for your divs. * 8 | * * 9 | * Version 1.2.9 * 10 | * Copyright (c) 2006 Cameron Cooke * 11 | * By: Cameron Cooke and Tim Hutchison. * 12 | * * 13 | * * 14 | * Website: http://www.curvycorners.net * 15 | * Email: info@totalinfinity.com * 16 | * Forum: http://www.curvycorners.net/forum/ * 17 | * * 18 | * * 19 | * This library is free software; you can redistribute * 20 | * it and/or modify it under the terms of the GNU * 21 | * Lesser General Public License as published by the * 22 | * Free Software Foundation; either version 2.1 of the * 23 | * License, or (at your option) any later version. * 24 | * * 25 | * This library is distributed in the hope that it will * 26 | * be useful, but WITHOUT ANY WARRANTY; without even the * 27 | * implied warranty of MERCHANTABILITY or FITNESS FOR A * 28 | * PARTICULAR PURPOSE. See the GNU Lesser General Public * 29 | * License for more details. * 30 | * * 31 | * You should have received a copy of the GNU Lesser * 32 | * General Public License along with this library; * 33 | * Inc., 59 Temple Place, Suite 330, Boston, * 34 | * MA 02111-1307 USA * 35 | * * 36 | ****************************************************************/ 37 | 38 | var isIE = navigator.userAgent.toLowerCase().indexOf("msie") > -1; var isMoz = document.implementation && document.implementation.createDocument; var isSafari = ((navigator.userAgent.toLowerCase().indexOf('safari')!=-1)&&(navigator.userAgent.toLowerCase().indexOf('mac')!=-1))?true:false; function curvyCorners() 39 | { if(typeof(arguments[0]) != "object") throw newCurvyError("First parameter of curvyCorners() must be an object."); if(typeof(arguments[1]) != "object" && typeof(arguments[1]) != "string") throw newCurvyError("Second parameter of curvyCorners() must be an object or a class name."); if(typeof(arguments[1]) == "string") 40 | { var startIndex = 0; var boxCol = getElementsByClass(arguments[1]);} 41 | else 42 | { var startIndex = 1; var boxCol = arguments;} 43 | var curvyCornersCol = new Array(); if(arguments[0].validTags) 44 | var validElements = arguments[0].validTags; else 45 | var validElements = ["div"]; for(var i = startIndex, j = boxCol.length; i < j; i++) 46 | { var currentTag = boxCol[i].tagName.toLowerCase(); if(inArray(validElements, currentTag) !== false) 47 | { curvyCornersCol[curvyCornersCol.length] = new curvyObject(arguments[0], boxCol[i]);} 48 | } 49 | this.objects = curvyCornersCol; this.applyCornersToAll = function() 50 | { for(var x = 0, k = this.objects.length; x < k; x++) 51 | { this.objects[x].applyCorners();} 52 | } 53 | } 54 | function curvyObject() 55 | { this.box = arguments[1]; this.settings = arguments[0]; this.topContainer = null; this.bottomContainer = null; this.masterCorners = new Array(); this.contentDIV = null; var boxHeight = get_style(this.box, "height", "height"); var boxWidth = get_style(this.box, "width", "width"); var borderWidth = get_style(this.box, "borderTopWidth", "border-top-width"); var borderColour = get_style(this.box, "borderTopColor", "border-top-color"); var boxColour = get_style(this.box, "backgroundColor", "background-color"); var backgroundImage = get_style(this.box, "backgroundImage", "background-image"); var boxPosition = get_style(this.box, "position", "position"); var boxPadding = get_style(this.box, "paddingTop", "padding-top"); this.boxHeight = parseInt(((boxHeight != "" && boxHeight != "auto" && boxHeight.indexOf("%") == -1)? boxHeight.substring(0, boxHeight.indexOf("px")) : this.box.scrollHeight)); this.boxWidth = parseInt(((boxWidth != "" && boxWidth != "auto" && boxWidth.indexOf("%") == -1)? boxWidth.substring(0, boxWidth.indexOf("px")) : this.box.scrollWidth)); this.borderWidth = parseInt(((borderWidth != "" && borderWidth.indexOf("px") !== -1)? borderWidth.slice(0, borderWidth.indexOf("px")) : 0)); this.boxColour = format_colour(boxColour); this.boxPadding = parseInt(((boxPadding != "" && boxPadding.indexOf("px") !== -1)? boxPadding.slice(0, boxPadding.indexOf("px")) : 0)); this.borderColour = format_colour(borderColour); this.borderString = this.borderWidth + "px" + " solid " + this.borderColour; this.backgroundImage = ((backgroundImage != "none")? backgroundImage : ""); this.boxContent = this.box.innerHTML; if(boxPosition != "absolute") this.box.style.position = "relative"; this.box.style.padding = "0px"; if(isIE && boxWidth == "auto" && boxHeight == "auto") this.box.style.width = "100%"; if(this.settings.autoPad == true && this.boxPadding > 0) 56 | this.box.innerHTML = ""; this.applyCorners = function() 57 | { for(var t = 0; t < 2; t++) 58 | { switch(t) 59 | { case 0: 60 | if(this.settings.tl || this.settings.tr) 61 | { var newMainContainer = document.createElement("DIV"); newMainContainer.style.width = "100%"; newMainContainer.style.fontSize = "1px"; newMainContainer.style.overflow = "hidden"; newMainContainer.style.position = "absolute"; newMainContainer.style.paddingLeft = this.borderWidth + "px"; newMainContainer.style.paddingRight = this.borderWidth + "px"; var topMaxRadius = Math.max(this.settings.tl ? this.settings.tl.radius : 0, this.settings.tr ? this.settings.tr.radius : 0); newMainContainer.style.height = topMaxRadius + "px"; newMainContainer.style.top = 0 - topMaxRadius + "px"; newMainContainer.style.left = 0 - this.borderWidth + "px"; this.topContainer = this.box.appendChild(newMainContainer);} 62 | break; case 1: 63 | if(this.settings.bl || this.settings.br) 64 | { var newMainContainer = document.createElement("DIV"); newMainContainer.style.width = "100%"; newMainContainer.style.fontSize = "1px"; newMainContainer.style.overflow = "hidden"; newMainContainer.style.position = "absolute"; newMainContainer.style.paddingLeft = this.borderWidth + "px"; newMainContainer.style.paddingRight = this.borderWidth + "px"; var botMaxRadius = Math.max(this.settings.bl ? this.settings.bl.radius : 0, this.settings.br ? this.settings.br.radius : 0); newMainContainer.style.height = botMaxRadius + "px"; newMainContainer.style.bottom = 0 - botMaxRadius + "px"; newMainContainer.style.left = 0 - this.borderWidth + "px"; this.bottomContainer = this.box.appendChild(newMainContainer);} 65 | break;} 66 | } 67 | if(this.topContainer) this.box.style.borderTopWidth = "0px"; if(this.bottomContainer) this.box.style.borderBottomWidth = "0px"; var corners = ["tr", "tl", "br", "bl"]; for(var i in corners) 68 | { if(i > -1 < 4) 69 | { var cc = corners[i]; if(!this.settings[cc]) 70 | { if(((cc == "tr" || cc == "tl") && this.topContainer != null) || ((cc == "br" || cc == "bl") && this.bottomContainer != null)) 71 | { var newCorner = document.createElement("DIV"); newCorner.style.position = "relative"; newCorner.style.fontSize = "1px"; newCorner.style.overflow = "hidden"; if(this.backgroundImage == "") 72 | newCorner.style.backgroundColor = this.boxColour; else 73 | newCorner.style.backgroundImage = this.backgroundImage; switch(cc) 74 | { case "tl": 75 | newCorner.style.height = topMaxRadius - this.borderWidth + "px"; newCorner.style.marginRight = this.settings.tr.radius - (this.borderWidth*2) + "px"; newCorner.style.borderLeft = this.borderString; newCorner.style.borderTop = this.borderString; newCorner.style.left = -this.borderWidth + "px"; break; case "tr": 76 | newCorner.style.height = topMaxRadius - this.borderWidth + "px"; newCorner.style.marginLeft = this.settings.tl.radius - (this.borderWidth*2) + "px"; newCorner.style.borderRight = this.borderString; newCorner.style.borderTop = this.borderString; newCorner.style.backgroundPosition = "-" + (topMaxRadius + this.borderWidth) + "px 0px"; newCorner.style.left = this.borderWidth + "px"; break; case "bl": 77 | newCorner.style.height = botMaxRadius - this.borderWidth + "px"; newCorner.style.marginRight = this.settings.br.radius - (this.borderWidth*2) + "px"; newCorner.style.borderLeft = this.borderString; newCorner.style.borderBottom = this.borderString; newCorner.style.left = -this.borderWidth + "px"; newCorner.style.backgroundPosition = "-" + (this.borderWidth) + "px -" + (this.boxHeight + (botMaxRadius + this.borderWidth)) + "px"; break; case "br": 78 | newCorner.style.height = botMaxRadius - this.borderWidth + "px"; newCorner.style.marginLeft = this.settings.bl.radius - (this.borderWidth*2) + "px"; newCorner.style.borderRight = this.borderString; newCorner.style.borderBottom = this.borderString; newCorner.style.left = this.borderWidth + "px" 79 | newCorner.style.backgroundPosition = "-" + (botMaxRadius + this.borderWidth) + "px -" + (this.boxHeight + (botMaxRadius + this.borderWidth)) + "px"; break;} 80 | } 81 | } 82 | else 83 | { if(this.masterCorners[this.settings[cc].radius]) 84 | { var newCorner = this.masterCorners[this.settings[cc].radius].cloneNode(true);} 85 | else 86 | { var newCorner = document.createElement("DIV"); newCorner.style.height = this.settings[cc].radius + "px"; newCorner.style.width = this.settings[cc].radius + "px"; newCorner.style.position = "absolute"; newCorner.style.fontSize = "1px"; newCorner.style.overflow = "hidden"; var borderRadius = parseInt(this.settings[cc].radius - this.borderWidth); for(var intx = 0, j = this.settings[cc].radius; intx < j; intx++) 87 | { if((intx +1) >= borderRadius) 88 | var y1 = -1; else 89 | var y1 = (Math.floor(Math.sqrt(Math.pow(borderRadius, 2) - Math.pow((intx+1), 2))) - 1); if(borderRadius != j) 90 | { if((intx) >= borderRadius) 91 | var y2 = -1; else 92 | var y2 = Math.ceil(Math.sqrt(Math.pow(borderRadius,2) - Math.pow(intx, 2))); if((intx+1) >= j) 93 | var y3 = -1; else 94 | var y3 = (Math.floor(Math.sqrt(Math.pow(j ,2) - Math.pow((intx+1), 2))) - 1);} 95 | if((intx) >= j) 96 | var y4 = -1; else 97 | var y4 = Math.ceil(Math.sqrt(Math.pow(j ,2) - Math.pow(intx, 2))); if(y1 > -1) this.drawPixel(intx, 0, this.boxColour, 100, (y1+1), newCorner, -1, this.settings[cc].radius); if(borderRadius != j) 98 | { for(var inty = (y1 + 1); inty < y2; inty++) 99 | { if(this.settings.antiAlias) 100 | { if(this.backgroundImage != "") 101 | { var borderFract = (pixelFraction(intx, inty, borderRadius) * 100); if(borderFract < 30) 102 | { this.drawPixel(intx, inty, this.borderColour, 100, 1, newCorner, 0, this.settings[cc].radius);} 103 | else 104 | { this.drawPixel(intx, inty, this.borderColour, 100, 1, newCorner, -1, this.settings[cc].radius);} 105 | } 106 | else 107 | { var pixelcolour = BlendColour(this.boxColour, this.borderColour, pixelFraction(intx, inty, borderRadius)); this.drawPixel(intx, inty, pixelcolour, 100, 1, newCorner, 0, this.settings[cc].radius, cc);} 108 | } 109 | } 110 | if(this.settings.antiAlias) 111 | { if(y3 >= y2) 112 | { if (y2 == -1) y2 = 0; this.drawPixel(intx, y2, this.borderColour, 100, (y3 - y2 + 1), newCorner, 0, 0);} 113 | } 114 | else 115 | { if(y3 >= y1) 116 | { this.drawPixel(intx, (y1 + 1), this.borderColour, 100, (y3 - y1), newCorner, 0, 0);} 117 | } 118 | var outsideColour = this.borderColour;} 119 | else 120 | { var outsideColour = this.boxColour; var y3 = y1;} 121 | if(this.settings.antiAlias) 122 | { for(var inty = (y3 + 1); inty < y4; inty++) 123 | { this.drawPixel(intx, inty, outsideColour, (pixelFraction(intx, inty , j) * 100), 1, newCorner, ((this.borderWidth > 0)? 0 : -1), this.settings[cc].radius);} 124 | } 125 | } 126 | this.masterCorners[this.settings[cc].radius] = newCorner.cloneNode(true);} 127 | if(cc != "br") 128 | { for(var t = 0, k = newCorner.childNodes.length; t < k; t++) 129 | { var pixelBar = newCorner.childNodes[t]; var pixelBarTop = parseInt(pixelBar.style.top.substring(0, pixelBar.style.top.indexOf("px"))); var pixelBarLeft = parseInt(pixelBar.style.left.substring(0, pixelBar.style.left.indexOf("px"))); var pixelBarHeight = parseInt(pixelBar.style.height.substring(0, pixelBar.style.height.indexOf("px"))); if(cc == "tl" || cc == "bl"){ pixelBar.style.left = this.settings[cc].radius -pixelBarLeft -1 + "px";} 130 | if(cc == "tr" || cc == "tl"){ pixelBar.style.top = this.settings[cc].radius -pixelBarHeight -pixelBarTop + "px";} 131 | switch(cc) 132 | { case "tr": 133 | pixelBar.style.backgroundPosition = "-" + Math.abs((this.boxWidth - this.settings[cc].radius + this.borderWidth) + pixelBarLeft) + "px -" + Math.abs(this.settings[cc].radius -pixelBarHeight -pixelBarTop - this.borderWidth) + "px"; break; case "tl": 134 | pixelBar.style.backgroundPosition = "-" + Math.abs((this.settings[cc].radius -pixelBarLeft -1) - this.borderWidth) + "px -" + Math.abs(this.settings[cc].radius -pixelBarHeight -pixelBarTop - this.borderWidth) + "px"; break; case "bl": 135 | pixelBar.style.backgroundPosition = "-" + Math.abs((this.settings[cc].radius -pixelBarLeft -1) - this.borderWidth) + "px -" + Math.abs((this.boxHeight + this.settings[cc].radius + pixelBarTop) -this.borderWidth) + "px"; break;} 136 | } 137 | } 138 | } 139 | if(newCorner) 140 | { switch(cc) 141 | { case "tl": 142 | if(newCorner.style.position == "absolute") newCorner.style.top = "0px"; if(newCorner.style.position == "absolute") newCorner.style.left = "0px"; if(this.topContainer) this.topContainer.appendChild(newCorner); break; case "tr": 143 | if(newCorner.style.position == "absolute") newCorner.style.top = "0px"; if(newCorner.style.position == "absolute") newCorner.style.right = "0px"; if(this.topContainer) this.topContainer.appendChild(newCorner); break; case "bl": 144 | if(newCorner.style.position == "absolute") newCorner.style.bottom = "0px"; if(newCorner.style.position == "absolute") newCorner.style.left = "0px"; if(this.bottomContainer) this.bottomContainer.appendChild(newCorner); break; case "br": 145 | if(newCorner.style.position == "absolute") newCorner.style.bottom = "0px"; if(newCorner.style.position == "absolute") newCorner.style.right = "0px"; if(this.bottomContainer) this.bottomContainer.appendChild(newCorner); break;} 146 | } 147 | } 148 | } 149 | var radiusDiff = new Array(); radiusDiff["t"] = Math.abs(this.settings.tl.radius - this.settings.tr.radius) 150 | radiusDiff["b"] = Math.abs(this.settings.bl.radius - this.settings.br.radius); for(z in radiusDiff) 151 | { if(z == "t" || z == "b") 152 | { if(radiusDiff[z]) 153 | { var smallerCornerType = ((this.settings[z + "l"].radius < this.settings[z + "r"].radius)? z +"l" : z +"r"); var newFiller = document.createElement("DIV"); newFiller.style.height = radiusDiff[z] + "px"; newFiller.style.width = this.settings[smallerCornerType].radius+ "px" 154 | newFiller.style.position = "absolute"; newFiller.style.fontSize = "1px"; newFiller.style.overflow = "hidden"; newFiller.style.backgroundColor = this.boxColour; switch(smallerCornerType) 155 | { case "tl": 156 | newFiller.style.bottom = "0px"; newFiller.style.left = "0px"; newFiller.style.borderLeft = this.borderString; this.topContainer.appendChild(newFiller); break; case "tr": 157 | newFiller.style.bottom = "0px"; newFiller.style.right = "0px"; newFiller.style.borderRight = this.borderString; this.topContainer.appendChild(newFiller); break; case "bl": 158 | newFiller.style.top = "0px"; newFiller.style.left = "0px"; newFiller.style.borderLeft = this.borderString; this.bottomContainer.appendChild(newFiller); break; case "br": 159 | newFiller.style.top = "0px"; newFiller.style.right = "0px"; newFiller.style.borderRight = this.borderString; this.bottomContainer.appendChild(newFiller); break;} 160 | } 161 | var newFillerBar = document.createElement("DIV"); newFillerBar.style.position = "relative"; newFillerBar.style.fontSize = "1px"; newFillerBar.style.overflow = "hidden"; newFillerBar.style.backgroundColor = this.boxColour; newFillerBar.style.backgroundImage = this.backgroundImage; switch(z) 162 | { case "t": 163 | if(this.topContainer) 164 | { if(this.settings.tl.radius && this.settings.tr.radius) 165 | { newFillerBar.style.height = topMaxRadius - this.borderWidth + "px"; newFillerBar.style.marginLeft = this.settings.tl.radius - this.borderWidth + "px"; newFillerBar.style.marginRight = this.settings.tr.radius - this.borderWidth + "px"; newFillerBar.style.borderTop = this.borderString; if(this.backgroundImage != "") 166 | newFillerBar.style.backgroundPosition = "-" + (topMaxRadius + this.borderWidth) + "px 0px"; this.topContainer.appendChild(newFillerBar);} 167 | this.box.style.backgroundPosition = "0px -" + (topMaxRadius - this.borderWidth) + "px";} 168 | break; case "b": 169 | if(this.bottomContainer) 170 | { if(this.settings.bl.radius && this.settings.br.radius) 171 | { newFillerBar.style.height = botMaxRadius - this.borderWidth + "px"; newFillerBar.style.marginLeft = this.settings.bl.radius - this.borderWidth + "px"; newFillerBar.style.marginRight = this.settings.br.radius - this.borderWidth + "px"; newFillerBar.style.borderBottom = this.borderString; if(this.backgroundImage != "") 172 | newFillerBar.style.backgroundPosition = "-" + (botMaxRadius + this.borderWidth) + "px -" + (this.boxHeight + (topMaxRadius + this.borderWidth)) + "px"; this.bottomContainer.appendChild(newFillerBar);} 173 | } 174 | break;} 175 | } 176 | } 177 | if(this.settings.autoPad == true && this.boxPadding > 0) 178 | { var contentContainer = document.createElement("DIV"); contentContainer.style.position = "relative"; contentContainer.innerHTML = this.boxContent; contentContainer.className = "autoPadDiv"; var topPadding = Math.abs(topMaxRadius - this.boxPadding); var botPadding = Math.abs(botMaxRadius - this.boxPadding); if(topMaxRadius < this.boxPadding) 179 | contentContainer.style.paddingTop = topPadding + "px"; if(botMaxRadius < this.boxPadding) 180 | contentContainer.style.paddingBottom = botMaxRadius + "px"; contentContainer.style.paddingLeft = this.boxPadding + "px"; contentContainer.style.paddingRight = this.boxPadding + "px"; this.contentDIV = this.box.appendChild(contentContainer);} 181 | } 182 | this.drawPixel = function(intx, inty, colour, transAmount, height, newCorner, image, cornerRadius) 183 | { var pixel = document.createElement("DIV"); pixel.style.height = height + "px"; pixel.style.width = "1px"; pixel.style.position = "absolute"; pixel.style.fontSize = "1px"; pixel.style.overflow = "hidden"; var topMaxRadius = Math.max(this.settings["tr"].radius, this.settings["tl"].radius); if(image == -1 && this.backgroundImage != "") 184 | { pixel.style.backgroundImage = this.backgroundImage; pixel.style.backgroundPosition = "-" + (this.boxWidth - (cornerRadius - intx) + this.borderWidth) + "px -" + ((this.boxHeight + topMaxRadius + inty) -this.borderWidth) + "px";} 185 | else 186 | { pixel.style.backgroundColor = colour;} 187 | if (transAmount != 100) 188 | setOpacity(pixel, transAmount); pixel.style.top = inty + "px"; pixel.style.left = intx + "px"; newCorner.appendChild(pixel);} 189 | } 190 | function insertAfter(parent, node, referenceNode) 191 | { parent.insertBefore(node, referenceNode.nextSibling);} 192 | function BlendColour(Col1, Col2, Col1Fraction) 193 | { var red1 = parseInt(Col1.substr(1,2),16); var green1 = parseInt(Col1.substr(3,2),16); var blue1 = parseInt(Col1.substr(5,2),16); var red2 = parseInt(Col2.substr(1,2),16); var green2 = parseInt(Col2.substr(3,2),16); var blue2 = parseInt(Col2.substr(5,2),16); if(Col1Fraction > 1 || Col1Fraction < 0) Col1Fraction = 1; var endRed = Math.round((red1 * Col1Fraction) + (red2 * (1 - Col1Fraction))); if(endRed > 255) endRed = 255; if(endRed < 0) endRed = 0; var endGreen = Math.round((green1 * Col1Fraction) + (green2 * (1 - Col1Fraction))); if(endGreen > 255) endGreen = 255; if(endGreen < 0) endGreen = 0; var endBlue = Math.round((blue1 * Col1Fraction) + (blue2 * (1 - Col1Fraction))); if(endBlue > 255) endBlue = 255; if(endBlue < 0) endBlue = 0; return "#" + IntToHex(endRed)+ IntToHex(endGreen)+ IntToHex(endBlue);} 194 | function IntToHex(strNum) 195 | { base = strNum / 16; rem = strNum % 16; base = base - (rem / 16); baseS = MakeHex(base); remS = MakeHex(rem); return baseS + '' + remS;} 196 | function MakeHex(x) 197 | { if((x >= 0) && (x <= 9)) 198 | { return x;} 199 | else 200 | { switch(x) 201 | { case 10: return "A"; case 11: return "B"; case 12: return "C"; case 13: return "D"; case 14: return "E"; case 15: return "F";} 202 | } 203 | } 204 | function pixelFraction(x, y, r) 205 | { var pixelfraction = 0; var xvalues = new Array(1); var yvalues = new Array(1); var point = 0; var whatsides = ""; var intersect = Math.sqrt((Math.pow(r,2) - Math.pow(x,2))); if ((intersect >= y) && (intersect < (y+1))) 206 | { whatsides = "Left"; xvalues[point] = 0; yvalues[point] = intersect - y; point = point + 1;} 207 | var intersect = Math.sqrt((Math.pow(r,2) - Math.pow(y+1,2))); if ((intersect >= x) && (intersect < (x+1))) 208 | { whatsides = whatsides + "Top"; xvalues[point] = intersect - x; yvalues[point] = 1; point = point + 1;} 209 | var intersect = Math.sqrt((Math.pow(r,2) - Math.pow(x+1,2))); if ((intersect >= y) && (intersect < (y+1))) 210 | { whatsides = whatsides + "Right"; xvalues[point] = 1; yvalues[point] = intersect - y; point = point + 1;} 211 | var intersect = Math.sqrt((Math.pow(r,2) - Math.pow(y,2))); if ((intersect >= x) && (intersect < (x+1))) 212 | { whatsides = whatsides + "Bottom"; xvalues[point] = intersect - x; yvalues[point] = 0;} 213 | switch (whatsides) 214 | { case "LeftRight": 215 | pixelfraction = Math.min(yvalues[0],yvalues[1]) + ((Math.max(yvalues[0],yvalues[1]) - Math.min(yvalues[0],yvalues[1]))/2); break; case "TopRight": 216 | pixelfraction = 1-(((1-xvalues[0])*(1-yvalues[1]))/2); break; case "TopBottom": 217 | pixelfraction = Math.min(xvalues[0],xvalues[1]) + ((Math.max(xvalues[0],xvalues[1]) - Math.min(xvalues[0],xvalues[1]))/2); break; case "LeftBottom": 218 | pixelfraction = (yvalues[0]*xvalues[1])/2; break; default: 219 | pixelfraction = 1;} 220 | return pixelfraction;} 221 | function rgb2Hex(rgbColour) 222 | { try{ var rgbArray = rgb2Array(rgbColour); var red = parseInt(rgbArray[0]); var green = parseInt(rgbArray[1]); var blue = parseInt(rgbArray[2]); var hexColour = "#" + IntToHex(red) + IntToHex(green) + IntToHex(blue);} 223 | catch(e){ alert("There was an error converting the RGB value to Hexadecimal in function rgb2Hex");} 224 | return hexColour;} 225 | function rgb2Array(rgbColour) 226 | { var rgbValues = rgbColour.substring(4, rgbColour.indexOf(")")); var rgbArray = rgbValues.split(", "); return rgbArray;} 227 | function setOpacity(obj, opacity) 228 | { opacity = (opacity == 100)?99.999:opacity; if(isSafari && obj.tagName != "IFRAME") 229 | { var rgbArray = rgb2Array(obj.style.backgroundColor); var red = parseInt(rgbArray[0]); var green = parseInt(rgbArray[1]); var blue = parseInt(rgbArray[2]); obj.style.backgroundColor = "rgba(" + red + ", " + green + ", " + blue + ", " + opacity/100 + ")";} 230 | else if(typeof(obj.style.opacity) != "undefined") 231 | { obj.style.opacity = opacity/100;} 232 | else if(typeof(obj.style.MozOpacity) != "undefined") 233 | { obj.style.MozOpacity = opacity/100;} 234 | else if(typeof(obj.style.filter) != "undefined") 235 | { obj.style.filter = "alpha(opacity:" + opacity + ")";} 236 | else if(typeof(obj.style.KHTMLOpacity) != "undefined") 237 | { obj.style.KHTMLOpacity = opacity/100;} 238 | } 239 | function inArray(array, value) 240 | { for(var i = 0; i < array.length; i++){ if (array[i] === value) return i;} 241 | return false;} 242 | function inArrayKey(array, value) 243 | { for(key in array){ if(key === value) return true;} 244 | return false;} 245 | function addEvent(elm, evType, fn, useCapture) { if (elm.addEventListener) { elm.addEventListener(evType, fn, useCapture); return true;} 246 | else if (elm.attachEvent) { var r = elm.attachEvent('on' + evType, fn); return r;} 247 | else { elm['on' + evType] = fn;} 248 | } 249 | function removeEvent(obj, evType, fn, useCapture){ if (obj.removeEventListener){ obj.removeEventListener(evType, fn, useCapture); return true;} else if (obj.detachEvent){ var r = obj.detachEvent("on"+evType, fn); return r;} else { alert("Handler could not be removed");} 250 | } 251 | function format_colour(colour) 252 | { var returnColour = "#ffffff"; if(colour != "" && colour != "transparent") 253 | { if(colour.substr(0, 3) == "rgb") 254 | { returnColour = rgb2Hex(colour);} 255 | else if(colour.length == 4) 256 | { returnColour = "#" + colour.substring(1, 2) + colour.substring(1, 2) + colour.substring(2, 3) + colour.substring(2, 3) + colour.substring(3, 4) + colour.substring(3, 4);} 257 | else 258 | { returnColour = colour;} 259 | } 260 | return returnColour;} 261 | function get_style(obj, property, propertyNS) 262 | { try 263 | { if(obj.currentStyle) 264 | { var returnVal = eval("obj.currentStyle." + property);} 265 | else 266 | { if(isSafari && obj.style.display == "none") 267 | { obj.style.display = ""; var wasHidden = true;} 268 | var returnVal = document.defaultView.getComputedStyle(obj, '').getPropertyValue(propertyNS); if(isSafari && wasHidden) 269 | { obj.style.display = "none";} 270 | } 271 | } 272 | catch(e) 273 | { } 274 | return returnVal;} 275 | function getElementsByClass(searchClass, node, tag) 276 | { var classElements = new Array(); if(node == null) 277 | node = document; if(tag == null) 278 | tag = '*'; var els = node.getElementsByTagName(tag); var elsLen = els.length; var pattern = new RegExp("(^|\s)"+searchClass+"(\s|$)"); for (i = 0, j = 0; i < elsLen; i++) 279 | { if(pattern.test(els[i].className)) 280 | { classElements[j] = els[i]; j++;} 281 | } 282 | return classElements;} 283 | function newCurvyError(errorMessage) 284 | { return new Error("curvyCorners Error:\n" + errorMessage) 285 | } 286 | -------------------------------------------------------------------------------- /setup.rb: -------------------------------------------------------------------------------- 1 | # 2 | # setup.rb 3 | # 4 | # Copyright (c) 2000-2005 Minero Aoki 5 | # 6 | # This program is free software. 7 | # You can distribute/modify this program under the terms of 8 | # the GNU LGPL, Lesser General Public License version 2.1. 9 | # 10 | 11 | unless Enumerable.method_defined?(:map) # Ruby 1.4.6 12 | module Enumerable 13 | alias map collect 14 | end 15 | end 16 | 17 | unless File.respond_to?(:read) # Ruby 1.6 18 | def File.read(fname) 19 | open(fname) {|f| 20 | return f.read 21 | } 22 | end 23 | end 24 | 25 | unless Errno.const_defined?(:ENOTEMPTY) # Windows? 26 | module Errno 27 | class ENOTEMPTY 28 | # We do not raise this exception, implementation is not needed. 29 | end 30 | end 31 | end 32 | 33 | def File.binread(fname) 34 | open(fname, 'rb') {|f| 35 | return f.read 36 | } 37 | end 38 | 39 | # for corrupted Windows' stat(2) 40 | def File.dir?(path) 41 | File.directory?((path[-1,1] == '/') ? path : path + '/') 42 | end 43 | 44 | 45 | class ConfigTable 46 | 47 | include Enumerable 48 | 49 | def initialize(rbconfig) 50 | @rbconfig = rbconfig 51 | @items = [] 52 | @table = {} 53 | # options 54 | @install_prefix = nil 55 | @config_opt = nil 56 | @verbose = true 57 | @no_harm = false 58 | end 59 | 60 | attr_accessor :install_prefix 61 | attr_accessor :config_opt 62 | 63 | attr_writer :verbose 64 | 65 | def verbose? 66 | @verbose 67 | end 68 | 69 | attr_writer :no_harm 70 | 71 | def no_harm? 72 | @no_harm 73 | end 74 | 75 | def [](key) 76 | lookup(key).resolve(self) 77 | end 78 | 79 | def []=(key, val) 80 | lookup(key).set val 81 | end 82 | 83 | def names 84 | @items.map {|i| i.name } 85 | end 86 | 87 | def each(&block) 88 | @items.each(&block) 89 | end 90 | 91 | def key?(name) 92 | @table.key?(name) 93 | end 94 | 95 | def lookup(name) 96 | @table[name] or setup_rb_error "no such config item: #{name}" 97 | end 98 | 99 | def add(item) 100 | @items.push item 101 | @table[item.name] = item 102 | end 103 | 104 | def remove(name) 105 | item = lookup(name) 106 | @items.delete_if {|i| i.name == name } 107 | @table.delete_if {|name, i| i.name == name } 108 | item 109 | end 110 | 111 | def load_script(path, inst = nil) 112 | if File.file?(path) 113 | MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path 114 | end 115 | end 116 | 117 | def savefile 118 | '.config' 119 | end 120 | 121 | def load_savefile 122 | begin 123 | File.foreach(savefile()) do |line| 124 | k, v = *line.split(/=/, 2) 125 | self[k] = v.strip 126 | end 127 | rescue Errno::ENOENT 128 | setup_rb_error $!.message + "\n#{File.basename($0)} config first" 129 | end 130 | end 131 | 132 | def save 133 | @items.each {|i| i.value } 134 | File.open(savefile(), 'w') {|f| 135 | @items.each do |i| 136 | f.printf "%s=%s\n", i.name, i.value if i.value? and i.value 137 | end 138 | } 139 | end 140 | 141 | def load_standard_entries 142 | standard_entries(@rbconfig).each do |ent| 143 | add ent 144 | end 145 | end 146 | 147 | def standard_entries(rbconfig) 148 | c = rbconfig 149 | 150 | rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) 151 | 152 | major = c['MAJOR'].to_i 153 | minor = c['MINOR'].to_i 154 | teeny = c['TEENY'].to_i 155 | version = "#{major}.#{minor}" 156 | 157 | # ruby ver. >= 1.4.4? 158 | newpath_p = ((major >= 2) or 159 | ((major == 1) and 160 | ((minor >= 5) or 161 | ((minor == 4) and (teeny >= 4))))) 162 | 163 | if c['rubylibdir'] 164 | # V > 1.6.3 165 | libruby = "#{c['prefix']}/lib/ruby" 166 | librubyver = c['rubylibdir'] 167 | librubyverarch = c['archdir'] 168 | siteruby = c['sitedir'] 169 | siterubyver = c['sitelibdir'] 170 | siterubyverarch = c['sitearchdir'] 171 | elsif newpath_p 172 | # 1.4.4 <= V <= 1.6.3 173 | libruby = "#{c['prefix']}/lib/ruby" 174 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 175 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 176 | siteruby = c['sitedir'] 177 | siterubyver = "$siteruby/#{version}" 178 | siterubyverarch = "$siterubyver/#{c['arch']}" 179 | else 180 | # V < 1.4.4 181 | libruby = "#{c['prefix']}/lib/ruby" 182 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 183 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 184 | siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" 185 | siterubyver = siteruby 186 | siterubyverarch = "$siterubyver/#{c['arch']}" 187 | end 188 | parameterize = lambda {|path| 189 | path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') 190 | } 191 | 192 | if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } 193 | makeprog = arg.sub(/'/, '').split(/=/, 2)[1] 194 | else 195 | makeprog = 'make' 196 | end 197 | 198 | [ 199 | ExecItem.new('installdirs', 'std/site/home', 200 | 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ 201 | {|val, table| 202 | case val 203 | when 'std' 204 | table['rbdir'] = '$librubyver' 205 | table['sodir'] = '$librubyverarch' 206 | when 'site' 207 | table['rbdir'] = '$siterubyver' 208 | table['sodir'] = '$siterubyverarch' 209 | when 'home' 210 | setup_rb_error '$HOME was not set' unless ENV['HOME'] 211 | table['prefix'] = ENV['HOME'] 212 | table['rbdir'] = '$libdir/ruby' 213 | table['sodir'] = '$libdir/ruby' 214 | end 215 | }, 216 | PathItem.new('prefix', 'path', c['prefix'], 217 | 'path prefix of target environment'), 218 | PathItem.new('bindir', 'path', parameterize.call(c['bindir']), 219 | 'the directory for commands'), 220 | PathItem.new('libdir', 'path', parameterize.call(c['libdir']), 221 | 'the directory for libraries'), 222 | PathItem.new('datadir', 'path', parameterize.call(c['datadir']), 223 | 'the directory for shared data'), 224 | PathItem.new('mandir', 'path', parameterize.call(c['mandir']), 225 | 'the directory for man pages'), 226 | PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), 227 | 'the directory for system configuration files'), 228 | PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), 229 | 'the directory for local state data'), 230 | PathItem.new('libruby', 'path', libruby, 231 | 'the directory for ruby libraries'), 232 | PathItem.new('librubyver', 'path', librubyver, 233 | 'the directory for standard ruby libraries'), 234 | PathItem.new('librubyverarch', 'path', librubyverarch, 235 | 'the directory for standard ruby extensions'), 236 | PathItem.new('siteruby', 'path', siteruby, 237 | 'the directory for version-independent aux ruby libraries'), 238 | PathItem.new('siterubyver', 'path', siterubyver, 239 | 'the directory for aux ruby libraries'), 240 | PathItem.new('siterubyverarch', 'path', siterubyverarch, 241 | 'the directory for aux ruby binaries'), 242 | PathItem.new('rbdir', 'path', '$siterubyver', 243 | 'the directory for ruby scripts'), 244 | PathItem.new('sodir', 'path', '$siterubyverarch', 245 | 'the directory for ruby extentions'), 246 | PathItem.new('rubypath', 'path', rubypath, 247 | 'the path to set to #! line'), 248 | ProgramItem.new('rubyprog', 'name', rubypath, 249 | 'the ruby program using for installation'), 250 | ProgramItem.new('makeprog', 'name', makeprog, 251 | 'the make program to compile ruby extentions'), 252 | SelectItem.new('shebang', 'all/ruby/never', 'ruby', 253 | 'shebang line (#!) editing mode'), 254 | BoolItem.new('without-ext', 'yes/no', 'no', 255 | 'does not compile/install ruby extentions') 256 | ] 257 | end 258 | private :standard_entries 259 | 260 | def load_multipackage_entries 261 | multipackage_entries().each do |ent| 262 | add ent 263 | end 264 | end 265 | 266 | def multipackage_entries 267 | [ 268 | PackageSelectionItem.new('with', 'name,name...', '', 'ALL', 269 | 'package names that you want to install'), 270 | PackageSelectionItem.new('without', 'name,name...', '', 'NONE', 271 | 'package names that you do not want to install') 272 | ] 273 | end 274 | private :multipackage_entries 275 | 276 | ALIASES = { 277 | 'std-ruby' => 'librubyver', 278 | 'stdruby' => 'librubyver', 279 | 'rubylibdir' => 'librubyver', 280 | 'archdir' => 'librubyverarch', 281 | 'site-ruby-common' => 'siteruby', # For backward compatibility 282 | 'site-ruby' => 'siterubyver', # For backward compatibility 283 | 'bin-dir' => 'bindir', 284 | 'bin-dir' => 'bindir', 285 | 'rb-dir' => 'rbdir', 286 | 'so-dir' => 'sodir', 287 | 'data-dir' => 'datadir', 288 | 'ruby-path' => 'rubypath', 289 | 'ruby-prog' => 'rubyprog', 290 | 'ruby' => 'rubyprog', 291 | 'make-prog' => 'makeprog', 292 | 'make' => 'makeprog' 293 | } 294 | 295 | def fixup 296 | ALIASES.each do |ali, name| 297 | @table[ali] = @table[name] 298 | end 299 | @items.freeze 300 | @table.freeze 301 | @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ 302 | end 303 | 304 | def parse_opt(opt) 305 | m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" 306 | m.to_a[1,2] 307 | end 308 | 309 | def dllext 310 | @rbconfig['DLEXT'] 311 | end 312 | 313 | def value_config?(name) 314 | lookup(name).value? 315 | end 316 | 317 | class Item 318 | def initialize(name, template, default, desc) 319 | @name = name.freeze 320 | @template = template 321 | @value = default 322 | @default = default 323 | @description = desc 324 | end 325 | 326 | attr_reader :name 327 | attr_reader :description 328 | 329 | attr_accessor :default 330 | alias help_default default 331 | 332 | def help_opt 333 | "--#{@name}=#{@template}" 334 | end 335 | 336 | def value? 337 | true 338 | end 339 | 340 | def value 341 | @value 342 | end 343 | 344 | def resolve(table) 345 | @value.gsub(%r<\$([^/]+)>) { table[$1] } 346 | end 347 | 348 | def set(val) 349 | @value = check(val) 350 | end 351 | 352 | private 353 | 354 | def check(val) 355 | setup_rb_error "config: --#{name} requires argument" unless val 356 | val 357 | end 358 | end 359 | 360 | class BoolItem < Item 361 | def config_type 362 | 'bool' 363 | end 364 | 365 | def help_opt 366 | "--#{@name}" 367 | end 368 | 369 | private 370 | 371 | def check(val) 372 | return 'yes' unless val 373 | case val 374 | when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' 375 | when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' 376 | else 377 | setup_rb_error "config: --#{@name} accepts only yes/no for argument" 378 | end 379 | end 380 | end 381 | 382 | class PathItem < Item 383 | def config_type 384 | 'path' 385 | end 386 | 387 | private 388 | 389 | def check(path) 390 | setup_rb_error "config: --#{@name} requires argument" unless path 391 | path[0,1] == '$' ? path : File.expand_path(path) 392 | end 393 | end 394 | 395 | class ProgramItem < Item 396 | def config_type 397 | 'program' 398 | end 399 | end 400 | 401 | class SelectItem < Item 402 | def initialize(name, selection, default, desc) 403 | super 404 | @ok = selection.split('/') 405 | end 406 | 407 | def config_type 408 | 'select' 409 | end 410 | 411 | private 412 | 413 | def check(val) 414 | unless @ok.include?(val.strip) 415 | setup_rb_error "config: use --#{@name}=#{@template} (#{val})" 416 | end 417 | val.strip 418 | end 419 | end 420 | 421 | class ExecItem < Item 422 | def initialize(name, selection, desc, &block) 423 | super name, selection, nil, desc 424 | @ok = selection.split('/') 425 | @action = block 426 | end 427 | 428 | def config_type 429 | 'exec' 430 | end 431 | 432 | def value? 433 | false 434 | end 435 | 436 | def resolve(table) 437 | setup_rb_error "$#{name()} wrongly used as option value" 438 | end 439 | 440 | undef set 441 | 442 | def evaluate(val, table) 443 | v = val.strip.downcase 444 | unless @ok.include?(v) 445 | setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" 446 | end 447 | @action.call v, table 448 | end 449 | end 450 | 451 | class PackageSelectionItem < Item 452 | def initialize(name, template, default, help_default, desc) 453 | super name, template, default, desc 454 | @help_default = help_default 455 | end 456 | 457 | attr_reader :help_default 458 | 459 | def config_type 460 | 'package' 461 | end 462 | 463 | private 464 | 465 | def check(val) 466 | unless File.dir?("packages/#{val}") 467 | setup_rb_error "config: no such package: #{val}" 468 | end 469 | val 470 | end 471 | end 472 | 473 | class MetaConfigEnvironment 474 | def initialize(config, installer) 475 | @config = config 476 | @installer = installer 477 | end 478 | 479 | def config_names 480 | @config.names 481 | end 482 | 483 | def config?(name) 484 | @config.key?(name) 485 | end 486 | 487 | def bool_config?(name) 488 | @config.lookup(name).config_type == 'bool' 489 | end 490 | 491 | def path_config?(name) 492 | @config.lookup(name).config_type == 'path' 493 | end 494 | 495 | def value_config?(name) 496 | @config.lookup(name).config_type != 'exec' 497 | end 498 | 499 | def add_config(item) 500 | @config.add item 501 | end 502 | 503 | def add_bool_config(name, default, desc) 504 | @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) 505 | end 506 | 507 | def add_path_config(name, default, desc) 508 | @config.add PathItem.new(name, 'path', default, desc) 509 | end 510 | 511 | def set_config_default(name, default) 512 | @config.lookup(name).default = default 513 | end 514 | 515 | def remove_config(name) 516 | @config.remove(name) 517 | end 518 | 519 | # For only multipackage 520 | def packages 521 | raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer 522 | @installer.packages 523 | end 524 | 525 | # For only multipackage 526 | def declare_packages(list) 527 | raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer 528 | @installer.packages = list 529 | end 530 | end 531 | 532 | end # class ConfigTable 533 | 534 | 535 | # This module requires: #verbose?, #no_harm? 536 | module FileOperations 537 | 538 | def mkdir_p(dirname, prefix = nil) 539 | dirname = prefix + File.expand_path(dirname) if prefix 540 | $stderr.puts "mkdir -p #{dirname}" if verbose? 541 | return if no_harm? 542 | 543 | # Does not check '/', it's too abnormal. 544 | dirs = File.expand_path(dirname).split(%r<(?=/)>) 545 | if /\A[a-z]:\z/i =~ dirs[0] 546 | disk = dirs.shift 547 | dirs[0] = disk + dirs[0] 548 | end 549 | dirs.each_index do |idx| 550 | path = dirs[0..idx].join('') 551 | Dir.mkdir path unless File.dir?(path) 552 | end 553 | end 554 | 555 | def rm_f(path) 556 | $stderr.puts "rm -f #{path}" if verbose? 557 | return if no_harm? 558 | force_remove_file path 559 | end 560 | 561 | def rm_rf(path) 562 | $stderr.puts "rm -rf #{path}" if verbose? 563 | return if no_harm? 564 | remove_tree path 565 | end 566 | 567 | def remove_tree(path) 568 | if File.symlink?(path) 569 | remove_file path 570 | elsif File.dir?(path) 571 | remove_tree0 path 572 | else 573 | force_remove_file path 574 | end 575 | end 576 | 577 | def remove_tree0(path) 578 | Dir.foreach(path) do |ent| 579 | next if ent == '.' 580 | next if ent == '..' 581 | entpath = "#{path}/#{ent}" 582 | if File.symlink?(entpath) 583 | remove_file entpath 584 | elsif File.dir?(entpath) 585 | remove_tree0 entpath 586 | else 587 | force_remove_file entpath 588 | end 589 | end 590 | begin 591 | Dir.rmdir path 592 | rescue Errno::ENOTEMPTY 593 | # directory may not be empty 594 | end 595 | end 596 | 597 | def move_file(src, dest) 598 | force_remove_file dest 599 | begin 600 | File.rename src, dest 601 | rescue 602 | File.open(dest, 'wb') {|f| 603 | f.write File.binread(src) 604 | } 605 | File.chmod File.stat(src).mode, dest 606 | File.unlink src 607 | end 608 | end 609 | 610 | def force_remove_file(path) 611 | begin 612 | remove_file path 613 | rescue 614 | end 615 | end 616 | 617 | def remove_file(path) 618 | File.chmod 0777, path 619 | File.unlink path 620 | end 621 | 622 | def install(from, dest, mode, prefix = nil) 623 | $stderr.puts "install #{from} #{dest}" if verbose? 624 | return if no_harm? 625 | 626 | realdest = prefix ? prefix + File.expand_path(dest) : dest 627 | realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) 628 | str = File.binread(from) 629 | if diff?(str, realdest) 630 | verbose_off { 631 | rm_f realdest if File.exist?(realdest) 632 | } 633 | File.open(realdest, 'wb') {|f| 634 | f.write str 635 | } 636 | File.chmod mode, realdest 637 | 638 | File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| 639 | if prefix 640 | f.puts realdest.sub(prefix, '') 641 | else 642 | f.puts realdest 643 | end 644 | } 645 | end 646 | end 647 | 648 | def diff?(new_content, path) 649 | return true unless File.exist?(path) 650 | new_content != File.binread(path) 651 | end 652 | 653 | def command(*args) 654 | $stderr.puts args.join(' ') if verbose? 655 | system(*args) or raise RuntimeError, 656 | "system(#{args.map{|a| a.inspect }.join(' ')}) failed" 657 | end 658 | 659 | def ruby(*args) 660 | command config('rubyprog'), *args 661 | end 662 | 663 | def make(task = nil) 664 | command(*[config('makeprog'), task].compact) 665 | end 666 | 667 | def extdir?(dir) 668 | File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") 669 | end 670 | 671 | def files_of(dir) 672 | Dir.open(dir) {|d| 673 | return d.select {|ent| File.file?("#{dir}/#{ent}") } 674 | } 675 | end 676 | 677 | DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) 678 | 679 | def directories_of(dir) 680 | Dir.open(dir) {|d| 681 | return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT 682 | } 683 | end 684 | 685 | end 686 | 687 | 688 | # This module requires: #srcdir_root, #objdir_root, #relpath 689 | module HookScriptAPI 690 | 691 | def get_config(key) 692 | @config[key] 693 | end 694 | 695 | alias config get_config 696 | 697 | # obsolete: use metaconfig to change configuration 698 | def set_config(key, val) 699 | @config[key] = val 700 | end 701 | 702 | # 703 | # srcdir/objdir (works only in the package directory) 704 | # 705 | 706 | def curr_srcdir 707 | "#{srcdir_root()}/#{relpath()}" 708 | end 709 | 710 | def curr_objdir 711 | "#{objdir_root()}/#{relpath()}" 712 | end 713 | 714 | def srcfile(path) 715 | "#{curr_srcdir()}/#{path}" 716 | end 717 | 718 | def srcexist?(path) 719 | File.exist?(srcfile(path)) 720 | end 721 | 722 | def srcdirectory?(path) 723 | File.dir?(srcfile(path)) 724 | end 725 | 726 | def srcfile?(path) 727 | File.file?(srcfile(path)) 728 | end 729 | 730 | def srcentries(path = '.') 731 | Dir.open("#{curr_srcdir()}/#{path}") {|d| 732 | return d.to_a - %w(. ..) 733 | } 734 | end 735 | 736 | def srcfiles(path = '.') 737 | srcentries(path).select {|fname| 738 | File.file?(File.join(curr_srcdir(), path, fname)) 739 | } 740 | end 741 | 742 | def srcdirectories(path = '.') 743 | srcentries(path).select {|fname| 744 | File.dir?(File.join(curr_srcdir(), path, fname)) 745 | } 746 | end 747 | 748 | end 749 | 750 | 751 | class ToplevelInstaller 752 | 753 | Version = '3.4.1' 754 | Copyright = 'Copyright (c) 2000-2005 Minero Aoki' 755 | 756 | TASKS = [ 757 | [ 'all', 'do config, setup, then install' ], 758 | [ 'config', 'saves your configurations' ], 759 | [ 'show', 'shows current configuration' ], 760 | [ 'setup', 'compiles ruby extentions and others' ], 761 | [ 'install', 'installs files' ], 762 | [ 'test', 'run all tests in test/' ], 763 | [ 'clean', "does `make clean' for each extention" ], 764 | [ 'distclean',"does `make distclean' for each extention" ] 765 | ] 766 | 767 | def ToplevelInstaller.invoke 768 | config = ConfigTable.new(load_rbconfig()) 769 | config.load_standard_entries 770 | config.load_multipackage_entries if multipackage? 771 | config.fixup 772 | klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) 773 | klass.new(File.dirname($0), config).invoke 774 | end 775 | 776 | def ToplevelInstaller.multipackage? 777 | File.dir?(File.dirname($0) + '/packages') 778 | end 779 | 780 | def ToplevelInstaller.load_rbconfig 781 | if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } 782 | ARGV.delete(arg) 783 | load File.expand_path(arg.split(/=/, 2)[1]) 784 | $".push 'rbconfig.rb' 785 | else 786 | require 'rbconfig' 787 | end 788 | ::Config::CONFIG 789 | end 790 | 791 | def initialize(ardir_root, config) 792 | @ardir = File.expand_path(ardir_root) 793 | @config = config 794 | # cache 795 | @valid_task_re = nil 796 | end 797 | 798 | def config(key) 799 | @config[key] 800 | end 801 | 802 | def inspect 803 | "#<#{self.class} #{__id__()}>" 804 | end 805 | 806 | def invoke 807 | run_metaconfigs 808 | case task = parsearg_global() 809 | when nil, 'all' 810 | parsearg_config 811 | init_installers 812 | exec_config 813 | exec_setup 814 | exec_install 815 | else 816 | case task 817 | when 'config', 'test' 818 | ; 819 | when 'clean', 'distclean' 820 | @config.load_savefile if File.exist?(@config.savefile) 821 | else 822 | @config.load_savefile 823 | end 824 | __send__ "parsearg_#{task}" 825 | init_installers 826 | __send__ "exec_#{task}" 827 | end 828 | end 829 | 830 | def run_metaconfigs 831 | @config.load_script "#{@ardir}/metaconfig" 832 | end 833 | 834 | def init_installers 835 | @installer = Installer.new(@config, @ardir, File.expand_path('.')) 836 | end 837 | 838 | # 839 | # Hook Script API bases 840 | # 841 | 842 | def srcdir_root 843 | @ardir 844 | end 845 | 846 | def objdir_root 847 | '.' 848 | end 849 | 850 | def relpath 851 | '.' 852 | end 853 | 854 | # 855 | # Option Parsing 856 | # 857 | 858 | def parsearg_global 859 | while arg = ARGV.shift 860 | case arg 861 | when /\A\w+\z/ 862 | setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) 863 | return arg 864 | when '-q', '--quiet' 865 | @config.verbose = false 866 | when '--verbose' 867 | @config.verbose = true 868 | when '--help' 869 | print_usage $stdout 870 | exit 0 871 | when '--version' 872 | puts "#{File.basename($0)} version #{Version}" 873 | exit 0 874 | when '--copyright' 875 | puts Copyright 876 | exit 0 877 | else 878 | setup_rb_error "unknown global option '#{arg}'" 879 | end 880 | end 881 | nil 882 | end 883 | 884 | def valid_task?(t) 885 | valid_task_re() =~ t 886 | end 887 | 888 | def valid_task_re 889 | @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ 890 | end 891 | 892 | def parsearg_no_options 893 | unless ARGV.empty? 894 | task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) 895 | setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" 896 | end 897 | end 898 | 899 | alias parsearg_show parsearg_no_options 900 | alias parsearg_setup parsearg_no_options 901 | alias parsearg_test parsearg_no_options 902 | alias parsearg_clean parsearg_no_options 903 | alias parsearg_distclean parsearg_no_options 904 | 905 | def parsearg_config 906 | evalopt = [] 907 | set = [] 908 | @config.config_opt = [] 909 | while i = ARGV.shift 910 | if /\A--?\z/ =~ i 911 | @config.config_opt = ARGV.dup 912 | break 913 | end 914 | name, value = *@config.parse_opt(i) 915 | if @config.value_config?(name) 916 | @config[name] = value 917 | else 918 | evalopt.push [name, value] 919 | end 920 | set.push name 921 | end 922 | evalopt.each do |name, value| 923 | @config.lookup(name).evaluate value, @config 924 | end 925 | # Check if configuration is valid 926 | set.each do |n| 927 | @config[n] if @config.value_config?(n) 928 | end 929 | end 930 | 931 | def parsearg_install 932 | @config.no_harm = false 933 | @config.install_prefix = '' 934 | while a = ARGV.shift 935 | case a 936 | when '--no-harm' 937 | @config.no_harm = true 938 | when /\A--prefix=/ 939 | path = a.split(/=/, 2)[1] 940 | path = File.expand_path(path) unless path[0,1] == '/' 941 | @config.install_prefix = path 942 | else 943 | setup_rb_error "install: unknown option #{a}" 944 | end 945 | end 946 | end 947 | 948 | def print_usage(out) 949 | out.puts 'Typical Installation Procedure:' 950 | out.puts " $ ruby #{File.basename $0} config" 951 | out.puts " $ ruby #{File.basename $0} setup" 952 | out.puts " # ruby #{File.basename $0} install (may require root privilege)" 953 | out.puts 954 | out.puts 'Detailed Usage:' 955 | out.puts " ruby #{File.basename $0} " 956 | out.puts " ruby #{File.basename $0} [] []" 957 | 958 | fmt = " %-24s %s\n" 959 | out.puts 960 | out.puts 'Global options:' 961 | out.printf fmt, '-q,--quiet', 'suppress message outputs' 962 | out.printf fmt, ' --verbose', 'output messages verbosely' 963 | out.printf fmt, ' --help', 'print this message' 964 | out.printf fmt, ' --version', 'print version and quit' 965 | out.printf fmt, ' --copyright', 'print copyright and quit' 966 | out.puts 967 | out.puts 'Tasks:' 968 | TASKS.each do |name, desc| 969 | out.printf fmt, name, desc 970 | end 971 | 972 | fmt = " %-24s %s [%s]\n" 973 | out.puts 974 | out.puts 'Options for CONFIG or ALL:' 975 | @config.each do |item| 976 | out.printf fmt, item.help_opt, item.description, item.help_default 977 | end 978 | out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" 979 | out.puts 980 | out.puts 'Options for INSTALL:' 981 | out.printf fmt, '--no-harm', 'only display what to do if given', 'off' 982 | out.printf fmt, '--prefix=path', 'install path prefix', '' 983 | out.puts 984 | end 985 | 986 | # 987 | # Task Handlers 988 | # 989 | 990 | def exec_config 991 | @installer.exec_config 992 | @config.save # must be final 993 | end 994 | 995 | def exec_setup 996 | @installer.exec_setup 997 | end 998 | 999 | def exec_install 1000 | @installer.exec_install 1001 | end 1002 | 1003 | def exec_test 1004 | @installer.exec_test 1005 | end 1006 | 1007 | def exec_show 1008 | @config.each do |i| 1009 | printf "%-20s %s\n", i.name, i.value if i.value? 1010 | end 1011 | end 1012 | 1013 | def exec_clean 1014 | @installer.exec_clean 1015 | end 1016 | 1017 | def exec_distclean 1018 | @installer.exec_distclean 1019 | end 1020 | 1021 | end # class ToplevelInstaller 1022 | 1023 | 1024 | class ToplevelInstallerMulti < ToplevelInstaller 1025 | 1026 | include FileOperations 1027 | 1028 | def initialize(ardir_root, config) 1029 | super 1030 | @packages = directories_of("#{@ardir}/packages") 1031 | raise 'no package exists' if @packages.empty? 1032 | @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) 1033 | end 1034 | 1035 | def run_metaconfigs 1036 | @config.load_script "#{@ardir}/metaconfig", self 1037 | @packages.each do |name| 1038 | @config.load_script "#{@ardir}/packages/#{name}/metaconfig" 1039 | end 1040 | end 1041 | 1042 | attr_reader :packages 1043 | 1044 | def packages=(list) 1045 | raise 'package list is empty' if list.empty? 1046 | list.each do |name| 1047 | raise "directory packages/#{name} does not exist"\ 1048 | unless File.dir?("#{@ardir}/packages/#{name}") 1049 | end 1050 | @packages = list 1051 | end 1052 | 1053 | def init_installers 1054 | @installers = {} 1055 | @packages.each do |pack| 1056 | @installers[pack] = Installer.new(@config, 1057 | "#{@ardir}/packages/#{pack}", 1058 | "packages/#{pack}") 1059 | end 1060 | with = extract_selection(config('with')) 1061 | without = extract_selection(config('without')) 1062 | @selected = @installers.keys.select {|name| 1063 | (with.empty? or with.include?(name)) \ 1064 | and not without.include?(name) 1065 | } 1066 | end 1067 | 1068 | def extract_selection(list) 1069 | a = list.split(/,/) 1070 | a.each do |name| 1071 | setup_rb_error "no such package: #{name}" unless @installers.key?(name) 1072 | end 1073 | a 1074 | end 1075 | 1076 | def print_usage(f) 1077 | super 1078 | f.puts 'Inluded packages:' 1079 | f.puts ' ' + @packages.sort.join(' ') 1080 | f.puts 1081 | end 1082 | 1083 | # 1084 | # Task Handlers 1085 | # 1086 | 1087 | def exec_config 1088 | run_hook 'pre-config' 1089 | each_selected_installers {|inst| inst.exec_config } 1090 | run_hook 'post-config' 1091 | @config.save # must be final 1092 | end 1093 | 1094 | def exec_setup 1095 | run_hook 'pre-setup' 1096 | each_selected_installers {|inst| inst.exec_setup } 1097 | run_hook 'post-setup' 1098 | end 1099 | 1100 | def exec_install 1101 | run_hook 'pre-install' 1102 | each_selected_installers {|inst| inst.exec_install } 1103 | run_hook 'post-install' 1104 | end 1105 | 1106 | def exec_test 1107 | run_hook 'pre-test' 1108 | each_selected_installers {|inst| inst.exec_test } 1109 | run_hook 'post-test' 1110 | end 1111 | 1112 | def exec_clean 1113 | rm_f @config.savefile 1114 | run_hook 'pre-clean' 1115 | each_selected_installers {|inst| inst.exec_clean } 1116 | run_hook 'post-clean' 1117 | end 1118 | 1119 | def exec_distclean 1120 | rm_f @config.savefile 1121 | run_hook 'pre-distclean' 1122 | each_selected_installers {|inst| inst.exec_distclean } 1123 | run_hook 'post-distclean' 1124 | end 1125 | 1126 | # 1127 | # lib 1128 | # 1129 | 1130 | def each_selected_installers 1131 | Dir.mkdir 'packages' unless File.dir?('packages') 1132 | @selected.each do |pack| 1133 | $stderr.puts "Processing the package `#{pack}' ..." if verbose? 1134 | Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") 1135 | Dir.chdir "packages/#{pack}" 1136 | yield @installers[pack] 1137 | Dir.chdir '../..' 1138 | end 1139 | end 1140 | 1141 | def run_hook(id) 1142 | @root_installer.run_hook id 1143 | end 1144 | 1145 | # module FileOperations requires this 1146 | def verbose? 1147 | @config.verbose? 1148 | end 1149 | 1150 | # module FileOperations requires this 1151 | def no_harm? 1152 | @config.no_harm? 1153 | end 1154 | 1155 | end # class ToplevelInstallerMulti 1156 | 1157 | 1158 | class Installer 1159 | 1160 | FILETYPES = %w( bin lib ext data conf man ) 1161 | 1162 | include FileOperations 1163 | include HookScriptAPI 1164 | 1165 | def initialize(config, srcroot, objroot) 1166 | @config = config 1167 | @srcdir = File.expand_path(srcroot) 1168 | @objdir = File.expand_path(objroot) 1169 | @currdir = '.' 1170 | end 1171 | 1172 | def inspect 1173 | "#<#{self.class} #{File.basename(@srcdir)}>" 1174 | end 1175 | 1176 | def noop(rel) 1177 | end 1178 | 1179 | # 1180 | # Hook Script API base methods 1181 | # 1182 | 1183 | def srcdir_root 1184 | @srcdir 1185 | end 1186 | 1187 | def objdir_root 1188 | @objdir 1189 | end 1190 | 1191 | def relpath 1192 | @currdir 1193 | end 1194 | 1195 | # 1196 | # Config Access 1197 | # 1198 | 1199 | # module FileOperations requires this 1200 | def verbose? 1201 | @config.verbose? 1202 | end 1203 | 1204 | # module FileOperations requires this 1205 | def no_harm? 1206 | @config.no_harm? 1207 | end 1208 | 1209 | def verbose_off 1210 | begin 1211 | save, @config.verbose = @config.verbose?, false 1212 | yield 1213 | ensure 1214 | @config.verbose = save 1215 | end 1216 | end 1217 | 1218 | # 1219 | # TASK config 1220 | # 1221 | 1222 | def exec_config 1223 | exec_task_traverse 'config' 1224 | end 1225 | 1226 | alias config_dir_bin noop 1227 | alias config_dir_lib noop 1228 | 1229 | def config_dir_ext(rel) 1230 | extconf if extdir?(curr_srcdir()) 1231 | end 1232 | 1233 | alias config_dir_data noop 1234 | alias config_dir_conf noop 1235 | alias config_dir_man noop 1236 | 1237 | def extconf 1238 | ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt 1239 | end 1240 | 1241 | # 1242 | # TASK setup 1243 | # 1244 | 1245 | def exec_setup 1246 | exec_task_traverse 'setup' 1247 | end 1248 | 1249 | def setup_dir_bin(rel) 1250 | files_of(curr_srcdir()).each do |fname| 1251 | update_shebang_line "#{curr_srcdir()}/#{fname}" 1252 | end 1253 | end 1254 | 1255 | alias setup_dir_lib noop 1256 | 1257 | def setup_dir_ext(rel) 1258 | make if extdir?(curr_srcdir()) 1259 | end 1260 | 1261 | alias setup_dir_data noop 1262 | alias setup_dir_conf noop 1263 | alias setup_dir_man noop 1264 | 1265 | def update_shebang_line(path) 1266 | return if no_harm? 1267 | return if config('shebang') == 'never' 1268 | old = Shebang.load(path) 1269 | if old 1270 | $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 1271 | new = new_shebang(old) 1272 | return if new.to_s == old.to_s 1273 | else 1274 | return unless config('shebang') == 'all' 1275 | new = Shebang.new(config('rubypath')) 1276 | end 1277 | $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? 1278 | open_atomic_writer(path) {|output| 1279 | File.open(path, 'rb') {|f| 1280 | f.gets if old # discard 1281 | output.puts new.to_s 1282 | output.print f.read 1283 | } 1284 | } 1285 | end 1286 | 1287 | def new_shebang(old) 1288 | if /\Aruby/ =~ File.basename(old.cmd) 1289 | Shebang.new(config('rubypath'), old.args) 1290 | elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' 1291 | Shebang.new(config('rubypath'), old.args[1..-1]) 1292 | else 1293 | return old unless config('shebang') == 'all' 1294 | Shebang.new(config('rubypath')) 1295 | end 1296 | end 1297 | 1298 | def open_atomic_writer(path, &block) 1299 | tmpfile = File.basename(path) + '.tmp' 1300 | begin 1301 | File.open(tmpfile, 'wb', &block) 1302 | File.rename tmpfile, File.basename(path) 1303 | ensure 1304 | File.unlink tmpfile if File.exist?(tmpfile) 1305 | end 1306 | end 1307 | 1308 | class Shebang 1309 | def Shebang.load(path) 1310 | line = nil 1311 | File.open(path) {|f| 1312 | line = f.gets 1313 | } 1314 | return nil unless /\A#!/ =~ line 1315 | parse(line) 1316 | end 1317 | 1318 | def Shebang.parse(line) 1319 | cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') 1320 | new(cmd, args) 1321 | end 1322 | 1323 | def initialize(cmd, args = []) 1324 | @cmd = cmd 1325 | @args = args 1326 | end 1327 | 1328 | attr_reader :cmd 1329 | attr_reader :args 1330 | 1331 | def to_s 1332 | "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") 1333 | end 1334 | end 1335 | 1336 | # 1337 | # TASK install 1338 | # 1339 | 1340 | def exec_install 1341 | rm_f 'InstalledFiles' 1342 | exec_task_traverse 'install' 1343 | end 1344 | 1345 | def install_dir_bin(rel) 1346 | install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 1347 | end 1348 | 1349 | def install_dir_lib(rel) 1350 | install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 1351 | end 1352 | 1353 | def install_dir_ext(rel) 1354 | return unless extdir?(curr_srcdir()) 1355 | install_files rubyextentions('.'), 1356 | "#{config('sodir')}/#{File.dirname(rel)}", 1357 | 0555 1358 | end 1359 | 1360 | def install_dir_data(rel) 1361 | install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 1362 | end 1363 | 1364 | def install_dir_conf(rel) 1365 | # FIXME: should not remove current config files 1366 | # (rename previous file to .old/.org) 1367 | install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 1368 | end 1369 | 1370 | def install_dir_man(rel) 1371 | install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 1372 | end 1373 | 1374 | def install_files(list, dest, mode) 1375 | mkdir_p dest, @config.install_prefix 1376 | list.each do |fname| 1377 | install fname, dest, mode, @config.install_prefix 1378 | end 1379 | end 1380 | 1381 | def libfiles 1382 | glob_reject(%w(*.y *.output), targetfiles()) 1383 | end 1384 | 1385 | def rubyextentions(dir) 1386 | ents = glob_select("*.#{@config.dllext}", targetfiles()) 1387 | if ents.empty? 1388 | setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" 1389 | end 1390 | ents 1391 | end 1392 | 1393 | def targetfiles 1394 | mapdir(existfiles() - hookfiles()) 1395 | end 1396 | 1397 | def mapdir(ents) 1398 | ents.map {|ent| 1399 | if File.exist?(ent) 1400 | then ent # objdir 1401 | else "#{curr_srcdir()}/#{ent}" # srcdir 1402 | end 1403 | } 1404 | end 1405 | 1406 | # picked up many entries from cvs-1.11.1/src/ignore.c 1407 | JUNK_FILES = %w( 1408 | core RCSLOG tags TAGS .make.state 1409 | .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb 1410 | *~ *.old *.bak *.BAK *.orig *.rej _$* *$ 1411 | 1412 | *.org *.in .* 1413 | ) 1414 | 1415 | def existfiles 1416 | glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) 1417 | end 1418 | 1419 | def hookfiles 1420 | %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| 1421 | %w( config setup install clean ).map {|t| sprintf(fmt, t) } 1422 | }.flatten 1423 | end 1424 | 1425 | def glob_select(pat, ents) 1426 | re = globs2re([pat]) 1427 | ents.select {|ent| re =~ ent } 1428 | end 1429 | 1430 | def glob_reject(pats, ents) 1431 | re = globs2re(pats) 1432 | ents.reject {|ent| re =~ ent } 1433 | end 1434 | 1435 | GLOB2REGEX = { 1436 | '.' => '\.', 1437 | '$' => '\$', 1438 | '#' => '\#', 1439 | '*' => '.*' 1440 | } 1441 | 1442 | def globs2re(pats) 1443 | /\A(?:#{ 1444 | pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') 1445 | })\z/ 1446 | end 1447 | 1448 | # 1449 | # TASK test 1450 | # 1451 | 1452 | TESTDIR = 'test' 1453 | 1454 | def exec_test 1455 | unless File.directory?('test') 1456 | $stderr.puts 'no test in this package' if verbose? 1457 | return 1458 | end 1459 | $stderr.puts 'Running tests...' if verbose? 1460 | begin 1461 | require 'test/unit' 1462 | rescue LoadError 1463 | setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' 1464 | end 1465 | runner = Test::Unit::AutoRunner.new(true) 1466 | runner.to_run << TESTDIR 1467 | runner.run 1468 | end 1469 | 1470 | # 1471 | # TASK clean 1472 | # 1473 | 1474 | def exec_clean 1475 | exec_task_traverse 'clean' 1476 | rm_f @config.savefile 1477 | rm_f 'InstalledFiles' 1478 | end 1479 | 1480 | alias clean_dir_bin noop 1481 | alias clean_dir_lib noop 1482 | alias clean_dir_data noop 1483 | alias clean_dir_conf noop 1484 | alias clean_dir_man noop 1485 | 1486 | def clean_dir_ext(rel) 1487 | return unless extdir?(curr_srcdir()) 1488 | make 'clean' if File.file?('Makefile') 1489 | end 1490 | 1491 | # 1492 | # TASK distclean 1493 | # 1494 | 1495 | def exec_distclean 1496 | exec_task_traverse 'distclean' 1497 | rm_f @config.savefile 1498 | rm_f 'InstalledFiles' 1499 | end 1500 | 1501 | alias distclean_dir_bin noop 1502 | alias distclean_dir_lib noop 1503 | 1504 | def distclean_dir_ext(rel) 1505 | return unless extdir?(curr_srcdir()) 1506 | make 'distclean' if File.file?('Makefile') 1507 | end 1508 | 1509 | alias distclean_dir_data noop 1510 | alias distclean_dir_conf noop 1511 | alias distclean_dir_man noop 1512 | 1513 | # 1514 | # Traversing 1515 | # 1516 | 1517 | def exec_task_traverse(task) 1518 | run_hook "pre-#{task}" 1519 | FILETYPES.each do |type| 1520 | if type == 'ext' and config('without-ext') == 'yes' 1521 | $stderr.puts 'skipping ext/* by user option' if verbose? 1522 | next 1523 | end 1524 | traverse task, type, "#{task}_dir_#{type}" 1525 | end 1526 | run_hook "post-#{task}" 1527 | end 1528 | 1529 | def traverse(task, rel, mid) 1530 | dive_into(rel) { 1531 | run_hook "pre-#{task}" 1532 | __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') 1533 | directories_of(curr_srcdir()).each do |d| 1534 | traverse task, "#{rel}/#{d}", mid 1535 | end 1536 | run_hook "post-#{task}" 1537 | } 1538 | end 1539 | 1540 | def dive_into(rel) 1541 | return unless File.dir?("#{@srcdir}/#{rel}") 1542 | 1543 | dir = File.basename(rel) 1544 | Dir.mkdir dir unless File.dir?(dir) 1545 | prevdir = Dir.pwd 1546 | Dir.chdir dir 1547 | $stderr.puts '---> ' + rel if verbose? 1548 | @currdir = rel 1549 | yield 1550 | Dir.chdir prevdir 1551 | $stderr.puts '<--- ' + rel if verbose? 1552 | @currdir = File.dirname(rel) 1553 | end 1554 | 1555 | def run_hook(id) 1556 | path = [ "#{curr_srcdir()}/#{id}", 1557 | "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } 1558 | return unless path 1559 | begin 1560 | instance_eval File.read(path), path, 1 1561 | rescue 1562 | raise if $DEBUG 1563 | setup_rb_error "hook #{path} failed:\n" + $!.message 1564 | end 1565 | end 1566 | 1567 | end # class Installer 1568 | 1569 | 1570 | class SetupError < StandardError; end 1571 | 1572 | def setup_rb_error(msg) 1573 | raise SetupError, msg 1574 | end 1575 | 1576 | if $0 == __FILE__ 1577 | begin 1578 | ToplevelInstaller.invoke 1579 | rescue SetupError 1580 | raise if $DEBUG 1581 | $stderr.puts $!.message 1582 | $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." 1583 | exit 1 1584 | end 1585 | end 1586 | --------------------------------------------------------------------------------