summaryrefslogtreecommitdiffstats
path: root/lib/facter/util/resolution.rb
blob: d82fab2eb16d864af8f6315e1a5e47b9a7389053 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# 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 'timeout'
require 'rbconfig'

class Facter::Util::Resolution
    attr_accessor :interpreter, :code, :name, :timeout

    WINDOWS = Config::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i

    INTERPRETER = WINDOWS ? 'cmd.exe' : '/bin/sh'

    def self.have_which
        if ! defined?(@have_which) or @have_which.nil?
            if Facter.value(:kernel) == 'windows'
                @have_which = false
            else
                %x{which which >/dev/null 2>&1}
                @have_which = ($? == 0)
            end
        end
        @have_which
    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 = INTERPRETER)
        raise ArgumentError, "invalid interpreter" unless interpreter == INTERPRETER

        # Try to guess whether the specified code can be executed by looking at the
        # first word. If it cannot be found on the PATH defer on resolving the fact
        # by returning nil.
        # This only fails on shell built-ins, most of which are masked by stuff in 
        # /bin or of dubious value anyways. In the worst case, "sh -c 'builtin'" can
        # be used to work around this limitation
        #
        # Windows' %x{} throws Errno::ENOENT when the command is not found, so we 
        # can skip the check there. This is good, since builtins cannot be found 
        # elsewhere.
        if have_which and !WINDOWS
            path = nil
            binary = code.split.first
            if code =~ /^\//
                path = binary
            else
                path = %x{which #{binary} 2>/dev/null}.chomp
                # we don't have the binary necessary
                return nil if path == "" or path.match(/Command not found\./)
            end

            return nil unless FileTest.exists?(path)
        end

        out = nil

        begin
            out = %x{#{code}}.chomp
        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

    # 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)
        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

    # 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
        result = nil
        return result if @code == nil and @interpreter == 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,@interpreter)
                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