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
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 25 def exists?(service_name) 26 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |_| 27 true 28 end 29 rescue Puppet::Util::Windows::Error => e 30 return false if e.code == ERROR_SERVICE_DOES_NOT_EXIST 31 raise e 32 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 150 def logon_account(service_name) 151 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 152 query_config(service) do |config| 153 return config[:lpServiceStartName].read_arbitrary_wide_string_up_to(Puppet::Util::Windows::ADSI::User::MAX_USERNAME_LENGTH) 154 end 155 end 156 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 79 def resume(service_name, timeout: DEFAULT_TIMEOUT) 80 Puppet.debug _("Resuming the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 81 82 valid_initial_states = [ 83 SERVICE_PAUSE_PENDING, 84 SERVICE_PAUSED, 85 SERVICE_CONTINUE_PENDING 86 ] 87 88 transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| 89 # The SERVICE_CONTROL_CONTINUE signal can only be sent when 90 # the service is in the SERVICE_PAUSED state 91 wait_on_pending_state(service, SERVICE_PAUSE_PENDING, timeout) 92 93 send_service_control_signal(service, SERVICE_CONTROL_CONTINUE) 94 end 95 96 Puppet.debug _("Successfully resumed the %{service_name} service") % { service_name: service_name } 97 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 560 def send_service_control_signal(service, signal) 561 FFI::MemoryPointer.new(SERVICE_STATUS.size) do |status_ptr| 562 status = SERVICE_STATUS.new(status_ptr) 563 if ControlService(service, signal, status) == FFI::WIN32_FALSE 564 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]] } 565 end 566 end 567 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 123 def service_start_type(service_name) 124 start_type = nil 125 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 126 query_config(service) do |config| 127 start_type = SERVICE_START_TYPES[config[:dwStartType]] 128 end 129 end 130 # if the service has type AUTO_START, check if it's a delayed service 131 if start_type == :SERVICE_AUTO_START 132 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 133 query_config2(service, SERVICE_CONFIG_DELAYED_AUTO_START_INFO) do |config| 134 return :SERVICE_DELAYED_AUTO_START if config[:fDelayedAutostart] == 1 135 end 136 end 137 end 138 if start_type.nil? 139 raise Puppet::Error.new(_("Unknown start type '%{start_type}' for '%{service_name}'") % { start_type: start_type.to_s, service_name: service_name}) 140 end 141 start_type 142 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 104 def service_state(service_name) 105 state = nil 106 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |service| 107 query_status(service) do |status| 108 state = SERVICE_STATES[status[:dwCurrentState]] 109 end 110 end 111 if state.nil? 112 raise Puppet::Error.new(_("Unknown Service state '%{current_state}' for '%{service_name}'") % { current_state: state.to_s, service_name: service_name}) 113 end 114 state 115 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 208 def services 209 services = {} 210 open_scm(SC_MANAGER_ENUMERATE_SERVICE) do |scm| 211 size_required = 0 212 services_returned = 0 213 FFI::MemoryPointer.new(:dword) do |bytes_pointer| 214 FFI::MemoryPointer.new(:dword) do |svcs_ret_ptr| 215 FFI::MemoryPointer.new(:dword) do |resume_ptr| 216 resume_ptr.write_dword(0) 217 # Fetch the bytes of memory required to be allocated 218 # for QueryServiceConfigW to return succesfully. This 219 # is done by sending NULL and 0 for the pointer and size 220 # respectively, letting the command fail, then reading the 221 # value of pcbBytesNeeded 222 # 223 # return value will be false from this call, since it's designed 224 # to fail. Just ignore it 225 EnumServicesStatusExW( 226 scm, 227 :SC_ENUM_PROCESS_INFO, 228 ALL_SERVICE_TYPES, 229 SERVICE_STATE_ALL, 230 FFI::Pointer::NULL, 231 0, 232 bytes_pointer, 233 svcs_ret_ptr, 234 resume_ptr, 235 FFI::Pointer::NULL 236 ) 237 size_required = bytes_pointer.read_dword 238 FFI::MemoryPointer.new(size_required) do |buffer_ptr| 239 resume_ptr.write_dword(0) 240 svcs_ret_ptr.write_dword(0) 241 success = EnumServicesStatusExW( 242 scm, 243 :SC_ENUM_PROCESS_INFO, 244 ALL_SERVICE_TYPES, 245 SERVICE_STATE_ALL, 246 buffer_ptr, 247 buffer_ptr.size, 248 bytes_pointer, 249 svcs_ret_ptr, 250 resume_ptr, 251 FFI::Pointer::NULL 252 ) 253 if success == FFI::WIN32_FALSE 254 raise Puppet::Util::Windows::Error.new(_("Failed to fetch services")) 255 end 256 # Now that the buffer is populated with services 257 # we pull the data from memory using pointer arithmetic: 258 # the number of services returned by the function is 259 # available to be read from svcs_ret_ptr, and we iterate 260 # that many times moving the cursor pointer the length of 261 # ENUM_SERVICE_STATUS_PROCESSW.size. This should iterate 262 # over the buffer and extract each struct. 263 services_returned = svcs_ret_ptr.read_dword 264 cursor_ptr = FFI::Pointer.new(ENUM_SERVICE_STATUS_PROCESSW, buffer_ptr) 265 0.upto(services_returned - 1) do |index| 266 service = ENUM_SERVICE_STATUS_PROCESSW.new(cursor_ptr[index]) 267 services[service[:lpServiceName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX)] = { 268 :display_name => service[:lpDisplayName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX), 269 :service_status_process => service[:ServiceStatusProcess] 270 } 271 end 272 end # buffer_ptr 273 end # resume_ptr 274 end # scvs_ret_ptr 275 end # bytes_ptr 276 end # open_scm 277 services 278 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 169 def set_startup_configuration(service_name, options: {}) 170 options[:startup_type] = SERVICE_START_TYPES.key(options[:startup_type]) || SERVICE_NO_CHANGE 171 options[:logon_account] = wide_string(options[:logon_account]) || FFI::Pointer::NULL 172 options[:logon_password] = wide_string(options[:logon_password]) || FFI::Pointer::NULL 173 174 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_CHANGE_CONFIG) do |service| 175 success = ChangeServiceConfigW( 176 service, 177 SERVICE_NO_CHANGE, # dwServiceType 178 options[:startup_type], # dwStartType 179 SERVICE_NO_CHANGE, # dwErrorControl 180 FFI::Pointer::NULL, # lpBinaryPathName 181 FFI::Pointer::NULL, # lpLoadOrderGroup 182 FFI::Pointer::NULL, # lpdwTagId 183 FFI::Pointer::NULL, # lpDependencies 184 options[:logon_account], # lpServiceStartName 185 options[:logon_password], # lpPassword 186 FFI::Pointer::NULL # lpDisplayName 187 ) 188 if success == FFI::WIN32_FALSE 189 raise Puppet::Util::Windows::Error.new(_("Failed to update service configuration")) 190 end 191 end 192 193 if options[:startup_type] 194 options[:delayed] ||= false 195 set_startup_mode_delayed(service_name, options[:delayed]) 196 end 197 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 39 def start(service_name, timeout: DEFAULT_TIMEOUT) 40 Puppet.debug _("Starting the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 41 42 valid_initial_states = [ 43 SERVICE_STOP_PENDING, 44 SERVICE_STOPPED, 45 SERVICE_START_PENDING 46 ] 47 48 transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| 49 if StartServiceW(service, 0, FFI::Pointer::NULL) == FFI::WIN32_FALSE 50 raise Puppet::Util::Windows::Error, _("Failed to start the service") 51 end 52 end 53 54 Puppet.debug _("Successfully started the %{service_name} service") % { service_name: service_name } 55 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 62 def stop(service_name, timeout: DEFAULT_TIMEOUT) 63 Puppet.debug _("Stopping the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 64 65 valid_initial_states = SERVICE_STATES.keys - [SERVICE_STOPPED] 66 67 transition_service_state(service_name, valid_initial_states, SERVICE_STOPPED, timeout) do |service| 68 send_service_control_signal(service, SERVICE_CONTROL_STOP) 69 end 70 71 Puppet.debug _("Successfully stopped the %{service_name} service") % { service_name: service_name } 72 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 691 def milliseconds_to_seconds(wait_hint) 692 wait_hint / 1000; 693 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 318 def open_scm(scm_access, &block) 319 scm = OpenSCManagerW(FFI::Pointer::NULL, FFI::Pointer::NULL, scm_access) 320 raise Puppet::Util::Windows::Error.new(_("Failed to open a handle to the service control manager")) if scm == FFI::Pointer::NULL_HANDLE 321 yield scm 322 ensure 323 CloseServiceHandle(scm) 324 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 297 def open_service(service_name, scm_access, service_access, &block) 298 service = FFI::Pointer::NULL_HANDLE 299 300 result = nil 301 open_scm(scm_access) do |scm| 302 service = OpenServiceW(scm, wide_string(service_name), service_access) 303 raise Puppet::Util::Windows::Error.new(_("Failed to open a handle to the service")) if service == FFI::Pointer::NULL_HANDLE 304 result = yield service 305 end 306 307 result 308 ensure 309 CloseServiceHandle(service) 310 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 449 def query_config(service, &block) 450 config = nil 451 size_required = nil 452 # Fetch the bytes of memory required to be allocated 453 # for QueryServiceConfigW to return succesfully. This 454 # is done by sending NULL and 0 for the pointer and size 455 # respectively, letting the command fail, then reading the 456 # value of pcbBytesNeeded 457 FFI::MemoryPointer.new(:lpword) do |bytes_pointer| 458 # return value will be false from this call, since it's designed 459 # to fail. Just ignore it 460 QueryServiceConfigW(service, FFI::Pointer::NULL, 0, bytes_pointer) 461 size_required = bytes_pointer.read_dword 462 FFI::MemoryPointer.new(size_required) do |ssp_ptr| 463 config = QUERY_SERVICE_CONFIGW.new(ssp_ptr) 464 success = QueryServiceConfigW( 465 service, 466 ssp_ptr, 467 size_required, 468 bytes_pointer 469 ) 470 if success == FFI::WIN32_FALSE 471 raise Puppet::Util::Windows::Error.new(_("Service query failed")) 472 end 473 yield config 474 end 475 end 476 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 486 def query_config2(service, info_level, &block) 487 config = nil 488 size_required = nil 489 # Fetch the bytes of memory required to be allocated 490 # for QueryServiceConfig2W to return succesfully. This 491 # is done by sending NULL and 0 for the pointer and size 492 # respectively, letting the command fail, then reading the 493 # value of pcbBytesNeeded 494 FFI::MemoryPointer.new(:lpword) do |bytes_pointer| 495 # return value will be false from this call, since it's designed 496 # to fail. Just ignore it 497 QueryServiceConfig2W(service, info_level, FFI::Pointer::NULL, 0, bytes_pointer) 498 size_required = bytes_pointer.read_dword 499 FFI::MemoryPointer.new(size_required) do |ssp_ptr| 500 # We need to supply the appropriate struct to be created based on 501 # the info_level 502 case info_level 503 when SERVICE_CONFIG_DELAYED_AUTO_START_INFO 504 config = SERVICE_DELAYED_AUTO_START_INFO.new(ssp_ptr) 505 end 506 success = QueryServiceConfig2W( 507 service, 508 info_level, 509 ssp_ptr, 510 size_required, 511 bytes_pointer 512 ) 513 if success == FFI::WIN32_FALSE 514 raise Puppet::Util::Windows::Error.new(_("Service query for %{parameter_name} failed") % { parameter_name: SERVICE_CONFIG_TYPES[info_level] } ) 515 end 516 yield config 517 end 518 end 519 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 406 def query_status(service) 407 size_required = nil 408 status = nil 409 # Fetch the bytes of memory required to be allocated 410 # for QueryServiceConfigW to return succesfully. This 411 # is done by sending NULL and 0 for the pointer and size 412 # respectively, letting the command fail, then reading the 413 # value of pcbBytesNeeded 414 FFI::MemoryPointer.new(:lpword) do |bytes_pointer| 415 # return value will be false from this call, since it's designed 416 # to fail. Just ignore it 417 QueryServiceStatusEx( 418 service, 419 :SC_STATUS_PROCESS_INFO, 420 FFI::Pointer::NULL, 421 0, 422 bytes_pointer 423 ) 424 size_required = bytes_pointer.read_dword 425 FFI::MemoryPointer.new(size_required) do |ssp_ptr| 426 status = SERVICE_STATUS_PROCESS.new(ssp_ptr) 427 success = QueryServiceStatusEx( 428 service, 429 :SC_STATUS_PROCESS_INFO, 430 ssp_ptr, 431 size_required, 432 bytes_pointer 433 ) 434 if success == FFI::WIN32_FALSE 435 raise Puppet::Util::Windows::Error.new(_("Service query failed")) 436 end 437 yield status 438 end 439 end 440 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 529 def set_optional_parameter(service_name, change, value) 530 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_CHANGE_CONFIG) do |service| 531 success = ChangeServiceConfig2W( 532 service, 533 change, # dwInfoLevel 534 value, # lpInfo 535 ) 536 if success == FFI::WIN32_FALSE 537 raise Puppet::Util::windows::Error.new(_("Failed to update service %{change} configuration") % { change: change } ) 538 end 539 end 540 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 548 def set_startup_mode_delayed(service_name, delayed) 549 delayed_start = SERVICE_DELAYED_AUTO_START_INFO.new 550 delayed_start[:fDelayedAutostart] = delayed 551 set_optional_parameter(service_name, SERVICE_CONFIG_DELAYED_AUTO_START_INFO, delayed_start) 552 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 335 def transition_service_state(service_name, valid_initial_states, final_state, timeout, &block) 336 service_access = SERVICE_START | SERVICE_STOP | SERVICE_PAUSE_CONTINUE | SERVICE_QUERY_STATUS 337 open_service(service_name, SC_MANAGER_CONNECT, service_access) do |service| 338 query_status(service) do |status| 339 initial_state = status[:dwCurrentState] 340 # If the service is already in the final_state, then 341 # no further work needs to be done 342 if initial_state == final_state 343 Puppet.debug _("The service is already in the %{final_state} state. No further work needs to be done.") % { final_state: SERVICE_STATES[final_state] } 344 345 next 346 end 347 348 # Check that initial_state corresponds to a valid 349 # initial state 350 unless valid_initial_states.include?(initial_state) 351 valid_initial_states_str = valid_initial_states.map do |state| 352 SERVICE_STATES[state] 353 end.join(", ") 354 355 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] } 356 end 357 358 # Check if there's a pending transition to the final_state. If so, then wait for 359 # that transition to finish. 360 possible_pending_states = FINAL_STATES.keys.select do |pending_state| 361 # SERVICE_RUNNING has two pending states, SERVICE_START_PENDING and 362 # SERVICE_CONTINUE_PENDING. That is why we need the #select here 363 FINAL_STATES[pending_state] == final_state 364 end 365 if possible_pending_states.include?(initial_state) 366 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 } 367 wait_on_pending_state(service, initial_state, timeout) 368 369 next 370 end 371 372 # If we are in an unsafe pending state like SERVICE_START_PENDING 373 # or SERVICE_STOP_PENDING, then we want to wait for that pending 374 # transition to finish before transitioning the service state. 375 # The reason we do this is because SERVICE_START_PENDING is when 376 # the service thread is being created and initialized, while 377 # SERVICE_STOP_PENDING is when the service thread is being cleaned 378 # up and destroyed. Thus there is a chance that when the service is 379 # in either of these states, its service thread may not yet be ready 380 # to perform the state transition (it may not even exist). 381 if UNSAFE_PENDING_STATES.include?(initial_state) 382 Puppet.debug _("The service is in the %{pending_state} state, which is an unsafe pending state.") % { pending_state: SERVICE_STATES[initial_state] } 383 wait_on_pending_state(service, initial_state, timeout) 384 initial_state = FINAL_STATES[initial_state] 385 end 386 387 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] } 388 389 yield service 390 391 Puppet.debug _("Waiting for the transition to finish") 392 wait_on_state_transition(service, initial_state, final_state, timeout) 393 end 394 end 395 rescue => detail 396 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 397 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 674 def wait_hint_to_wait_time(wait_hint) 675 # Wait 1/10th the wait_hint, but no less than 1 and 676 # no more than 10 seconds 677 wait_time = milliseconds_to_seconds(wait_hint) / 10; 678 wait_time = 1 if wait_time < 1 679 wait_time = 10 if wait_time > 10 680 wait_time 681 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 625 def wait_on_pending_state(service, pending_state, timeout) 626 final_state = FINAL_STATES[pending_state] 627 628 Puppet.debug _("Waiting for the pending transition to the %{final_state} state to finish.") % { final_state: SERVICE_STATES[final_state] } 629 630 elapsed_time = 0 631 last_checkpoint = -1 632 loop do 633 query_status(service) do |status| 634 state = status[:dwCurrentState] 635 checkpoint = status[:dwCheckPoint] 636 wait_hint = status[:dwWaitHint] 637 # Check if our service has finished transitioning to 638 # the final_state OR if an unexpected transition 639 # has occurred 640 return if state == final_state 641 unless state == pending_state 642 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] } 643 end 644 645 # Check if any progress has been made since our last sleep 646 # using the dwCheckPoint. If no progress has been made then 647 # check if we've timed out, and raise an error if so 648 if checkpoint > last_checkpoint 649 elapsed_time = 0 650 last_checkpoint = checkpoint 651 else 652 wait_hint = milliseconds_to_seconds(status[:dwWaitHint]) 653 timeout = wait_hint < timeout ? timeout : wait_hint 654 655 if elapsed_time >= timeout 656 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] } 657 end 658 end 659 wait_time = wait_hint_to_wait_time(wait_hint) 660 # Wait a bit before rechecking the service's state 661 sleep(wait_time) 662 elapsed_time += wait_time 663 end 664 end 665 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 577 def wait_on_state_transition(service, initial_state, final_state, timeout) 578 # Get the pending state for this transition. Note that SERVICE_RUNNING 579 # has two possible pending states, which is why we need this logic. 580 if final_state != SERVICE_RUNNING 581 pending_state = FINAL_STATES.key(final_state) 582 elsif initial_state == SERVICE_STOPPED 583 # SERVICE_STOPPED => SERVICE_RUNNING 584 pending_state = SERVICE_START_PENDING 585 else 586 # SERVICE_PAUSED => SERVICE_RUNNING 587 pending_state = SERVICE_CONTINUE_PENDING 588 end 589 590 # Wait for the transition to finish 591 state = nil 592 elapsed_time = 0 593 while elapsed_time <= timeout 594 595 query_status(service) do |status| 596 state = status[:dwCurrentState] 597 return if state == final_state 598 if state == pending_state 599 Puppet.debug _("The service transitioned to the %{pending_state} state.") % { pending_state: SERVICE_STATES[pending_state] } 600 wait_on_pending_state(service, pending_state, timeout) 601 return 602 end 603 sleep(1) 604 elapsed_time += 1 605 end 606 end 607 # Timed out while waiting for the transition to finish. Raise an error 608 # We can still use the state variable read from the FFI struct because 609 # FFI creates new Integer objects during an assignment of an integer value 610 # stored in an FFI struct. We verified that the '=' operater is safe 611 # from the freed memory since the new ruby object created during the 612 # assignment will remain in ruby memory and remain immutable and constant. 613 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] } 614 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 25 def exists?(service_name) 26 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |_| 27 true 28 end 29 rescue Puppet::Util::Windows::Error => e 30 return false if e.code == ERROR_SERVICE_DOES_NOT_EXIST 31 raise e 32 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 150 def logon_account(service_name) 151 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 152 query_config(service) do |config| 153 return config[:lpServiceStartName].read_arbitrary_wide_string_up_to(Puppet::Util::Windows::ADSI::User::MAX_USERNAME_LENGTH) 154 end 155 end 156 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 79 def resume(service_name, timeout: DEFAULT_TIMEOUT) 80 Puppet.debug _("Resuming the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 81 82 valid_initial_states = [ 83 SERVICE_PAUSE_PENDING, 84 SERVICE_PAUSED, 85 SERVICE_CONTINUE_PENDING 86 ] 87 88 transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| 89 # The SERVICE_CONTROL_CONTINUE signal can only be sent when 90 # the service is in the SERVICE_PAUSED state 91 wait_on_pending_state(service, SERVICE_PAUSE_PENDING, timeout) 92 93 send_service_control_signal(service, SERVICE_CONTROL_CONTINUE) 94 end 95 96 Puppet.debug _("Successfully resumed the %{service_name} service") % { service_name: service_name } 97 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 123 def service_start_type(service_name) 124 start_type = nil 125 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 126 query_config(service) do |config| 127 start_type = SERVICE_START_TYPES[config[:dwStartType]] 128 end 129 end 130 # if the service has type AUTO_START, check if it's a delayed service 131 if start_type == :SERVICE_AUTO_START 132 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_CONFIG) do |service| 133 query_config2(service, SERVICE_CONFIG_DELAYED_AUTO_START_INFO) do |config| 134 return :SERVICE_DELAYED_AUTO_START if config[:fDelayedAutostart] == 1 135 end 136 end 137 end 138 if start_type.nil? 139 raise Puppet::Error.new(_("Unknown start type '%{start_type}' for '%{service_name}'") % { start_type: start_type.to_s, service_name: service_name}) 140 end 141 start_type 142 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 104 def service_state(service_name) 105 state = nil 106 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_QUERY_STATUS) do |service| 107 query_status(service) do |status| 108 state = SERVICE_STATES[status[:dwCurrentState]] 109 end 110 end 111 if state.nil? 112 raise Puppet::Error.new(_("Unknown Service state '%{current_state}' for '%{service_name}'") % { current_state: state.to_s, service_name: service_name}) 113 end 114 state 115 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 208 def services 209 services = {} 210 open_scm(SC_MANAGER_ENUMERATE_SERVICE) do |scm| 211 size_required = 0 212 services_returned = 0 213 FFI::MemoryPointer.new(:dword) do |bytes_pointer| 214 FFI::MemoryPointer.new(:dword) do |svcs_ret_ptr| 215 FFI::MemoryPointer.new(:dword) do |resume_ptr| 216 resume_ptr.write_dword(0) 217 # Fetch the bytes of memory required to be allocated 218 # for QueryServiceConfigW to return succesfully. This 219 # is done by sending NULL and 0 for the pointer and size 220 # respectively, letting the command fail, then reading the 221 # value of pcbBytesNeeded 222 # 223 # return value will be false from this call, since it's designed 224 # to fail. Just ignore it 225 EnumServicesStatusExW( 226 scm, 227 :SC_ENUM_PROCESS_INFO, 228 ALL_SERVICE_TYPES, 229 SERVICE_STATE_ALL, 230 FFI::Pointer::NULL, 231 0, 232 bytes_pointer, 233 svcs_ret_ptr, 234 resume_ptr, 235 FFI::Pointer::NULL 236 ) 237 size_required = bytes_pointer.read_dword 238 FFI::MemoryPointer.new(size_required) do |buffer_ptr| 239 resume_ptr.write_dword(0) 240 svcs_ret_ptr.write_dword(0) 241 success = EnumServicesStatusExW( 242 scm, 243 :SC_ENUM_PROCESS_INFO, 244 ALL_SERVICE_TYPES, 245 SERVICE_STATE_ALL, 246 buffer_ptr, 247 buffer_ptr.size, 248 bytes_pointer, 249 svcs_ret_ptr, 250 resume_ptr, 251 FFI::Pointer::NULL 252 ) 253 if success == FFI::WIN32_FALSE 254 raise Puppet::Util::Windows::Error.new(_("Failed to fetch services")) 255 end 256 # Now that the buffer is populated with services 257 # we pull the data from memory using pointer arithmetic: 258 # the number of services returned by the function is 259 # available to be read from svcs_ret_ptr, and we iterate 260 # that many times moving the cursor pointer the length of 261 # ENUM_SERVICE_STATUS_PROCESSW.size. This should iterate 262 # over the buffer and extract each struct. 263 services_returned = svcs_ret_ptr.read_dword 264 cursor_ptr = FFI::Pointer.new(ENUM_SERVICE_STATUS_PROCESSW, buffer_ptr) 265 0.upto(services_returned - 1) do |index| 266 service = ENUM_SERVICE_STATUS_PROCESSW.new(cursor_ptr[index]) 267 services[service[:lpServiceName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX)] = { 268 :display_name => service[:lpDisplayName].read_arbitrary_wide_string_up_to(SERVICENAME_MAX), 269 :service_status_process => service[:ServiceStatusProcess] 270 } 271 end 272 end # buffer_ptr 273 end # resume_ptr 274 end # scvs_ret_ptr 275 end # bytes_ptr 276 end # open_scm 277 services 278 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 169 def set_startup_configuration(service_name, options: {}) 170 options[:startup_type] = SERVICE_START_TYPES.key(options[:startup_type]) || SERVICE_NO_CHANGE 171 options[:logon_account] = wide_string(options[:logon_account]) || FFI::Pointer::NULL 172 options[:logon_password] = wide_string(options[:logon_password]) || FFI::Pointer::NULL 173 174 open_service(service_name, SC_MANAGER_CONNECT, SERVICE_CHANGE_CONFIG) do |service| 175 success = ChangeServiceConfigW( 176 service, 177 SERVICE_NO_CHANGE, # dwServiceType 178 options[:startup_type], # dwStartType 179 SERVICE_NO_CHANGE, # dwErrorControl 180 FFI::Pointer::NULL, # lpBinaryPathName 181 FFI::Pointer::NULL, # lpLoadOrderGroup 182 FFI::Pointer::NULL, # lpdwTagId 183 FFI::Pointer::NULL, # lpDependencies 184 options[:logon_account], # lpServiceStartName 185 options[:logon_password], # lpPassword 186 FFI::Pointer::NULL # lpDisplayName 187 ) 188 if success == FFI::WIN32_FALSE 189 raise Puppet::Util::Windows::Error.new(_("Failed to update service configuration")) 190 end 191 end 192 193 if options[:startup_type] 194 options[:delayed] ||= false 195 set_startup_mode_delayed(service_name, options[:delayed]) 196 end 197 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 39 def start(service_name, timeout: DEFAULT_TIMEOUT) 40 Puppet.debug _("Starting the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 41 42 valid_initial_states = [ 43 SERVICE_STOP_PENDING, 44 SERVICE_STOPPED, 45 SERVICE_START_PENDING 46 ] 47 48 transition_service_state(service_name, valid_initial_states, SERVICE_RUNNING, timeout) do |service| 49 if StartServiceW(service, 0, FFI::Pointer::NULL) == FFI::WIN32_FALSE 50 raise Puppet::Util::Windows::Error, _("Failed to start the service") 51 end 52 end 53 54 Puppet.debug _("Successfully started the %{service_name} service") % { service_name: service_name } 55 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 62 def stop(service_name, timeout: DEFAULT_TIMEOUT) 63 Puppet.debug _("Stopping the %{service_name} service. Timeout set to: %{timeout} seconds") % { service_name: service_name, timeout: timeout } 64 65 valid_initial_states = SERVICE_STATES.keys - [SERVICE_STOPPED] 66 67 transition_service_state(service_name, valid_initial_states, SERVICE_STOPPED, timeout) do |service| 68 send_service_control_signal(service, SERVICE_CONTROL_STOP) 69 end 70 71 Puppet.debug _("Successfully stopped the %{service_name} service") % { service_name: service_name } 72 end