module Puppet::Util::Windows::Service
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: docs.microsoft.com/en-us/windows/desktop/Services/service-status-transitions
Constants
- DEFAULT_TIMEOUT
integer value of the floor for timeouts when waiting for service pending states. puppet will wait the length of dwWaitHint if it is longer than this value, but no shorter
Public Class Methods
Returns true if the service exists, false otherwise.
@param [String] service_name name of the service
# File lib/puppet/util/windows/service.rb 29 def exists?(service_name) 30 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |_| 31 true 32 end 33 rescue Puppet::Util::Windows::Error => e 34 return false if e.code == ERROR_SERVICE_DOES_NOT_EXIST 35 raise e 36 end
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
# File lib/puppet/util/windows/service.rb 154 def logon_account(service_name) 155 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 156 query_config(service) do |config| 157 return config[:lpServiceStartName].read_arbitrary_wide_string_up_to(Puppet::Util::Windows::ADSI::User::MAX_USERNAME_LENGTH) 158 end 159 end 160 end
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
# File lib/puppet/util/windows/service.rb 83 def resume(service_name, timeout: DEFAULT_TIMEOUT) 84 Puppet.debug _("Resuming the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 85 86 valid_initial_states = [ 87 SERVICE_PAUSE_PENDING, 88 SERVICE_PAUSED, 89 SERVICE_CONTINUE_PENDING 90 ] 91 92 transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| 93 # The SERVICE_CONTROL_CONTINUE signal can only be sent when 94 # the service is in the SERVICE_PAUSED state 95 wait_on_pending_state(service, SERVICE_PAUSE_PENDING, timeout) 96 97 send_service_control_signal(service, SERVICE_CONTROL_CONTINUE) 98 end 99 100 Puppet.debug _("Successfully resumed the %{service_name} service") % { service_name: service_name } 101 end
@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
# File lib/puppet/util/windows/service.rb 564 def send_service_control_signal(service, signal) 565 FFI::MemoryPointer.new(SERVICE_STATUS.size) do |status_ptr| 566 status = SERVICE_STATUS.new(status_ptr) 567 if ControlService(service, signal, status) == FFI::WIN32_FALSE 568 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]] } 569 end 570 end 571 end
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
# File lib/puppet/util/windows/service.rb 127 def service_start_type(service_name) 128 start_type = nil 129 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 130 query_config(service) do |config| 131 start_type = SERVICE_START_TYPES[config[:dwStartType]] 132 end 133 end 134 # if the service has type AUTO_START, check if it's a delayed service 135 if start_type == :SERVICE_AUTO_START 136 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 137 query_config2(service, SERVICE_CONFIG_DELAYED_AUTO_START_INFO) do |config| 138 return :SERVICE_DELAYED_AUTO_START if config[:fDelayedAutostart] == 1 139 end 140 end 141 end 142 if start_type.nil? 143 raise Puppet::Error.new(_("Unknown start type '%{start_type}' for '%{service_name}'") % { start_type: start_type.to_s, service_name: service_name}) 144 end 145 start_type 146 end
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
# File lib/puppet/util/windows/service.rb 108 def service_state(service_name) 109 state = nil 110 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |service| 111 query_status(service) do |status| 112 state = SERVICE_STATES[status[:dwCurrentState]] 113 end 114 end 115 if state.nil? 116 raise Puppet::Error.new(_("Unknown Service state '%{current_state}' for '%{service_name}'") % { current_state: state.to_s, service_name: service_name}) 117 end 118 state 119 end
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 } }
# File lib/puppet/util/windows/service.rb 212 def services 213 services = {} 214 open_scm(SC_MANAGER_ENUMERATE_SERVICE) do |scm| 215 size_required = 0 216 services_returned = 0 217 FFI::MemoryPointer.new(:dword) do |bytes_pointer| 218 FFI::MemoryPointer.new(:dword) do |svcs_ret_ptr| 219 FFI::MemoryPointer.new(:dword) do |resume_ptr| 220 resume_ptr.write_dword(0) 221 # Fetch the bytes of memory required to be allocated 222 # for QueryServiceConfigW to return succesfully. This 223 # is done by sending NULL and 0 for the pointer and size 224 # respectively, letting the command fail, then reading the 225 # value of pcbBytesNeeded 226 # 227 # return value will be false from this call, since it's designed 228 # to fail. Just ignore it 229 EnumServicesStatusExW( 230 scm, 231 :SC_ENUM_PROCESS_INFO, 232 ALL_SERVICE_TYPES, 233 SERVICE_STATE_ALL, 234 FFI::Pointer::NULL, 235 0, 236 bytes_pointer, 237 svcs_ret_ptr, 238 resume_ptr, 239 FFI::Pointer::NULL 240 ) 241 size_required = bytes_pointer.read_dword 242 FFI::MemoryPointer.new(size_required) do |buffer_ptr| 243 resume_ptr.write_dword(0) 244 svcs_ret_ptr.write_dword(0) 245 success = EnumServicesStatusExW( 246 scm, 247 :SC_ENUM_PROCESS_INFO, 248 ALL_SERVICE_TYPES, 249 SERVICE_STATE_ALL, 250 buffer_ptr, 251 buffer_ptr.size, 252 bytes_pointer, 253 svcs_ret_ptr, 254 resume_ptr, 255 FFI::Pointer::NULL 256 ) 257 if success == FFI::WIN32_FALSE 258 raise Puppet::Util::Windows::Error.new(_("Failed to fetch services")) 259 end 260 # Now that the buffer is populated with services 261 # we pull the data from memory using pointer arithmetic: 262 # the number of services returned by the function is 263 # available to be read from svcs_ret_ptr, and we iterate 264 # that many times moving the cursor pointer the length of 265 # ENUM_SERVICE_STATUS_PROCESSW.size. This should iterate 266 # over the buffer and extract each struct. 267 services_returned = svcs_ret_ptr.read_dword 268 cursor_ptr = FFI::Pointer.new(ENUM_SERVICE_STATUS_PROCESSW, buffer_ptr) 269 0.upto(services_returned - 1) do |index| 270 service = ENUM_SERVICE_STATUS_PROCESSW.new(cursor_ptr[index]) 271 services[service[:lpServiceName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX)] = { 272 :display_name => service[:lpDisplayName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX), 273 :service_status_process => service[:ServiceStatusProcess] 274 } 275 end 276 end # buffer_ptr 277 end # resume_ptr 278 end # scvs_ret_ptr 279 end # bytes_ptr 280 end # open_scm 281 services 282 end
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
# File lib/puppet/util/windows/service.rb 173 def set_startup_configuration(service_name, options: {}) 174 options[:startup_type] = SERVICE_START_TYPES.key(options[:startup_type]) || SERVICE_NO_CHANGE 175 options[:logon_account] = wide_string(options[:logon_account]) || FFI::Pointer::NULL 176 options[:logon_password] = wide_string(options[:logon_password]) || FFI::Pointer::NULL 177 178 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_CHANGE_CONFIG) do |service| 179 success = ChangeServiceConfigW( 180 service, 181 SERVICE_NO_CHANGE, # dwServiceType 182 options[:startup_type], # dwStartType 183 SERVICE_NO_CHANGE, # dwErrorControl 184 FFI::Pointer::NULL, # lpBinaryPathName 185 FFI::Pointer::NULL, # lpLoadOrderGroup 186 FFI::Pointer::NULL, # lpdwTagId 187 FFI::Pointer::NULL, # lpDependencies 188 options[:logon_account], # lpServiceStartName 189 options[:logon_password], # lpPassword 190 FFI::Pointer::NULL # lpDisplayName 191 ) 192 if success == FFI::WIN32_FALSE 193 raise Puppet::Util::Windows::Error.new(_("Failed to update service configuration")) 194 end 195 end 196 197 if options[:startup_type] 198 options[:delayed] ||= false 199 set_startup_mode_delayed(service_name, options[:delayed]) 200 end 201 end
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
# File lib/puppet/util/windows/service.rb 43 def start(service_name, timeout: DEFAULT_TIMEOUT) 44 Puppet.debug _("Starting the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 45 46 valid_initial_states = [ 47 SERVICE_STOP_PENDING, 48 SERVICE_STOPPED, 49 SERVICE_START_PENDING 50 ] 51 52 transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| 53 if StartServiceW(service, 0, FFI::Pointer::NULL) == FFI::WIN32_FALSE 54 raise Puppet::Util::Windows::Error, _("Failed to start the service") 55 end 56 end 57 58 Puppet.debug _("Successfully started the %{service_name} service") % { service_name: service_name } 59 end
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
# File lib/puppet/util/windows/service.rb 66 def stop(service_name, timeout: DEFAULT_TIMEOUT) 67 Puppet.debug _("Stopping the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 68 69 valid_initial_states = SERVICE_STATES.keys - [SERVICE_STOPPED] 70 71 transition_service_state(service_name, valid_initial_states, SERVICE_STOPPED, timeout) do |service| 72 send_service_control_signal(service, SERVICE_CONTROL_STOP) 73 end 74 75 Puppet.debug _("Successfully stopped the %{service_name} service") % { service_name: service_name } 76 end
Private Class Methods
@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
# File lib/puppet/util/windows/service.rb 695 def milliseconds_to_seconds(wait_hint) 696 wait_hint / 1000; 697 end
@api private
Opens a handle to the service control manager
@param [Integer] scm_access code corresponding to the access type requested for the scm
# File lib/puppet/util/windows/service.rb 322 def open_scm(scm_access, &block) 323 scm = OpenSCManagerW(FFI::Pointer::NULL, FFI::Pointer::NULL, scm_access) 324 raise Puppet::Util::Windows::Error.new(_("Failed to open a handle to the service control manager")) if scm == FFI::Pointer::NULL_HANDLE 325 yield scm 326 ensure 327 CloseServiceHandle(scm) 328 end
@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
# File lib/puppet/util/windows/service.rb 301 def open_service(service_name, scm_access, service_access, &block) 302 service = FFI::Pointer::NULL_HANDLE 303 304 result = nil 305 open_scm(scm_access) do |scm| 306 service = OpenServiceW(scm, wide_string(service_name), service_access) 307 raise Puppet::Util::Windows::Error.new(_("Failed to open a handle to the service")) if service == FFI::Pointer::NULL_HANDLE 308 result = yield service 309 end 310 311 result 312 ensure 313 CloseServiceHandle(service) 314 end
@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
# File lib/puppet/util/windows/service.rb 453 def query_config(service, &block) 454 config = nil 455 size_required = nil 456 # Fetch the bytes of memory required to be allocated 457 # for QueryServiceConfigW to return succesfully. This 458 # is done by sending NULL and 0 for the pointer and size 459 # respectively, letting the command fail, then reading the 460 # value of pcbBytesNeeded 461 FFI::MemoryPointer.new(:lpword) do |bytes_pointer| 462 # return value will be false from this call, since it's designed 463 # to fail. Just ignore it 464 QueryServiceConfigW(service, FFI::Pointer::NULL, 0, bytes_pointer) 465 size_required = bytes_pointer.read_dword 466 FFI::MemoryPointer.new(size_required) do |ssp_ptr| 467 config = QUERY_SERVICE_CONFIGW.new(ssp_ptr) 468 success = QueryServiceConfigW( 469 service, 470 ssp_ptr, 471 size_required, 472 bytes_pointer 473 ) 474 if success == FFI::WIN32_FALSE 475 raise Puppet::Util::Windows::Error.new(_("Service query failed")) 476 end 477 yield config 478 end 479 end 480 end
@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
# File lib/puppet/util/windows/service.rb 490 def query_config2(service, info_level, &block) 491 config = nil 492 size_required = nil 493 # Fetch the bytes of memory required to be allocated 494 # for QueryServiceConfig2W to return succesfully. This 495 # is done by sending NULL and 0 for the pointer and size 496 # respectively, letting the command fail, then reading the 497 # value of pcbBytesNeeded 498 FFI::MemoryPointer.new(:lpword) do |bytes_pointer| 499 # return value will be false from this call, since it's designed 500 # to fail. Just ignore it 501 QueryServiceConfig2W(service, info_level, FFI::Pointer::NULL, 0, bytes_pointer) 502 size_required = bytes_pointer.read_dword 503 FFI::MemoryPointer.new(size_required) do |ssp_ptr| 504 # We need to supply the appropriate struct to be created based on 505 # the info_level 506 case info_level 507 when SERVICE_CONFIG_DELAYED_AUTO_START_INFO 508 config = SERVICE_DELAYED_AUTO_START_INFO.new(ssp_ptr) 509 end 510 success = QueryServiceConfig2W( 511 service, 512 info_level, 513 ssp_ptr, 514 size_required, 515 bytes_pointer 516 ) 517 if success == FFI::WIN32_FALSE 518 raise Puppet::Util::Windows::Error.new(_("Service query for %{parameter_name} failed") % { parameter_name: SERVICE_CONFIG_TYPES[info_level] } ) 519 end 520 yield config 521 end 522 end 523 end
@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
# File lib/puppet/util/windows/service.rb 410 def query_status(service) 411 size_required = nil 412 status = nil 413 # Fetch the bytes of memory required to be allocated 414 # for QueryServiceConfigW to return succesfully. This 415 # is done by sending NULL and 0 for the pointer and size 416 # respectively, letting the command fail, then reading the 417 # value of pcbBytesNeeded 418 FFI::MemoryPointer.new(:lpword) do |bytes_pointer| 419 # return value will be false from this call, since it's designed 420 # to fail. Just ignore it 421 QueryServiceStatusEx( 422 service, 423 :SC_STATUS_PROCESS_INFO, 424 FFI::Pointer::NULL, 425 0, 426 bytes_pointer 427 ) 428 size_required = bytes_pointer.read_dword 429 FFI::MemoryPointer.new(size_required) do |ssp_ptr| 430 status = SERVICE_STATUS_PROCESS.new(ssp_ptr) 431 success = QueryServiceStatusEx( 432 service, 433 :SC_STATUS_PROCESS_INFO, 434 ssp_ptr, 435 size_required, 436 bytes_pointer 437 ) 438 if success == FFI::WIN32_FALSE 439 raise Puppet::Util::Windows::Error.new(_("Service query failed")) 440 end 441 yield status 442 end 443 end 444 end
@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
# File lib/puppet/util/windows/service.rb 533 def set_optional_parameter(service_name, change, value) 534 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_CHANGE_CONFIG) do |service| 535 success = ChangeServiceConfig2W( 536 service, 537 change, # dwInfoLevel 538 value, # lpInfo 539 ) 540 if success == FFI::WIN32_FALSE 541 raise Puppet::Util::windows::Error.new(_("Failed to update service %{change} configuration") % { change: change } ) 542 end 543 end 544 end
@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
# File lib/puppet/util/windows/service.rb 552 def set_startup_mode_delayed(service_name, delayed) 553 delayed_start = SERVICE_DELAYED_AUTO_START_INFO.new 554 delayed_start[:fDelayedAutostart] = delayed 555 set_optional_parameter(service_name, SERVICE_CONFIG_DELAYED_AUTO_START_INFO, delayed_start) 556 end
@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
# File lib/puppet/util/windows/service.rb 339 def transition_service_state(service_name, valid_initial_states, final_state, timeout, &block) 340 service_access = SERVICE_START | SERVICE_STOP | SERVICE_PAUSE_CONTINUE | SERVICE_QUERY_STATUS 341 open_service(service_name, SC_MANAGER_CONNECT, service_access) do |service| 342 query_status(service) do |status| 343 initial_state = status[:dwCurrentState] 344 # If the service is already in the final_state, then 345 # no further work needs to be done 346 if initial_state == final_state 347 Puppet.debug _("The service is already in the %{final_state} state. No further work needs to be done.") % { final_state: SERVICE_STATES[final_state] } 348 349 next 350 end 351 352 # Check that initial_state corresponds to a valid 353 # initial state 354 unless valid_initial_states.include?(initial_state) 355 valid_initial_states_str = valid_initial_states.map do |state| 356 SERVICE_STATES[state] 357 end.join(", ") 358 359 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] } 360 end 361 362 # Check if there's a pending transition to the final_state. If so, then wait for 363 # that transition to finish. 364 possible_pending_states = FINAL_STATES.keys.select do |pending_state| 365 # SERVICE_RUNNING has two pending states, SERVICE_START_PENDING and 366 # SERVICE_CONTINUE_PENDING. That is why we need the #select here 367 FINAL_STATES[pending_state] == final_state 368 end 369 if possible_pending_states.include?(initial_state) 370 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 } 371 wait_on_pending_state(service, initial_state, timeout) 372 373 next 374 end 375 376 # If we are in an unsafe pending state like SERVICE_START_PENDING 377 # or SERVICE_STOP_PENDING, then we want to wait for that pending 378 # transition to finish before transitioning the service state. 379 # The reason we do this is because SERVICE_START_PENDING is when 380 # the service thread is being created and initialized, while 381 # SERVICE_STOP_PENDING is when the service thread is being cleaned 382 # up and destroyed. Thus there is a chance that when the service is 383 # in either of these states, its service thread may not yet be ready 384 # to perform the state transition (it may not even exist). 385 if UNSAFE_PENDING_STATES.include?(initial_state) 386 Puppet.debug _("The service is in the %{pending_state} state, which is an unsafe pending state.") % { pending_state: SERVICE_STATES[initial_state] } 387 wait_on_pending_state(service, initial_state, timeout) 388 initial_state = FINAL_STATES[initial_state] 389 end 390 391 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] } 392 393 yield service 394 395 Puppet.debug _("Waiting for the transition to finish") 396 wait_on_state_transition(service, initial_state, final_state, timeout) 397 end 398 end 399 rescue => detail 400 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 401 end
@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
# File lib/puppet/util/windows/service.rb 678 def wait_hint_to_wait_time(wait_hint) 679 # Wait 1/10th the wait_hint, but no less than 1 and 680 # no more than 10 seconds 681 wait_time = milliseconds_to_seconds(wait_hint) / 10; 682 wait_time = 1 if wait_time < 1 683 wait_time = 10 if wait_time > 10 684 wait_time 685 end
@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
# File lib/puppet/util/windows/service.rb 629 def wait_on_pending_state(service, pending_state, timeout) 630 final_state = FINAL_STATES[pending_state] 631 632 Puppet.debug _("Waiting for the pending transition to the %{final_state} state to finish.") % { final_state: SERVICE_STATES[final_state] } 633 634 elapsed_time = 0 635 last_checkpoint = -1 636 loop do 637 query_status(service) do |status| 638 state = status[:dwCurrentState] 639 checkpoint = status[:dwCheckPoint] 640 wait_hint = status[:dwWaitHint] 641 # Check if our service has finished transitioning to 642 # the final_state OR if an unexpected transition 643 # has occurred 644 return if state == final_state 645 unless state == pending_state 646 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] } 647 end 648 649 # Check if any progress has been made since our last sleep 650 # using the dwCheckPoint. If no progress has been made then 651 # check if we've timed out, and raise an error if so 652 if checkpoint > last_checkpoint 653 elapsed_time = 0 654 last_checkpoint = checkpoint 655 else 656 wait_hint = milliseconds_to_seconds(status[:dwWaitHint]) 657 timeout = wait_hint < timeout ? timeout : wait_hint 658 659 if elapsed_time >= timeout 660 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] } 661 end 662 end 663 wait_time = wait_hint_to_wait_time(wait_hint) 664 # Wait a bit before rechecking the service's state 665 sleep(wait_time) 666 elapsed_time += wait_time 667 end 668 end 669 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
# File lib/puppet/util/windows/service.rb 581 def wait_on_state_transition(service, initial_state, final_state, timeout) 582 # Get the pending state for this transition. Note that SERVICE_RUNNING 583 # has two possible pending states, which is why we need this logic. 584 if final_state != SERVICE_RUNNING 585 pending_state = FINAL_STATES.key(final_state) 586 elsif initial_state == SERVICE_STOPPED 587 # SERVICE_STOPPED => SERVICE_RUNNING 588 pending_state = SERVICE_START_PENDING 589 else 590 # SERVICE_PAUSED => SERVICE_RUNNING 591 pending_state = SERVICE_CONTINUE_PENDING 592 end 593 594 # Wait for the transition to finish 595 state = nil 596 elapsed_time = 0 597 while elapsed_time <= timeout 598 599 query_status(service) do |status| 600 state = status[:dwCurrentState] 601 return if state == final_state 602 if state == pending_state 603 Puppet.debug _("The service transitioned to the %{pending_state} state.") % { pending_state: SERVICE_STATES[pending_state] } 604 wait_on_pending_state(service, pending_state, timeout) 605 return 606 end 607 sleep(1) 608 elapsed_time += 1 609 end 610 end 611 # Timed out while waiting for the transition to finish. Raise an error 612 # We can still use the state variable read from the FFI struct because 613 # FFI creates new Integer objects during an assignment of an integer value 614 # stored in an FFI struct. We verified that the '=' operater is safe 615 # from the freed memory since the new ruby object created during the 616 # assignment will remain in ruby memory and remain immutable and constant. 617 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] } 618 end
Private Instance Methods
Returns true if the service exists, false otherwise.
@param [String] service_name name of the service
# File lib/puppet/util/windows/service.rb 29 def exists?(service_name) 30 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |_| 31 true 32 end 33 rescue Puppet::Util::Windows::Error => e 34 return false if e.code == ERROR_SERVICE_DOES_NOT_EXIST 35 raise e 36 end
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
# File lib/puppet/util/windows/service.rb 154 def logon_account(service_name) 155 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 156 query_config(service) do |config| 157 return config[:lpServiceStartName].read_arbitrary_wide_string_up_to(Puppet::Util::Windows::ADSI::User::MAX_USERNAME_LENGTH) 158 end 159 end 160 end
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
# File lib/puppet/util/windows/service.rb 83 def resume(service_name, timeout: DEFAULT_TIMEOUT) 84 Puppet.debug _("Resuming the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 85 86 valid_initial_states = [ 87 SERVICE_PAUSE_PENDING, 88 SERVICE_PAUSED, 89 SERVICE_CONTINUE_PENDING 90 ] 91 92 transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| 93 # The SERVICE_CONTROL_CONTINUE signal can only be sent when 94 # the service is in the SERVICE_PAUSED state 95 wait_on_pending_state(service, SERVICE_PAUSE_PENDING, timeout) 96 97 send_service_control_signal(service, SERVICE_CONTROL_CONTINUE) 98 end 99 100 Puppet.debug _("Successfully resumed the %{service_name} service") % { service_name: service_name } 101 end
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
# File lib/puppet/util/windows/service.rb 127 def service_start_type(service_name) 128 start_type = nil 129 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 130 query_config(service) do |config| 131 start_type = SERVICE_START_TYPES[config[:dwStartType]] 132 end 133 end 134 # if the service has type AUTO_START, check if it's a delayed service 135 if start_type == :SERVICE_AUTO_START 136 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 137 query_config2(service, SERVICE_CONFIG_DELAYED_AUTO_START_INFO) do |config| 138 return :SERVICE_DELAYED_AUTO_START if config[:fDelayedAutostart] == 1 139 end 140 end 141 end 142 if start_type.nil? 143 raise Puppet::Error.new(_("Unknown start type '%{start_type}' for '%{service_name}'") % { start_type: start_type.to_s, service_name: service_name}) 144 end 145 start_type 146 end
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
# File lib/puppet/util/windows/service.rb 108 def service_state(service_name) 109 state = nil 110 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |service| 111 query_status(service) do |status| 112 state = SERVICE_STATES[status[:dwCurrentState]] 113 end 114 end 115 if state.nil? 116 raise Puppet::Error.new(_("Unknown Service state '%{current_state}' for '%{service_name}'") % { current_state: state.to_s, service_name: service_name}) 117 end 118 state 119 end
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 } }
# File lib/puppet/util/windows/service.rb 212 def services 213 services = {} 214 open_scm(SC_MANAGER_ENUMERATE_SERVICE) do |scm| 215 size_required = 0 216 services_returned = 0 217 FFI::MemoryPointer.new(:dword) do |bytes_pointer| 218 FFI::MemoryPointer.new(:dword) do |svcs_ret_ptr| 219 FFI::MemoryPointer.new(:dword) do |resume_ptr| 220 resume_ptr.write_dword(0) 221 # Fetch the bytes of memory required to be allocated 222 # for QueryServiceConfigW to return succesfully. This 223 # is done by sending NULL and 0 for the pointer and size 224 # respectively, letting the command fail, then reading the 225 # value of pcbBytesNeeded 226 # 227 # return value will be false from this call, since it's designed 228 # to fail. Just ignore it 229 EnumServicesStatusExW( 230 scm, 231 :SC_ENUM_PROCESS_INFO, 232 ALL_SERVICE_TYPES, 233 SERVICE_STATE_ALL, 234 FFI::Pointer::NULL, 235 0, 236 bytes_pointer, 237 svcs_ret_ptr, 238 resume_ptr, 239 FFI::Pointer::NULL 240 ) 241 size_required = bytes_pointer.read_dword 242 FFI::MemoryPointer.new(size_required) do |buffer_ptr| 243 resume_ptr.write_dword(0) 244 svcs_ret_ptr.write_dword(0) 245 success = EnumServicesStatusExW( 246 scm, 247 :SC_ENUM_PROCESS_INFO, 248 ALL_SERVICE_TYPES, 249 SERVICE_STATE_ALL, 250 buffer_ptr, 251 buffer_ptr.size, 252 bytes_pointer, 253 svcs_ret_ptr, 254 resume_ptr, 255 FFI::Pointer::NULL 256 ) 257 if success == FFI::WIN32_FALSE 258 raise Puppet::Util::Windows::Error.new(_("Failed to fetch services")) 259 end 260 # Now that the buffer is populated with services 261 # we pull the data from memory using pointer arithmetic: 262 # the number of services returned by the function is 263 # available to be read from svcs_ret_ptr, and we iterate 264 # that many times moving the cursor pointer the length of 265 # ENUM_SERVICE_STATUS_PROCESSW.size. This should iterate 266 # over the buffer and extract each struct. 267 services_returned = svcs_ret_ptr.read_dword 268 cursor_ptr = FFI::Pointer.new(ENUM_SERVICE_STATUS_PROCESSW, buffer_ptr) 269 0.upto(services_returned - 1) do |index| 270 service = ENUM_SERVICE_STATUS_PROCESSW.new(cursor_ptr[index]) 271 services[service[:lpServiceName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX)] = { 272 :display_name => service[:lpDisplayName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX), 273 :service_status_process => service[:ServiceStatusProcess] 274 } 275 end 276 end # buffer_ptr 277 end # resume_ptr 278 end # scvs_ret_ptr 279 end # bytes_ptr 280 end # open_scm 281 services 282 end
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
# File lib/puppet/util/windows/service.rb 173 def set_startup_configuration(service_name, options: {}) 174 options[:startup_type] = SERVICE_START_TYPES.key(options[:startup_type]) || SERVICE_NO_CHANGE 175 options[:logon_account] = wide_string(options[:logon_account]) || FFI::Pointer::NULL 176 options[:logon_password] = wide_string(options[:logon_password]) || FFI::Pointer::NULL 177 178 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_CHANGE_CONFIG) do |service| 179 success = ChangeServiceConfigW( 180 service, 181 SERVICE_NO_CHANGE, # dwServiceType 182 options[:startup_type], # dwStartType 183 SERVICE_NO_CHANGE, # dwErrorControl 184 FFI::Pointer::NULL, # lpBinaryPathName 185 FFI::Pointer::NULL, # lpLoadOrderGroup 186 FFI::Pointer::NULL, # lpdwTagId 187 FFI::Pointer::NULL, # lpDependencies 188 options[:logon_account], # lpServiceStartName 189 options[:logon_password], # lpPassword 190 FFI::Pointer::NULL # lpDisplayName 191 ) 192 if success == FFI::WIN32_FALSE 193 raise Puppet::Util::Windows::Error.new(_("Failed to update service configuration")) 194 end 195 end 196 197 if options[:startup_type] 198 options[:delayed] ||= false 199 set_startup_mode_delayed(service_name, options[:delayed]) 200 end 201 end
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
# File lib/puppet/util/windows/service.rb 43 def start(service_name, timeout: DEFAULT_TIMEOUT) 44 Puppet.debug _("Starting the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 45 46 valid_initial_states = [ 47 SERVICE_STOP_PENDING, 48 SERVICE_STOPPED, 49 SERVICE_START_PENDING 50 ] 51 52 transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| 53 if StartServiceW(service, 0, FFI::Pointer::NULL) == FFI::WIN32_FALSE 54 raise Puppet::Util::Windows::Error, _("Failed to start the service") 55 end 56 end 57 58 Puppet.debug _("Successfully started the %{service_name} service") % { service_name: service_name } 59 end
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
# File lib/puppet/util/windows/service.rb 66 def stop(service_name, timeout: DEFAULT_TIMEOUT) 67 Puppet.debug _("Stopping the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 68 69 valid_initial_states = SERVICE_STATES.keys - [SERVICE_STOPPED] 70 71 transition_service_state(service_name, valid_initial_states, SERVICE_STOPPED, timeout) do |service| 72 send_service_control_signal(service, SERVICE_CONTROL_STOP) 73 end 74 75 Puppet.debug _("Successfully stopped the %{service_name} service") % { service_name: service_name } 76 end