# # = uuid.rb - UUID generator # # Author:: Assaf Arkin assaf@labnotes.org # Eric Hodel drbrain@segment7.net # Documentation:: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/Ruby/UuidGenerator # Copyright:: Copyright (c) 2005-2008 Assaf Arkin, Eric Hodel # License:: MIT and/or Creative Commons Attribution-ShareAlike require 'fileutils' require 'thread' require 'tmpdir' require 'rubygems' require 'macaddr' ## # = Generating UUIDs # # Call #generate to generate a new UUID. The method returns a string in one of # three formats. The default format is 36 characters long, and contains the 32 # hexadecimal octets and hyphens separating the various value parts. The # :compact format omits the hyphens, while the :urn format # adds the :urn:uuid prefix. # # For example: # # uuid = UUID.new # # 10.times do # p uuid.generate # end # # = UUIDs in Brief # # UUID (universally unique identifier) are guaranteed to be unique across time # and space. # # A UUID is 128 bit long, and consists of a 60-bit time value, a 16-bit # sequence number and a 48-bit node identifier. # # The time value is taken from the system clock, and is monotonically # incrementing. However, since it is possible to set the system clock # backward, a sequence number is added. The sequence number is incremented # each time the UUID generator is started. The combination guarantees that # identifiers created on the same machine are unique with a high degree of # probability. # # Note that due to the structure of the UUID and the use of sequence number, # there is no guarantee that UUID values themselves are monotonically # incrementing. The UUID value cannot itself be used to sort based on order # of creation. # # To guarantee that UUIDs are unique across all machines in the network, # the IEEE 802 MAC address of the machine's network interface card is used as # the node identifier. # # For more information see {RFC 4122}[http://www.ietf.org/rfc/rfc4122.txt]. class UUID VERSION = '2.0.1' ## # Clock multiplier. Converts Time (resolution: seconds) to UUID clock # (resolution: 10ns) CLOCK_MULTIPLIER = 10000000 ## # Clock gap is the number of ticks (resolution: 10ns) between two Ruby Time # ticks. CLOCK_GAPS = 100000 ## # Version number stamped into the UUID to identify it as time-based. VERSION_CLOCK = 0x0100 ## # Formats supported by the UUID generator. # # :default:: Produces 36 characters, including hyphens separating # the UUID value parts # :compact:: Produces a 32 digits (hexadecimal) value with no # hyphens # :urn:: Adds the prefix urn:uuid: to the default format FORMATS = { :compact => '%08x%04x%04x%04x%012x', :default => '%08x-%04x-%04x-%04x-%012x', :urn => 'urn:uuid:%08x-%04x-%04x-%04x-%012x', } ## # MAC address (48 bits), sequence number and last clock STATE_FILE_FORMAT = 'SLLQ' @state_file = nil @mode = nil @uuid = nil ## # The access mode of the state file. Set it with state_file. def self.mode @mode end ## # Generates a new UUID string using +format+. See FORMATS for a list of # supported formats. def self.generate(format = :default) @uuid ||= new @uuid.generate format end ## # Creates an empty state file in /var/tmp/ruby-uuid or the windows common # application data directory using mode 0644. Call with a different mode # before creating a UUID generator if you want to open access beyond your # user by default. # # If the default state dir is not writable, UUID falls back to ~/.ruby-uuid. # # State files are not portable across machines. def self.state_file(mode = 0644) return @state_file if @state_file @mode = mode begin require 'Win32API' csidl_common_appdata = 0x0023 path = 0.chr * 260 get_folder_path = Win32API.new('shell32', 'SHGetFolderPath', 'LLLLP', 'L') get_folder_path.call 0, csidl_common_appdata, 0, 1, path state_dir = File.join(path.strip) rescue LoadError state_dir = File.join('', 'var', 'tmp') end if File.writable?(state_dir) then @state_file = File.join(state_dir, 'ruby-uuid') else @state_file = File.expand_path(File.join('~', '.ruby-uuid')) end @state_file end ## # Create a new UUID generator. You really only need to do this once. def initialize @drift = 0 @last_clock = (Time.now.to_f * CLOCK_MULTIPLIER).to_i @mutex = Mutex.new if File.exist?(self.class.state_file) then next_sequence else @mac = Mac.addr.gsub(/:|-/, '').hex & 0x7FFFFFFFFFFF fail "Cannot determine MAC address from any available interface, tried with #{Mac.addr}" if @mac == 0 @sequence = rand 0x10000 open_lock 'w' do |io| write_state io end end end ## # Generates a new UUID string using +format+. See FORMATS for a list of # supported formats. def generate(format = :default) template = FORMATS[format] raise ArgumentError, "invalid UUID format #{format.inspect}" unless template # The clock must be monotonically increasing. The clock resolution is at # best 100 ns (UUID spec), but practically may be lower (on my setup, # around 1ms). If this method is called too fast, we don't have a # monotonically increasing clock, so the solution is to just wait. # # It is possible for the clock to be adjusted backwards, in which case we # would end up blocking for a long time. When backward clock is detected, # we prevent duplicates by asking for a new sequence number and continue # with the new clock. clock = @mutex.synchronize do clock = (Time.new.to_f * CLOCK_MULTIPLIER).to_i & 0xFFFFFFFFFFFFFFF0 if clock > @last_clock then @drift = 0 @last_clock = clock elsif clock == @last_clock then drift = @drift += 1 if drift < 10000 then @last_clock += 1 else Thread.pass nil end else next_sequence @last_clock = clock end end until clock template % [ clock & 0xFFFFFFFF, (clock >> 32) & 0xFFFF, ((clock >> 48) & 0xFFFF | VERSION_CLOCK), @sequence & 0xFFFF, @mac & 0xFFFFFFFFFFFF ] end ## # Updates the state file with a new sequence number. def next_sequence open_lock 'r+' do |io| @mac, @sequence, @last_clock = read_state(io) io.rewind io.truncate 0 @sequence += 1 write_state io end rescue Errno::ENOENT open_lock 'w' do |io| write_state io end ensure @last_clock = (Time.now.to_f * CLOCK_MULTIPLIER).to_i @drift = 0 end def inspect mac = ("%012x" % @mac).scan(/[0-9a-f]{2}/).join(':') "MAC: #{mac} Sequence: #{@sequence}" end protected ## # Open the state file with an exclusive lock and access mode +mode+. def open_lock(mode) File.open self.class.state_file, mode, self.class.mode do |io| begin io.flock File::LOCK_EX yield io ensure io.flock File::LOCK_UN end end end ## # Read the state from +io+ def read_state(io) mac1, mac2, seq, last_clock = io.read(32).unpack(STATE_FILE_FORMAT) mac = (mac1 << 32) + mac2 return mac, seq, last_clock end ## # Write that state to +io+ def write_state(io) mac2 = @mac & 0xffffffff mac1 = (@mac >> 32) & 0xffff io.write [mac1, mac2, @sequence, @last_clock].pack(STATE_FILE_FORMAT) end end