lib/roma/tools/mkconfig.rb

Summary

Maintainability
D
2 days
Test Coverage
require 'pathname'
require 'yaml'
require 'fileutils'

module Roma

  class Mkconfig
    TREE_TOP = "menu"
    LIB_PATH = Pathname(__FILE__).dirname.parent.parent
    CONFIG_TEMPLATE_PATH = File.expand_path(File.join(LIB_PATH, "roma/config.rb"))
    CONFIG_OUT_PATH = File.expand_path(File.join(Pathname.pwd, "config.rb"))
    PLUGIN_DIR = File.expand_path(File.join(LIB_PATH, File.join("roma", "plugin")))
    BNUM_COEFFICIENT = 2 #reccomend1-4.
    TC_FILE = 10
    REDUNDANCY = 2
    DEFAULT_ROMA_CONNECTION = 12
    RUBY_CONNECTION = 10
    JAVA_CONNECTION = 30
    PHP_CONNECTION = 256
    KB = 1024
    GB = 1024 * 1024 * 1024
    OS_MEMORY_SIZE = 1 * GB
    END_MSG = ["exit", "quit", "balse"]

    class Base
      attr_accessor :all, :flag

      def initialize
        flag = false
        @all = YAML.load <<-YAML
        menu:
          name:
          path_name: menu
          message: Please select by number.
          choice:
            - Select storage
            - Select plugin
            - Calculate File Descriptor
            - Save
          next:
            - storage
            - plugin
            - language
            - save

        storage:
          name: selected_storage
          path_name: storage
          message: Which storage will you use?
          choice:
            - Ruby Hash
            - Tokyo Cabinet
            - Groonga
          default: 1
          next:
            - menu
            - memory
            - memory
        memory:
          name: memory_size_GB
          path_name:
          float_flg: on
          message: How big memory size in 1 server? Please measure in GB.
          default: 0.6
          next: process
        process:
          name: process_num
          path_name:
          message: How many run ROMA process per machine?
          default: 2
          next: server
        server:
          name: server_num
          path_name:
          message: How many machine run as ROMA server?
          default: 1
          next: data
        data:
          name: data_num
          path_name:
          message: How many data will you store?
          default: 10000
          next: menu

        plugin:
          name: selected_plugin
          path_name: plugin
          message: Please select which plugin will you use.(plugin_storage.rb is essential unless you make alternative plugin.)
          choice:
            #{
              list = load_path(PLUGIN_DIR) << "Select all plugins"
              list.delete("plugin_storage.rb")
              list.unshift("plugin_storage.rb")
              list
            }
          default: 1
          next:
            #{
              r = Array.new
              load_path(PLUGIN_DIR).count.times{ r << "continue" }
              r << "menu"
              r
            }
          store_type: Array
        continue:
          name:
          path_name: 
          message: Will you use other plugin?
          choice:
            - Select more
            - No more
          default: 2
          next:
            - plugin
            - check_plugin
        check_plugin:
          name:
          path_name: 
          message: ROMA requires plugin_storage.rb or substitute plugin.Will you continue without plugin_storage.rb?
          choice:
            - Add plugin_storage.rb
            - Not necessary
          default: 2
          next:
            - add_plugin
            - menu

        language:
          name: client_language
          path_name:
          message: Please select programming language of client by number.
          choice:
            - Ruby
            - Java
            - PHP
          default: 3
          next:
            - fd_server
            - fd_server
            - fd_server
        fd_server:
          name: server_num
          path_name: FileDescriptor
          message: How many machine run as ROMA server?
          default: 1
          next: fd_client
        fd_client:
          name: client_num 
          path_name:
          message: How many machine run as ROMA client?
          default: 1
          next: menu

        save: END

        YAML
      end

      def keys
        all.keys
      end

      def [](s)
        all[s]
      end

      def load_path(path)
        ret = Array.new
        files = Dir::entries(path)
        files.delete("plugin_stub.rb") if files.include?("plugin_stub.rb")

        files.each do |file|
          ret << file if File::ftype(File.join(path, file)) == "file"
        end

        ret
      end

      def print_question(key)
        target = all[key]
        #print question
        print "#{target["message"]}\n"
        if target.key?("choice")
          target["choice"].each_with_index do |k, i|
            print "[#{i + 1}] #{k}\n"
          end
        end
      end

      def next (key, input)
        target = all[key]["next"]

        if target.class == Array
          return target[input.to_i - 1]
        else
          return target
        end
      end
    end

    class Input
      module InputReadline
        def get_line
          return Readline.readline("> ", false)
        end
      end

      module InputSimple
        def get_line
          print ">"
          return gets.chomp!
        end
      end

      def initialize
        begin
          require "readline"
        rescue LoadError
          self.extend InputSimple
          return
        end
        self.extend InputReadline
      end
    end

    class Config_status
      attr_accessor :name, :value, :print, :default_value
      def initialize(hash, input, store_type = nil)
        @name = hash["name"]
        @value = input
        if store_type == "Array"
          @value = Array.new
          @value << input
        end
        @print = true
      end
    end

    class Box
      def self.print_edge(width)
        print "+"
          width.times { print "-" }
        print "+\n"
      end

      def self.print_with_box(arg)
        return if arg.count == 0

        if arg.class == Hash
          strs = Array.new
          arg.each do |k, v|
             strs << "#{k}: #{arg[k]}"
          end
          arg = strs
        end

        width = max_length(arg) + 1
        print_edge(width)

        arg.each do |s|
          print "|#{s}"
          (width - s.length).times do
            print " "
          end
          print "|\n"
        end

        print_edge(width)
      end

      private

      def self.max_length(arg)
        max = 0
        arg.each do |s|
          max = s.length if s.length > max
        end
        max
      end
    end

    class Calculate
      def self.get_bnum(res)
        res["server"] = res["fd_server"] if !res["server"]
        ans = res["data"].value.to_i * BNUM_COEFFICIENT * REDUNDANCY / res["server"].value.to_i / TC_FILE
        return ans
      end

      def self.get_xmsize_max(res)
        ans = (res["memory"].value.to_f * GB - OS_MEMORY_SIZE) / res["process"].value.to_i / TC_FILE
        if ans <= 0
          ans = res["memory"].value.to_f * GB / 2 / res["process"].value.to_i / TC_FILE
        end
        ans = ans.to_i
        return ans
      end

      def self.get_fd(res)
        res["fd_server"] = res["server"] if !res["fd_server"]
        res["fd_server"].value.to_i * connection_num(res) + (res["fd_client"].value.to_i- 1) * DEFAULT_ROMA_CONNECTION * 2
      end

      def self.connection_num(res)
        case res["language"].value
          when "Ruby"
            connection = RUBY_CONNECTION
          when "Java"
            connection = JAVA_CONNECTION
          when "PHP"
            connection = PHP_CONNECTION
        end

        return connection
      end
    end

    def initialize(mode = :no_menu)
      # confirming overwrite
      if File.exist?(CONFIG_OUT_PATH)
        print("Config.rb already exist in current directory. \nWill you overwrite?[y/n]")
        if gets.chomp! != "y"
          p "config.rb  were not created!"
          exit
        end
      end

      @base = Base.new
      @results = Hash::new
      @next_hash = TREE_TOP

      begin
        @defaults = load_config([:STORAGE_CLASS, :STORAGE_OPTION, :PLUGIN_FILES])
      rescue LoadError
        puts 'Not found config.rb file.'
        return
      rescue
        p $!
        puts "Content of config.rb is wrong."
        return
      end
      mkconfig(mode)
    end

    def load_config(targets)
      require CONFIG_TEMPLATE_PATH
      d_value = Hash.new
      Config.constants.each do |cnst|
        if targets.include?(cnst)
          d_value[cnst] = Config.const_get(cnst)
        end
      end
      return d_value
    end

    def mkconfig(mode)
      skip = skip_menu!(mode)

      while true
        clear_screen

        if @next_hash == "add_plugin"
          @results["plugin"].value.unshift("plugin_storage.rb") unless @results["plugin"].value.include?("plugin_storage.rb")
          @next_hash = "menu"
        end

        skip.call if @next_hash == "menu" || @next_hash == "server" || @next_hash == "fd_server" || @next_hash == "check_plugin"
        break if end?(@base[@next_hash])
        puts "if you doesn't input anything, default value is set."
        Box.print_with_box(@defaults)
        print_status(@results)
        @base.print_question(@next_hash)
        input = get_input(@base[@next_hash])

        # if specific words(balse, exit, quit) was inputed, mkconfig.rb was finished.
        if END_MSG.include?(input)
          p "config.rb  were not created!"
          break

        else
          @results = store_result(@results, @base, @next_hash, input)
          @next_hash = @base.next(@next_hash, input)
        end
      end
    end

    def clear_screen
      print "\e[H\e[2J"
    end

    def skip_menu!(menu)
      # in case of "-m" or "--with_menu" option was used
      if menu == :with_menu
        return Proc.new do
          if @next_hash == "server" && @results["fd_server"]
            @next_hash = @base["server"]["next"]
          elsif @next_hash == "fd_server" && @results["server"]
            @next_hash = "fd_client"
          elsif @next_hash == "check_plugin" && @results["plugin"].value.include?("plugin_storage.rb")
            @next_hash = "menu"
          end
        end
      end

      # in case of "-m" or "--with_menu" option was NOT used
      i = 0
      return Proc.new do
        if @next_hash == "menu"
          @next_hash = @base["menu"]["next"][i]
          i += 1
        elsif @next_hash == "server" && @results["fd_server"]
          @next_hash = @base["server"]["next"]
        elsif @next_hash == "fd_server" && @results["server"]
          @next_hash = "fd_client"
        elsif @next_hash == "check_plugin" && @results["plugin"].value.include?("plugin_storage.rb")
          @next_hash = "language"
          i += 1
        end
      end
    end

    #judge whether data inputting finish or not
    def end?(s)
      if s == "END"
        save_data(@results)
        true
      end
    end

    def print_status(results)
      strs = Array.new
      results.each_value do |v|
        strs << "#{v.name} : #{v.value}"
      end
      Box.print_with_box(strs)
    end

    def get_input(hash)
      receiver = Input.new
      input = ""

      while !correct_in?(hash,input)
        input = receiver.get_line
        if input == ""
          #set defaults value
          input = hash["default"]
        end
      end

      input
    end

    def correct_in?(hash,input)
      if END_MSG.include?(input)
        return true
      end

      if hash["next"] == "continue"
        if (input == "y" || input == "n")
          return true
        end
      else
        if hash.key?('choice')
          if hash['choice'].count >= input.to_i && input.to_i > 0
            return true
          end
        else
          if hash["float_flg"]
            if 0 < input.to_f
              return true
            end
          else  
            if 0 < input.to_i
              return true
            end
          end
        end
      end

      return false
    end

    def store_result(results, base, hash, input)
      target = base[hash]

      return results if !target["name"]

      if target.key?("choice")
        if target["store_type"] == "Array"
          if base.flag
            results[hash].value << target["choice"][input.to_i - 1] if !results[hash].value.include?(target["choice"][input.to_i - 1])
          else
            results[hash] = Config_status.new(target, target["choice"][input.to_i - 1], target["store_type"])
            base.flag = true
          end

          if input.to_i == target["choice"].count
            results[hash].value = target["choice"][0..-2]
          end
        else
          results[hash] = Config_status.new(target, target["choice"][input.to_i - 1])
        end
      else
        results[hash] = Config_status.new(target, input)
      end

      base.flag = false if hash == "menu"
      return results
    end

    #make config.rb based on input data
    def save_data(res)
      if res.key?("storage")
        case res["storage"].value
        when "Ruby Hash"
          req = "rh_storage"
          storage = "RubyHashStorage"
        when "Tokyo Cabinet"
          req = "tc_storage"
          storage = "TCStorage"
          bnum = Calculate.get_bnum(res)
          bnum = 5000000 if bnum < 5000000
          xmsiz = Calculate.get_xmsize_max(res)
        when "Groonga"
          req = "groonga_storage"
          storage = "GroongaStorage"
          bnum = Calculate.get_bnum(res)
          bnum = 5000000 if bnum < 5000000
          xmsiz = Calculate.get_xmsize_max(res)
        end
      end

      if res.key?("language")
        fd = Calculate.get_fd(res)
        print "\r\nPlease set FileDescriptor bigger than #{fd}.\r\n\r\n" 
      end

      body = ""
      open(CONFIG_TEMPLATE_PATH, "r") do |f|
        body = f.read
      end

      if req
        body = ch_assign(body, "require", " ", "roma\/storage\/#{req}")
        body = ch_assign(body, "STORAGE_CLASS", "Roma::Storage::#{storage}")

        case req
        when "rh_storage"
          body = ch_assign(body, "STORAGE_OPTION","")
        when /^(tc_storage|groonga_storage)$/
          body = ch_assign(body, "STORAGE_OPTION", "bnum=#{bnum}\#xmsiz=#{xmsiz}\#opts=d#dfunit=10")
        end
      end

      if res.key?("plugin")
        body = ch_assign(body, "PLUGIN_FILES", res["plugin"].value)
      end

      open(CONFIG_OUT_PATH, "w") do |f|
        f.flock(File::LOCK_EX)
        f.puts body
        f.truncate(f.tell)
        f.flock(File::LOCK_UN)
      end

      puts "Before"
      Box.print_with_box(@defaults)

      re_require(CONFIG_OUT_PATH, Config)
      results = load_config([:STORAGE_CLASS, :STORAGE_OPTION, :PLUGIN_FILES])
      print "\r\nAfter\r\n"
      Box.print_with_box(results)
      print "\r\nMkconfig is finish.\r\n"
      print "\r\nIf you need, change directory path about LOG, RTTABLE, STORAGE, WB and other setting.\r\n\r\n"
    end

    # sep means separating right and left part(config.rb style)
    def ch_assign(text, exp, sep = " = ", str)
      sep = " = " if sep == "="
      text = text.gsub(/(\s*#{exp}).*/) do |s|
        name = $1
        if str.class == String
          if str =~ /::/ || str =~ /^\d+$/
            # storage type
           name + sep + str
          else
            # require & storage option
           name + sep + str.inspect
          end
        else
          # plugin
          # "to_s" equal "inspect" in Ruby 1.9
          name + sep + str.to_s.sub("\\", "")
        end
      end
    end

    def re_require(path, c_obj)
      $".delete(File.expand_path(path))
      c_obj.constants.each do |cnst|
        c_obj.class_eval { remove_const cnst }
      end
      require path
    end

  end # Mkconfig
end # module Roma