diff --git a/ext/windows/service/daemon.rb b/ext/windows/service/daemon.rb index 84e4a283d..2a4ec72ef 100755 --- a/ext/windows/service/daemon.rb +++ b/ext/windows/service/daemon.rb @@ -1,169 +1,170 @@ #!/usr/bin/env ruby require 'fileutils' require 'win32/daemon' require 'win32/dir' require 'win32/process' require 'win32/eventlog' require 'windows/synchronize' require 'windows/handle' class WindowsDaemon < Win32::Daemon include Windows::Synchronize include Windows::Handle - include Windows::Process + + CREATE_NEW_CONSOLE = 0x00000010 @LOG_TO_FILE = false LOG_FILE = File.expand_path(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'puppet', 'var', 'log', 'windows.log')) LEVELS = [:debug, :info, :notice, :err] LEVELS.each do |level| define_method("log_#{level}") do |msg| log(msg, level) end end def service_init end def service_main(*argsv) argsv = (argsv << ARGV).flatten.compact args = argsv.join(' ') @loglevel = LEVELS.index(argsv.index('--debug') ? :debug : :notice) @LOG_TO_FILE = (argsv.index('--logtofile') ? true : false) if (@LOG_TO_FILE) FileUtils.mkdir_p(File.dirname(LOG_FILE)) args = args.gsub("--logtofile","") end basedir = File.expand_path(File.join(File.dirname(__FILE__), '..')) # The puppet installer registers a 'Puppet' event source. For the moment events will be logged with this key, but # it may be a good idea to split the Service and Puppet events later so it's easier to read in the windows Event Log. # # Example code to register an event source; # eventlogdll = File.expand_path(File.join(basedir, 'puppet', 'ext', 'windows', 'eventlog', 'puppetres.dll')) # if (File.exists?(eventlogdll)) # Win32::EventLog.add_event_source( # 'source' => "Application", # 'key_name' => "Puppet Agent", # 'category_count' => 3, # 'event_message_file' => eventlogdll, # 'category_message_file' => eventlogdll # ) # end puppet = File.join(basedir, 'bin', 'puppet.bat') unless File.exists?(puppet) log_err("File not found: '#{puppet}'") return end log_debug("Using '#{puppet}'") log_notice('Service started') while running? do begin runinterval = %x{ "#{puppet}" agent --configprint runinterval }.to_i if runinterval == 0 runinterval = 1800 log_err("Failed to determine runinterval, defaulting to #{runinterval} seconds") end rescue Exception => e log_exception(e) runinterval = 1800 end if state == RUNNING or state == IDLE log_notice("Executing agent with arguments: #{args}") - pid = Process.create(:command_line => "\"#{puppet}\" agent --onetime #{args}", :creation_flags => Process::CREATE_NEW_CONSOLE).process_id + pid = Process.create(:command_line => "\"#{puppet}\" agent --onetime #{args}", :creation_flags => CREATE_NEW_CONSOLE).process_id log_debug("Process created: #{pid}") else log_debug("Service is paused. Not invoking Puppet agent") end log_debug("Service waiting for #{runinterval} seconds") sleep(runinterval) log_debug('Service woken up') end log_notice('Service stopped') rescue Exception => e log_exception(e) end def service_stop log_notice('Service stopping') Thread.main.wakeup end def service_pause # The service will not stay in a paused stated, instead it will go back into a running state after a short period of time. This is an issue in the Win32-Service ruby code # Raised bug https://github.com/djberg96/win32-service/issues/11 and is fixed in version 0.8.3. # Because the Pause feature is so rarely used, there is no point in creating a workaround until puppet uses 0.8.3. log_notice('Service pausing. The service will not stay paused. See Puppet Issue PUP-1471 for more information') end def service_resume log_notice('Service resuming') end def service_shutdown log_notice('Host shutting down') end # Interrogation handler is just for debug. Can be commented out or removed entirely. # def service_interrogate # log_debug('Service is being interrogated') # end def log_exception(e) log_err(e.message) log_err(e.backtrace.join("\n")) end def log(msg, level) if LEVELS.index(level) >= @loglevel if (@LOG_TO_FILE) File.open(LOG_FILE, 'a') { |f| f.puts("#{Time.now} Puppet (#{level}): #{msg}") } end case level when :debug report_windows_event(Win32::EventLog::INFO,0x01,msg.to_s) when :info report_windows_event(Win32::EventLog::INFO,0x01,msg.to_s) when :notice report_windows_event(Win32::EventLog::INFO,0x01,msg.to_s) when :err report_windows_event(Win32::EventLog::ERR,0x03,msg.to_s) else report_windows_event(Win32::EventLog::WARN,0x02,msg.to_s) end end end def report_windows_event(type,id,message) begin eventlog = nil eventlog = Win32::EventLog.open("Application") eventlog.report_event( :source => "Puppet", :event_type => type, # Win32::EventLog::INFO or WARN, ERROR :event_id => id, # 0x01 or 0x02, 0x03 etc. :data => message # "the message" ) rescue Exception => e # Ignore all errors ensure if (!eventlog.nil?) eventlog.close end end end end if __FILE__ == $0 WindowsDaemon.mainloop end diff --git a/lib/puppet/settings/priority_setting.rb b/lib/puppet/settings/priority_setting.rb index 707b8ab82..66443398f 100644 --- a/lib/puppet/settings/priority_setting.rb +++ b/lib/puppet/settings/priority_setting.rb @@ -1,42 +1,42 @@ require 'puppet/settings/base_setting' # A setting that represents a scheduling priority, and evaluates to an # OS-specific priority level. class Puppet::Settings::PrioritySetting < Puppet::Settings::BaseSetting PRIORITY_MAP = if Puppet::Util::Platform.windows? - require 'win32/process' + require 'puppet/util/windows/process' { - :high => Process::HIGH_PRIORITY_CLASS, - :normal => Process::NORMAL_PRIORITY_CLASS, - :low => Process::BELOW_NORMAL_PRIORITY_CLASS, - :idle => Process::IDLE_PRIORITY_CLASS + :high => Puppet::Util::Windows::Process::HIGH_PRIORITY_CLASS, + :normal => Puppet::Util::Windows::Process::NORMAL_PRIORITY_CLASS, + :low => Puppet::Util::Windows::Process::BELOW_NORMAL_PRIORITY_CLASS, + :idle => Puppet::Util::Windows::Process::IDLE_PRIORITY_CLASS } else { :high => -10, :normal => 0, :low => 10, :idle => 19 } end def type :priority end def munge(value) return unless value case when value.is_a?(Integer) value when (value.is_a?(String) and value =~ /\d+/) value.to_i when (value.is_a?(String) and PRIORITY_MAP[value.to_sym]) PRIORITY_MAP[value.to_sym] else raise Puppet::Settings::ValidationError, "Invalid priority format '#{value.inspect}' for parameter: #{@name}" end end end diff --git a/lib/puppet/util/windows/process.rb b/lib/puppet/util/windows/process.rb index c92c75a5b..c6a8c0db5 100644 --- a/lib/puppet/util/windows/process.rb +++ b/lib/puppet/util/windows/process.rb @@ -1,348 +1,354 @@ require 'puppet/util/windows' require 'win32/process' require 'ffi' module Puppet::Util::Windows::Process extend Puppet::Util::Windows::String extend FFI::Library WAIT_TIMEOUT = 0x102 def execute(command, arguments, stdin, stdout, stderr) Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr}, :close_handles => false ) end module_function :execute def wait_process(handle) while WaitForSingleObject(handle, 0) == WAIT_TIMEOUT sleep(1) end exit_status = -1 FFI::MemoryPointer.new(:dword, 1) do |exit_status_ptr| if GetExitCodeProcess(handle, exit_status_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to get child process exit code") end exit_status = exit_status_ptr.read_dword # $CHILD_STATUS is not set when calling win32/process Process.create # and since it's read-only, we can't set it. But we can execute a # a shell that simply returns the desired exit status, which has the # desired effect. %x{#{ENV['COMSPEC']} /c exit #{exit_status}} end exit_status end module_function :wait_process def get_current_process # this pseudo-handle does not require closing per MSDN docs GetCurrentProcess() end module_function :get_current_process def open_process_token(handle, desired_access, &block) token_handle = nil begin FFI::MemoryPointer.new(:handle, 1) do |token_handle_ptr| result = OpenProcessToken(handle, desired_access, token_handle_ptr) if result == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new( "OpenProcessToken(#{handle}, #{desired_access.to_s(8)}, #{token_handle_ptr})") end yield token_handle = token_handle_ptr.read_handle end token_handle ensure FFI::WIN32.CloseHandle(token_handle) if token_handle end # token_handle has had CloseHandle called against it, so nothing to return nil end module_function :open_process_token # Execute a block with the current process token def with_process_token(access, &block) handle = get_current_process open_process_token(handle, access) do |token_handle| yield token_handle end # all handles have been closed, so nothing to safely return nil end module_function :with_process_token def lookup_privilege_value(name, system_name = '', &block) FFI::MemoryPointer.new(LUID.size) do |luid_ptr| result = LookupPrivilegeValueW( wide_string(system_name), wide_string(name.to_s), luid_ptr ) if result == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new( "LookupPrivilegeValue(#{system_name}, #{name}, #{luid_ptr})") end yield LUID.new(luid_ptr) end # the underlying MemoryPointer for LUID is cleaned up by this point nil end module_function :lookup_privilege_value def get_token_information(token_handle, token_information, &block) # to determine buffer size FFI::MemoryPointer.new(:dword, 1) do |return_length_ptr| result = GetTokenInformation(token_handle, token_information, nil, 0, return_length_ptr) return_length = return_length_ptr.read_dword if return_length <= 0 raise Puppet::Util::Windows::Error.new( "GetTokenInformation(#{token_handle}, #{token_information}, nil, 0, #{return_length_ptr})") end # re-call API with properly sized buffer for all results FFI::MemoryPointer.new(return_length) do |token_information_buf| result = GetTokenInformation(token_handle, token_information, token_information_buf, return_length, return_length_ptr) if result == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new( "GetTokenInformation(#{token_handle}, #{token_information}, #{token_information_buf}, " + "#{return_length}, #{return_length_ptr})") end yield token_information_buf end end # GetTokenInformation buffer has been cleaned up by this point, nothing to return nil end module_function :get_token_information def parse_token_information_as_token_privileges(token_information_buf) raw_privileges = TOKEN_PRIVILEGES.new(token_information_buf) privileges = { :count => raw_privileges[:PrivilegeCount], :privileges => [] } offset = token_information_buf + TOKEN_PRIVILEGES.offset_of(:Privileges) privilege_ptr = FFI::Pointer.new(LUID_AND_ATTRIBUTES, offset) # extract each instance of LUID_AND_ATTRIBUTES 0.upto(privileges[:count] - 1) do |i| privileges[:privileges] << LUID_AND_ATTRIBUTES.new(privilege_ptr[i]) end privileges end module_function :parse_token_information_as_token_privileges def parse_token_information_as_token_elevation(token_information_buf) TOKEN_ELEVATION.new(token_information_buf) end module_function :parse_token_information_as_token_elevation TOKEN_ALL_ACCESS = 0xF01FF ERROR_NO_SUCH_PRIVILEGE = 1313 def process_privilege_symlink? privilege_symlink = false handle = get_current_process open_process_token(handle, TOKEN_ALL_ACCESS) do |token_handle| lookup_privilege_value('SeCreateSymbolicLinkPrivilege') do |luid| get_token_information(token_handle, :TokenPrivileges) do |token_info| token_privileges = parse_token_information_as_token_privileges(token_info) privilege_symlink = token_privileges[:privileges].any? { |p| p[:Luid].values == luid.values } end end end privilege_symlink rescue Puppet::Util::Windows::Error => e if e.code == ERROR_NO_SUCH_PRIVILEGE false # pre-Vista else raise e end end module_function :process_privilege_symlink? TOKEN_QUERY = 0x0008 # Returns whether or not the owner of the current process is running # with elevated security privileges. # # Only supported on Windows Vista or later. # def elevated_security? # default / pre-Vista elevated = false handle = nil begin handle = get_current_process open_process_token(handle, TOKEN_QUERY) do |token_handle| get_token_information(token_handle, :TokenElevation) do |token_info| token_elevation = parse_token_information_as_token_elevation(token_info) # TokenIsElevated member of the TOKEN_ELEVATION struct elevated = token_elevation[:TokenIsElevated] != 0 end end elevated rescue Puppet::Util::Windows::Error => e raise e if e.code != ERROR_NO_SUCH_PRIVILEGE ensure FFI::WIN32.CloseHandle(handle) if handle end end module_function :elevated_security? + ABOVE_NORMAL_PRIORITY_CLASS = 0x0008000 + BELOW_NORMAL_PRIORITY_CLASS = 0x0004000 + HIGH_PRIORITY_CLASS = 0x0000080 + IDLE_PRIORITY_CLASS = 0x0000040 + NORMAL_PRIORITY_CLASS = 0x0000020 + REALTIME_PRIORITY_CLASS = 0x0000010 ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx # DWORD WINAPI WaitForSingleObject( # _In_ HANDLE hHandle, # _In_ DWORD dwMilliseconds # ); ffi_lib :kernel32 attach_function_private :WaitForSingleObject, [:handle, :dword], :dword # http://msdn.microsoft.com/en-us/library/windows/desktop/ms683189(v=vs.85).aspx # BOOL WINAPI GetExitCodeProcess( # _In_ HANDLE hProcess, # _Out_ LPDWORD lpExitCode # ); ffi_lib :kernel32 attach_function_private :GetExitCodeProcess, [:handle, :lpdword], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/ms683179(v=vs.85).aspx # HANDLE WINAPI GetCurrentProcess(void); ffi_lib :kernel32 attach_function_private :GetCurrentProcess, [], :handle # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379295(v=vs.85).aspx # BOOL WINAPI OpenProcessToken( # _In_ HANDLE ProcessHandle, # _In_ DWORD DesiredAccess, # _Out_ PHANDLE TokenHandle # ); ffi_lib :advapi32 attach_function_private :OpenProcessToken, [:handle, :dword, :phandle], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379261(v=vs.85).aspx # typedef struct _LUID { # DWORD LowPart; # LONG HighPart; # } LUID, *PLUID; class LUID < FFI::Struct layout :LowPart, :dword, :HighPart, :win32_long end # http://msdn.microsoft.com/en-us/library/Windows/desktop/aa379180(v=vs.85).aspx # BOOL WINAPI LookupPrivilegeValue( # _In_opt_ LPCTSTR lpSystemName, # _In_ LPCTSTR lpName, # _Out_ PLUID lpLuid # ); ffi_lib :advapi32 attach_function_private :LookupPrivilegeValueW, [:lpcwstr, :lpcwstr, :pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379626(v=vs.85).aspx TOKEN_INFORMATION_CLASS = enum( :TokenUser, 1, :TokenGroups, :TokenPrivileges, :TokenOwner, :TokenPrimaryGroup, :TokenDefaultDacl, :TokenSource, :TokenType, :TokenImpersonationLevel, :TokenStatistics, :TokenRestrictedSids, :TokenSessionId, :TokenGroupsAndPrivileges, :TokenSessionReference, :TokenSandBoxInert, :TokenAuditPolicy, :TokenOrigin, :TokenElevationType, :TokenLinkedToken, :TokenElevation, :TokenHasRestrictions, :TokenAccessInformation, :TokenVirtualizationAllowed, :TokenVirtualizationEnabled, :TokenIntegrityLevel, :TokenUIAccess, :TokenMandatoryPolicy, :TokenLogonSid, :TokenIsAppContainer, :TokenCapabilities, :TokenAppContainerSid, :TokenAppContainerNumber, :TokenUserClaimAttributes, :TokenDeviceClaimAttributes, :TokenRestrictedUserClaimAttributes, :TokenRestrictedDeviceClaimAttributes, :TokenDeviceGroups, :TokenRestrictedDeviceGroups, :TokenSecurityAttributes, :TokenIsRestricted, :MaxTokenInfoClass ) # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379263(v=vs.85).aspx # typedef struct _LUID_AND_ATTRIBUTES { # LUID Luid; # DWORD Attributes; # } LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES; class LUID_AND_ATTRIBUTES < FFI::Struct layout :Luid, LUID, :Attributes, :dword end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379630(v=vs.85).aspx # typedef struct _TOKEN_PRIVILEGES { # DWORD PrivilegeCount; # LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY]; # } TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES; class TOKEN_PRIVILEGES < FFI::Struct layout :PrivilegeCount, :dword, :Privileges, [LUID_AND_ATTRIBUTES, 1] # placeholder for offset end # http://msdn.microsoft.com/en-us/library/windows/desktop/bb530717(v=vs.85).aspx # typedef struct _TOKEN_ELEVATION { # DWORD TokenIsElevated; # } TOKEN_ELEVATION, *PTOKEN_ELEVATION; class TOKEN_ELEVATION < FFI::Struct layout :TokenIsElevated, :dword end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa446671(v=vs.85).aspx # BOOL WINAPI GetTokenInformation( # _In_ HANDLE TokenHandle, # _In_ TOKEN_INFORMATION_CLASS TokenInformationClass, # _Out_opt_ LPVOID TokenInformation, # _In_ DWORD TokenInformationLength, # _Out_ PDWORD ReturnLength # ); ffi_lib :advapi32 attach_function_private :GetTokenInformation, [:handle, TOKEN_INFORMATION_CLASS, :lpvoid, :dword, :pdword ], :win32_bool end diff --git a/spec/unit/settings/priority_setting_spec.rb b/spec/unit/settings/priority_setting_spec.rb index d51e39dc4..62cad5def 100755 --- a/spec/unit/settings/priority_setting_spec.rb +++ b/spec/unit/settings/priority_setting_spec.rb @@ -1,66 +1,66 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/settings' require 'puppet/settings/priority_setting' require 'puppet/util/platform' describe Puppet::Settings::PrioritySetting do let(:setting) { described_class.new(:settings => mock('settings'), :desc => "test") } it "is of type :priority" do setting.type.should == :priority end describe "when munging the setting" do it "passes nil through" do setting.munge(nil).should be_nil end it "returns the same value if given an integer" do setting.munge(5).should == 5 end it "returns an integer if given a decimal string" do setting.munge('12').should == 12 end it "returns a negative integer if given a negative integer string" do setting.munge('-5').should == -5 end it "fails if given anything else" do [ 'foo', 'realtime', true, 8.3, [] ].each do |value| expect { setting.munge(value) }.to raise_error(Puppet::Settings::ValidationError) end end describe "on a Unix-like platform it", :unless => Puppet::Util::Platform.windows? do it "parses high, normal, low, and idle priorities" do { 'high' => -10, 'normal' => 0, 'low' => 10, 'idle' => 19 }.each do |value, converted_value| setting.munge(value).should == converted_value end end end describe "on a Windows-like platform it", :if => Puppet::Util::Platform.windows? do it "parses high, normal, low, and idle priorities" do { - 'high' => Process::HIGH_PRIORITY_CLASS, - 'normal' => Process::NORMAL_PRIORITY_CLASS, - 'low' => Process::BELOW_NORMAL_PRIORITY_CLASS, - 'idle' => Process::IDLE_PRIORITY_CLASS + 'high' => Puppet::Util::Windows::Process::HIGH_PRIORITY_CLASS, + 'normal' => Puppet::Util::Windows::Process::NORMAL_PRIORITY_CLASS, + 'low' => Puppet::Util::Windows::Process::BELOW_NORMAL_PRIORITY_CLASS, + 'idle' => Puppet::Util::Windows::Process::IDLE_PRIORITY_CLASS }.each do |value, converted_value| setting.munge(value).should == converted_value end end end end end