# coding: utf-8 # frozen_string_literal: true require_relative '../../../puppet/ffi/windows' module Puppet::Util::Windows # This module is designed to provide an API between the windows system and puppet for # service management. # # for an overview of the service state transitions see: https://docs.microsoft.com/en-us/windows/desktop/Services/service-status-transitions module Service extend Puppet::Util::Windows::String include Puppet::FFI::Windows::Constants extend Puppet::FFI::Windows::Constants include Puppet::FFI::Windows::Structs extend Puppet::FFI::Windows::Structs include Puppet::FFI::Windows::Functions extend Puppet::FFI::Windows::Functions # Returns true if the service exists, false otherwise. # # @param [String] service_name name of the service def exists?(service_name) open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |_| true end rescue Puppet::Util::Windows::Error => e return false if e.code == ERROR_SERVICE_DOES_NOT_EXIST raise e end module_function :exists? # Start a windows service # # @param [String] service_name name of the service to start # @param optional [Integer] timeout the minumum number of seconds to wait before timing out def start(service_name, timeout: DEFAULT_TIMEOUT) Puppet.debug _("Starting the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } valid_initial_states = [ SERVICE_STOP_PENDING, SERVICE_STOPPED, SERVICE_START_PENDING ] transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| if StartServiceW(service, 0, FFI::Pointer::NULL) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error, _("Failed to start the service") end end Puppet.debug _("Successfully started the %{service_name} service") % { service_name: service_name } end module_function :start # Stop a windows service # # @param [String] service_name name of the service to stop # @param optional [Integer] timeout the minumum number of seconds to wait before timing out def stop(service_name, timeout: DEFAULT_TIMEOUT) Puppet.debug _("Stopping the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } valid_initial_states = SERVICE_STATES.keys - [SERVICE_STOPPED] transition_service_state(service_name, valid_initial_states, SERVICE_STOPPED, timeout) do |service| send_service_control_signal(service, SERVICE_CONTROL_STOP) end Puppet.debug _("Successfully stopped the %{service_name} service") % { service_name: service_name } end module_function :stop # Resume a paused windows service # # @param [String] service_name name of the service to resume # @param optional [Integer] :timeout the minumum number of seconds to wait before timing out def resume(service_name, timeout: DEFAULT_TIMEOUT) Puppet.debug _("Resuming the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } valid_initial_states = [ SERVICE_PAUSE_PENDING, SERVICE_PAUSED, SERVICE_CONTINUE_PENDING ] transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| # The SERVICE_CONTROL_CONTINUE signal can only be sent when # the service is in the SERVICE_PAUSED state wait_on_pending_state(service, SERVICE_PAUSE_PENDING, timeout) send_service_control_signal(service, SERVICE_CONTROL_CONTINUE) end Puppet.debug _("Successfully resumed the %{service_name} service") % { service_name: service_name } end module_function :resume # Query the state of a service using QueryServiceStatusEx # # @param [string] service_name name of the service to query # @return [string] the status of the service def service_state(service_name) state = nil open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |service| query_status(service) do |status| state = SERVICE_STATES[status[:dwCurrentState]] end end if state.nil? raise Puppet::Error.new(_("Unknown Service state '%{current_state}' for '%{service_name}'") % { current_state: state.to_s, service_name: service_name}) end state end module_function :service_state # Query the configuration of a service using QueryServiceConfigW # or QueryServiceConfig2W # # @param [String] service_name name of the service to query # @return [QUERY_SERVICE_CONFIGW.struct] the configuration of the service def service_start_type(service_name) start_type = nil open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| query_config(service) do |config| start_type = SERVICE_START_TYPES[config[:dwStartType]] end end # if the service has type AUTO_START, check if it's a delayed service if start_type == :SERVICE_AUTO_START open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| query_config2(service, SERVICE_CONFIG_DELAYED_AUTO_START_INFO) do |config| return :SERVICE_DELAYED_AUTO_START if config[:fDelayedAutostart] == 1 end end end if start_type.nil? raise Puppet::Error.new(_("Unknown start type '%{start_type}' for '%{service_name}'") % { start_type: start_type.to_s, service_name: service_name}) end start_type end module_function :service_start_type # Query the configuration of a service using QueryServiceConfigW # to find its current logon account # # @return [String] logon_account account currently set for the service's logon # in the format "DOMAIN\Account" or ".\Account" if it's a local account def logon_account(service_name) open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| query_config(service) do |config| return config[:lpServiceStartName].read_arbitrary_wide_string_up_to(Puppet::Util::Windows::ADSI::User::MAX_USERNAME_LENGTH) end end end module_function :logon_account # Set the startup configuration of a windows service # # @param [String] service_name the name of the service to modify # @param [Hash] options the configuration to be applied. Expected option keys: # - [Integer] startup_type a code corresponding to a start type for # windows service, see the "Service start type codes" section in the # Puppet::Util::Windows::Service file for the list of available codes # - [String] logon_account the account to be used by the service for logon # - [String] logon_password the provided logon_account's password to be used by the service for logon # - [Bool] delayed whether the service should be started with a delay def set_startup_configuration(service_name, options: {}) options[:startup_type] = SERVICE_START_TYPES.key(options[:startup_type]) || SERVICE_NO_CHANGE options[:logon_account] = wide_string(options[:logon_account]) || FFI::Pointer::NULL options[:logon_password] = wide_string(options[:logon_password]) || FFI::Pointer::NULL open_service(service_name, SC_MANAGER_CONNECT, SERVICE_CHANGE_CONFIG) do |service| success = ChangeServiceConfigW( service, SERVICE_NO_CHANGE, # dwServiceType options[:startup_type], # dwStartType SERVICE_NO_CHANGE, # dwErrorControl FFI::Pointer::NULL, # lpBinaryPathName FFI::Pointer::NULL, # lpLoadOrderGroup FFI::Pointer::NULL, # lpdwTagId FFI::Pointer::NULL, # lpDependencies options[:logon_account], # lpServiceStartName options[:logon_password], # lpPassword FFI::Pointer::NULL # lpDisplayName ) if success == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new(_("Failed to update service configuration")) end end if options[:startup_type] options[:delayed] ||= false set_startup_mode_delayed(service_name, options[:delayed]) end end module_function :set_startup_configuration # enumerate over all services in all states and return them as a hash # # @return [Hash] a hash containing services: # { 'service name' => { # 'display_name' => 'display name', # 'service_status_process' => SERVICE_STATUS_PROCESS struct # } # } def services services = {} open_scm(SC_MANAGER_ENUMERATE_SERVICE) do |scm| size_required = 0 services_returned = 0 FFI::MemoryPointer.new(:dword) do |bytes_pointer| FFI::MemoryPointer.new(:dword) do |svcs_ret_ptr| FFI::MemoryPointer.new(:dword) do |resume_ptr| resume_ptr.write_dword(0) # Fetch the bytes of memory required to be allocated # for QueryServiceConfigW to return succesfully. This # is done by sending NULL and 0 for the pointer and size # respectively, letting the command fail, then reading the # value of pcbBytesNeeded # # return value will be false from this call, since it's designed # to fail. Just ignore it EnumServicesStatusExW( scm, :SC_ENUM_PROCESS_INFO, ALL_SERVICE_TYPES, SERVICE_STATE_ALL, FFI::Pointer::NULL, 0, bytes_pointer, svcs_ret_ptr, resume_ptr, FFI::Pointer::NULL ) size_required = bytes_pointer.read_dword FFI::MemoryPointer.new(size_required) do |buffer_ptr| resume_ptr.write_dword(0) svcs_ret_ptr.write_dword(0) success = EnumServicesStatusExW( scm, :SC_ENUM_PROCESS_INFO, ALL_SERVICE_TYPES, SERVICE_STATE_ALL, buffer_ptr, buffer_ptr.size, bytes_pointer, svcs_ret_ptr, resume_ptr, FFI::Pointer::NULL ) if success == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new(_("Failed to fetch services")) end # Now that the buffer is populated with services # we pull the data from memory using pointer arithmetic: # the number of services returned by the function is # available to be read from svcs_ret_ptr, and we iterate # that many times moving the cursor pointer the length of # ENUM_SERVICE_STATUS_PROCESSW.size. This should iterate # over the buffer and extract each struct. services_returned = svcs_ret_ptr.read_dword cursor_ptr = FFI::Pointer.new(ENUM_SERVICE_STATUS_PROCESSW, buffer_ptr) 0.upto(services_returned - 1) do |index| service = ENUM_SERVICE_STATUS_PROCESSW.new(cursor_ptr[index]) services[service[:lpServiceName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX)] = { :display_name => service[:lpDisplayName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX), :service_status_process => service[:ServiceStatusProcess] } end end # buffer_ptr end # resume_ptr end # scvs_ret_ptr end # bytes_ptr end # open_scm services end module_function :services class << self # @api private # Opens a connection to the SCManager on windows then uses that # handle to create a handle to a specific service in windows # corresponding to service_name # # this function takes a block that executes within the context of # the open service handler, and will close the service and SCManager # handles once the block finishes # # @param [string] service_name the name of the service to open # @param [Integer] scm_access code corresponding to the access type requested for the scm # @param [Integer] service_access code corresponding to the access type requested for the service # @yieldparam [:handle] service the windows native handle used to access # the service # @return the result of the block def open_service(service_name, scm_access, service_access, &block) service = FFI::Pointer::NULL_HANDLE result = nil open_scm(scm_access) do |scm| service = OpenServiceW(scm, wide_string(service_name), service_access) raise Puppet::Util::Windows::Error.new(_("Failed to open a handle to the service")) if service == FFI::Pointer::NULL_HANDLE result = yield service end result ensure CloseServiceHandle(service) end private :open_service # @api private # # Opens a handle to the service control manager # # @param [Integer] scm_access code corresponding to the access type requested for the scm def open_scm(scm_access, &block) scm = OpenSCManagerW(FFI::Pointer::NULL, FFI::Pointer::NULL, scm_access) raise Puppet::Util::Windows::Error.new(_("Failed to open a handle to the service control manager")) if scm == FFI::Pointer::NULL_HANDLE yield scm ensure CloseServiceHandle(scm) end private :open_scm # @api private # Transition the service to the specified state. The block should perform # the actual transition. # # @param [String] service_name the name of the service to transition # @param [[Integer]] valid_initial_states an array of valid states that the service can transition from # @param [Integer] final_state the state that the service will transition to # @param [Integer] timeout the minumum number of seconds to wait before timing out def transition_service_state(service_name, valid_initial_states, final_state, timeout, &block) service_access = SERVICE_START | SERVICE_STOP | SERVICE_PAUSE_CONTINUE | SERVICE_QUERY_STATUS open_service(service_name, SC_MANAGER_CONNECT, service_access) do |service| query_status(service) do |status| initial_state = status[:dwCurrentState] # If the service is already in the final_state, then # no further work needs to be done if initial_state == final_state Puppet.debug _("The service is already in the %{final_state} state. No further work needs to be done.") % { final_state: SERVICE_STATES[final_state] } next end # Check that initial_state corresponds to a valid # initial state unless valid_initial_states.include?(initial_state) valid_initial_states_str = valid_initial_states.map do |state| SERVICE_STATES[state] end.join(", ") raise Puppet::Error, _("The service must be in one of the %{valid_initial_states} states to perform this transition. It is currently in the %{current_state} state.") % { valid_initial_states: valid_initial_states_str, current_state: SERVICE_STATES[initial_state] } end # Check if there's a pending transition to the final_state. If so, then wait for # that transition to finish. possible_pending_states = FINAL_STATES.keys.select do |pending_state| # SERVICE_RUNNING has two pending states, SERVICE_START_PENDING and # SERVICE_CONTINUE_PENDING. That is why we need the #select here FINAL_STATES[pending_state] == final_state end if possible_pending_states.include?(initial_state) Puppet.debug _("There is already a pending transition to the %{final_state} state for the %{service_name} service.") % { final_state: SERVICE_STATES[final_state], service_name: service_name } wait_on_pending_state(service, initial_state, timeout) next end # If we are in an unsafe pending state like SERVICE_START_PENDING # or SERVICE_STOP_PENDING, then we want to wait for that pending # transition to finish before transitioning the service state. # The reason we do this is because SERVICE_START_PENDING is when # the service thread is being created and initialized, while # SERVICE_STOP_PENDING is when the service thread is being cleaned # up and destroyed. Thus there is a chance that when the service is # in either of these states, its service thread may not yet be ready # to perform the state transition (it may not even exist). if UNSAFE_PENDING_STATES.include?(initial_state) Puppet.debug _("The service is in the %{pending_state} state, which is an unsafe pending state.") % { pending_state: SERVICE_STATES[initial_state] } wait_on_pending_state(service, initial_state, timeout) initial_state = FINAL_STATES[initial_state] end Puppet.debug _("Transitioning the %{service_name} service from %{initial_state} to %{final_state}") % { service_name: service_name, initial_state: SERVICE_STATES[initial_state], final_state: SERVICE_STATES[final_state] } yield service Puppet.debug _("Waiting for the transition to finish") wait_on_state_transition(service, initial_state, final_state, timeout) end end rescue => detail raise Puppet::Error, _("Failed to transition the %{service_name} service to the %{final_state} state. Detail: %{detail}") % { service_name: service_name, final_state: SERVICE_STATES[final_state], detail: detail }, detail.backtrace end private :transition_service_state # @api private # perform QueryServiceStatusEx on a windows service and return the # result # # @param [:handle] service handle of the service to query # @return [SERVICE_STATUS_PROCESS struct] the result of the query def query_status(service) size_required = nil status = nil # Fetch the bytes of memory required to be allocated # for QueryServiceConfigW to return succesfully. This # is done by sending NULL and 0 for the pointer and size # respectively, letting the command fail, then reading the # value of pcbBytesNeeded FFI::MemoryPointer.new(:lpword) do |bytes_pointer| # return value will be false from this call, since it's designed # to fail. Just ignore it QueryServiceStatusEx( service, :SC_STATUS_PROCESS_INFO, FFI::Pointer::NULL, 0, bytes_pointer ) size_required = bytes_pointer.read_dword FFI::MemoryPointer.new(size_required) do |ssp_ptr| status = SERVICE_STATUS_PROCESS.new(ssp_ptr) success = QueryServiceStatusEx( service, :SC_STATUS_PROCESS_INFO, ssp_ptr, size_required, bytes_pointer ) if success == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new(_("Service query failed")) end yield status end end end private :query_status # @api private # perform QueryServiceConfigW on a windows service and return the # result # # @param [:handle] service handle of the service to query # @return [QUERY_SERVICE_CONFIGW struct] the result of the query def query_config(service, &block) config = nil size_required = nil # Fetch the bytes of memory required to be allocated # for QueryServiceConfigW to return succesfully. This # is done by sending NULL and 0 for the pointer and size # respectively, letting the command fail, then reading the # value of pcbBytesNeeded FFI::MemoryPointer.new(:lpword) do |bytes_pointer| # return value will be false from this call, since it's designed # to fail. Just ignore it QueryServiceConfigW(service, FFI::Pointer::NULL, 0, bytes_pointer) size_required = bytes_pointer.read_dword FFI::MemoryPointer.new(size_required) do |ssp_ptr| config = QUERY_SERVICE_CONFIGW.new(ssp_ptr) success = QueryServiceConfigW( service, ssp_ptr, size_required, bytes_pointer ) if success == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new(_("Service query failed")) end yield config end end end private :query_config # @api private # perform QueryServiceConfig2W on a windows service and return the # result # # @param [:handle] service handle of the service to query # @param [Integer] info_level the configuration information to be queried # @return [QUERY_SERVICE_CONFIG2W struct] the result of the query def query_config2(service, info_level, &block) config = nil size_required = nil # Fetch the bytes of memory required to be allocated # for QueryServiceConfig2W to return succesfully. This # is done by sending NULL and 0 for the pointer and size # respectively, letting the command fail, then reading the # value of pcbBytesNeeded FFI::MemoryPointer.new(:lpword) do |bytes_pointer| # return value will be false from this call, since it's designed # to fail. Just ignore it QueryServiceConfig2W(service, info_level, FFI::Pointer::NULL, 0, bytes_pointer) size_required = bytes_pointer.read_dword FFI::MemoryPointer.new(size_required) do |ssp_ptr| # We need to supply the appropriate struct to be created based on # the info_level case info_level when SERVICE_CONFIG_DELAYED_AUTO_START_INFO config = SERVICE_DELAYED_AUTO_START_INFO.new(ssp_ptr) end success = QueryServiceConfig2W( service, info_level, ssp_ptr, size_required, bytes_pointer ) if success == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new(_("Service query for %{parameter_name} failed") % { parameter_name: SERVICE_CONFIG_TYPES[info_level] } ) end yield config end end end private :query_config2 # @api private # Sets an optional parameter on a service by calling # ChangeServiceConfig2W # # @param [String] service_name name of service # @param [Integer] change parameter to change # @param [struct] value appropriate struct based on the parameter to change def set_optional_parameter(service_name, change, value) open_service(service_name, SC_MANAGER_CONNECT, SERVICE_CHANGE_CONFIG) do |service| success = ChangeServiceConfig2W( service, change, # dwInfoLevel value, # lpInfo ) if success == FFI::WIN32_FALSE raise Puppet::Util::windows::Error.new(_("Failed to update service %{change} configuration") % { change: change } ) end end end private :set_optional_parameter # @api private # Controls the delayed auto-start setting of a service # # @param [String] service_name name of service # @param [Bool] delayed whether the service should be started with a delay or not def set_startup_mode_delayed(service_name, delayed) delayed_start = SERVICE_DELAYED_AUTO_START_INFO.new delayed_start[:fDelayedAutostart] = delayed set_optional_parameter(service_name, SERVICE_CONFIG_DELAYED_AUTO_START_INFO, delayed_start) end private :set_startup_mode_delayed # @api private # Sends a service control signal to a service # # @param [:handle] service handle to the service # @param [Integer] signal the service control signal to send def send_service_control_signal(service, signal) FFI::MemoryPointer.new(SERVICE_STATUS.size) do |status_ptr| status = SERVICE_STATUS.new(status_ptr) if ControlService(service, signal, status) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error, _("Failed to send the %{control_signal} signal to the service. Its current state is %{current_state}. Reason for failure:") % { control_signal: SERVICE_CONTROL_SIGNALS[signal], current_state: SERVICE_STATES[status[:dwCurrentState]] } end end end # @api private # Waits for a service to transition from one state to # another state. # # @param [:handle] service handle to the service to wait on # @param [Integer] initial_state the state that the service is transitioning from. # @param [Integer] final_state the state that the service is transitioning to # @param [Integer] timeout the minumum number of seconds to wait before timing out def wait_on_state_transition(service, initial_state, final_state, timeout) # Get the pending state for this transition. Note that SERVICE_RUNNING # has two possible pending states, which is why we need this logic. if final_state != SERVICE_RUNNING pending_state = FINAL_STATES.key(final_state) elsif initial_state == SERVICE_STOPPED # SERVICE_STOPPED => SERVICE_RUNNING pending_state = SERVICE_START_PENDING else # SERVICE_PAUSED => SERVICE_RUNNING pending_state = SERVICE_CONTINUE_PENDING end # Wait for the transition to finish state = nil elapsed_time = 0 while elapsed_time <= timeout query_status(service) do |status| state = status[:dwCurrentState] return if state == final_state if state == pending_state Puppet.debug _("The service transitioned to the %{pending_state} state.") % { pending_state: SERVICE_STATES[pending_state] } wait_on_pending_state(service, pending_state, timeout) return end sleep(1) elapsed_time += 1 end end # Timed out while waiting for the transition to finish. Raise an error # We can still use the state variable read from the FFI struct because # FFI creates new Integer objects during an assignment of an integer value # stored in an FFI struct. We verified that the '=' operater is safe # from the freed memory since the new ruby object created during the # assignment will remain in ruby memory and remain immutable and constant. raise Puppet::Error, _("Timed out while waiting for the service to transition from %{initial_state} to %{final_state} OR from %{initial_state} to %{pending_state} to %{final_state}. The service's current state is %{current_state}.") % { initial_state: SERVICE_STATES[initial_state], final_state: SERVICE_STATES[final_state], pending_state: SERVICE_STATES[pending_state], current_state: SERVICE_STATES[state] } end private :wait_on_state_transition # @api private # Waits for a service to finish transitioning from # a pending state. The service must be in the pending state # before invoking this routine. # # @param [:handle] service handle to the service to wait on # @param [Integer] pending_state the pending state # @param [Integer] timeout the minumum number of seconds to wait before timing out def wait_on_pending_state(service, pending_state, timeout) final_state = FINAL_STATES[pending_state] Puppet.debug _("Waiting for the pending transition to the %{final_state} state to finish.") % { final_state: SERVICE_STATES[final_state] } elapsed_time = 0 last_checkpoint = -1 loop do query_status(service) do |status| state = status[:dwCurrentState] checkpoint = status[:dwCheckPoint] wait_hint = status[:dwWaitHint] # Check if our service has finished transitioning to # the final_state OR if an unexpected transition # has occurred return if state == final_state unless state == pending_state raise Puppet::Error, _("Unexpected transition to the %{current_state} state while waiting for the pending transition from %{pending_state} to %{final_state} to finish.") % { current_state: SERVICE_STATES[state], pending_state: SERVICE_STATES[pending_state], final_state: SERVICE_STATES[final_state] } end # Check if any progress has been made since our last sleep # using the dwCheckPoint. If no progress has been made then # check if we've timed out, and raise an error if so if checkpoint > last_checkpoint elapsed_time = 0 last_checkpoint = checkpoint else wait_hint = milliseconds_to_seconds(status[:dwWaitHint]) timeout = wait_hint < timeout ? timeout : wait_hint if elapsed_time >= timeout raise Puppet::Error, _("Timed out while waiting for the pending transition from %{pending_state} to %{final_state} to finish. The current state is %{current_state}.") % { pending_state: SERVICE_STATES[pending_state], final_state: SERVICE_STATES[final_state], current_state: SERVICE_STATES[state] } end end wait_time = wait_hint_to_wait_time(wait_hint) # Wait a bit before rechecking the service's state sleep(wait_time) elapsed_time += wait_time end end end private :wait_on_pending_state # @api private # # create a usable wait time to wait between querying the service. # # @param [Integer] wait_hint the wait hint of a service in milliseconds # @return [Integer] the time to wait in seconds between querying the service def wait_hint_to_wait_time(wait_hint) # Wait 1/10th the wait_hint, but no less than 1 and # no more than 10 seconds wait_time = milliseconds_to_seconds(wait_hint) / 10; wait_time = 1 if wait_time < 1 wait_time = 10 if wait_time > 10 wait_time end private :wait_hint_to_wait_time # @api private # # process the wait hint listed by a service to something # usable by ruby sleep # # @param [Integer] wait_hint the wait hint of a service in milliseconds # @return [Integer] wait_hint in seconds def milliseconds_to_seconds(wait_hint) wait_hint / 1000; end private :milliseconds_to_seconds end end end