# An actual fact resolution mechanism. These are largely just chunks of
# code, with optional confinements restricting the mechanisms to only working on
# specific systems. Note that the confinements are always ANDed, so any
# confinements specified must all be true for the resolution to be
# suitable.
require 'facter/util/confine'
require 'facter/util/config'
require 'timeout'
class Facter::Util::Resolution
attr_accessor :interpreter, :code, :name, :timeout
attr_writer :value, :weight
INTERPRETER = Facter::Util::Config.is_windows? ? "cmd.exe" : "/bin/sh"
# Returns the locations to be searched when looking for a binary. This
# is currently determined by the +PATH+ environment variable plus
# /sbin and /usr/sbin when run on unix
def self.search_paths
if Facter::Util::Config.is_windows?
ENV['PATH'].split(File::PATH_SEPARATOR)
else
# Make sure facter is usable even for non-root users. Most commands
# in /sbin (like ifconfig) can be run as non priviledged users as
# long as they do not modify anything - which we do not do with facter
ENV['PATH'].split(File::PATH_SEPARATOR) + [ '/sbin', '/usr/sbin' ]
end
end
# Determine the full path to a binary. If the supplied filename does not
# already describe an absolute path then different locations (determined
# by self.search_paths) will be searched for a match.
#
# Returns nil if no matching executable can be found otherwise returns
# the expanded pathname.
def self.which(bin)
if absolute_path?(bin)
return bin if File.executable?(bin)
if Facter::Util::Config.is_windows? and File.extname(bin).empty?
exts = ENV['PATHEXT']
exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD]
exts.each do |ext|
destext = bin + ext
if File.executable?(destext)
Facter.warnonce("Using Facter::Util::Resolution.which with an absolute path like #{bin} but no fileextension is deprecated. Please add the correct extension (#{ext})")
return destext
end
end
end
else
search_paths.each do |dir|
dest = File.join(dir, bin)
if Facter::Util::Config.is_windows?
dest.gsub!(File::SEPARATOR, File::ALT_SEPARATOR)
if File.extname(dest).empty?
exts = ENV['PATHEXT']
exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD]
exts.each do |ext|
destext = dest + ext
return destext if File.executable?(destext)
end
end
end
return dest if File.executable?(dest)
end
end
nil
end
# Determine in a platform-specific way whether a path is absolute. This
# defaults to the local platform if none is specified.
def self.absolute_path?(path, platform=nil)
# Escape once for the string literal, and once for the regex.
slash = '[\\\\/]'
name = '[^\\\\/]+'
regexes = {
:windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i,
:posix => %r!^/!,
}
platform ||= Facter::Util::Config.is_windows? ? :windows : :posix
!! (path =~ regexes[platform])
end
# Expand the executable of a commandline to an absolute path. The executable
# is the first word of the commandline. If the executable contains spaces,
# it has be but in double quotes to be properly recognized.
#
# Returns the commandline with the expanded binary or nil if the binary
# can't be found. If the path to the binary contains quotes, the whole binary
# is put in quotes.
def self.expand_command(command)
if match = /^"(.+?)"(?:\s+(.*))?/.match(command)
exe, arguments = match.captures
exe = which(exe) and [ "\"#{exe}\"", arguments ].compact.join(" ")
elsif match = /^'(.+?)'(?:\s+(.*))?/.match(command) and not Facter::Util::Config.is_windows?
exe, arguments = match.captures
exe = which(exe) and [ "'#{exe}'", arguments ].compact.join(" ")
else
exe, arguments = command.split(/ /,2)
if exe = which(exe)
# the binary was not quoted which means it contains no spaces. But the
# full path to the binary may do so.
exe = "\"#{exe}\"" if exe =~ /\s/ and Facter::Util::Config.is_windows?
exe = "'#{exe}'" if exe =~ /\s/ and not Facter::Util::Config.is_windows?
[ exe, arguments ].compact.join(" ")
end
end
end
#
# Call this method with a block of code for which you would like to temporarily modify
# one or more environment variables; the specified values will be set for the duration
# of your block, after which the original values (if any) will be restored.
#
# [values] a Hash containing the key/value pairs of any environment variables that you
# would like to temporarily override
def self.with_env(values)
old = {}
values.each do |var, value|
# save the old value if it exists
if old_val = ENV[var]
old[var] = old_val
end
# set the new (temporary) value for the environment variable
ENV[var] = value
end
# execute the caller's block, capture the return value
rv = yield
# use an ensure block to make absolutely sure we restore the variables
ensure
# restore the old values
values.each do |var, value|
if old.include?(var)
ENV[var] = old[var]
else
# if there was no old value, delete the key from the current environment variables hash
ENV.delete(var)
end
end
# return the captured return value
rv
end
# Execute a program and return the output of that program.
#
# Returns nil if the program can't be found, or if there is a problem
# executing the code.
#
def self.exec(code, interpreter = nil)
Facter.warnonce "The interpreter parameter to 'exec' is deprecated and will be removed in a future version." if interpreter
## Set LANG to force i18n to C for the duration of this exec; this ensures that any code that parses the
## output of the command can expect it to be in a consistent / predictable format / locale
with_env "LANG" => "C" do
if expanded_code = expand_command(code)
# if we can find the binary, we'll run the command with the expanded path to the binary
code = expanded_code
else
# if we cannot find the binary return nil on posix. On windows we'll still try to run the
# command in case it is a shell-builtin. In case it is not, windows will raise Errno::ENOENT
return nil unless Facter::Util::Config.is_windows?
return nil if absolute_path?(code)
end
out = nil
begin
out = %x{#{code}}.chomp
Facter.warnonce 'Using Facter::Util::Resolution.exec with a shell built-in is deprecated. Most built-ins can be replaced with native ruby commands. If you really have to run a built-in, pass "cmd /c your_builtin" as a command' unless expanded_code
rescue Errno::ENOENT => detail
# command not found on Windows
return nil
rescue => detail
$stderr.puts detail
return nil
end
if out == ""
return nil
else
return out
end
end
end
# Add a new confine to the resolution mechanism.
def confine(confines)
confines.each do |fact, values|
@confines.push Facter::Util::Confine.new(fact, *values)
end
end
def has_weight(weight)
@weight = weight
end
# Create a new resolution mechanism.
def initialize(name)
@name = name
@confines = []
@value = nil
@timeout = 0
@weight = nil
end
# Return the importance of this resolution.
def weight
if @weight
@weight
else
@confines.length
end
end
# We need this as a getter for 'timeout', because some versions
# of ruby seem to already have a 'timeout' method and we can't
# seem to override the instance methods, somehow.
def limit
@timeout
end
# Set our code for returning a value.
def setcode(string = nil, interp = nil, &block)
Facter.warnonce "The interpreter parameter to 'setcode' is deprecated and will be removed in a future version." if interp
if string
@code = string
@interpreter = interp || INTERPRETER
else
unless block_given?
raise ArgumentError, "You must pass either code or a block"
end
@code = block
end
end
##
# on_flush accepts a block and executes the block when the resolution's value
# is flushed. This makes it possible to model a single, expensive system
# call inside of a Ruby object and then define multiple dynamic facts which
# resolve by sending messages to the model instance. If one of the dynamic
# facts is flushed then it can, in turn, flush the data stored in the model
# instance to keep all of the dynamic facts in sync without making multiple,
# expensive, system calls.
#
# Please see the Solaris zones fact for an example of how this feature may be
# used.
#
# @see Facter::Util::Fact#flush
# @see Facter::Util::Resolution#flush
#
# @api public
def on_flush(&block)
@on_flush_block = block
end
##
# flush executes the block, if any, stored by the {on_flush} method
#
# @see Facter::Util::Fact#flush
# @see Facter::Util::Resolution#on_flush
#
# @api private
def flush
@on_flush_block.call if @on_flush_block
end
def interpreter
Facter.warnonce "The 'Facter::Util::Resolution.interpreter' method is deprecated and will be removed in a future version."
@interpreter
end
def interpreter=(interp)
Facter.warnonce "The 'Facter::Util::Resolution.interpreter=' method is deprecated and will be removed in a future version."
@interpreter = interp
end
# Is this resolution mechanism suitable on the system in question?
def suitable?
unless defined? @suitable
@suitable = ! @confines.detect { |confine| ! confine.true? }
end
return @suitable
end
def to_s
return self.value()
end
# How we get a value for our resolution mechanism.
def value
return @value if @value
result = nil
return result if @code == nil
starttime = Time.now.to_f
begin
Timeout.timeout(limit) do
if @code.is_a?(Proc)
result = @code.call()
else
result = Facter::Util::Resolution.exec(@code)
end
end
rescue Timeout::Error => detail
warn "Timed out seeking value for %s" % self.name
# This call avoids zombies -- basically, create a thread that will
# dezombify all of the child processes that we're ignoring because
# of the timeout.
Thread.new { Process.waitall }
return nil
rescue => details
warn "Could not retrieve %s: %s" % [self.name, details]
return nil
end
finishtime = Time.now.to_f
ms = (finishtime - starttime) * 1000
Facter.show_time "#{self.name}: #{"%.2f" % ms}ms"
return nil if result == ""
return result
end
end