Module: Puppet::Util::Windows::Service

Extended by:
FFI::Library, String
Defined in:
lib/puppet/util/windows/service.rb

Overview

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

Defined Under Namespace

Classes: ENUM_SERVICE_STATUS_PROCESSW, QUERY_SERVICE_CONFIGW, SERVICE_DELAYED_AUTO_START_INFO, SERVICE_STATUS, SERVICE_STATUS_PROCESS

Constant Summary collapse

FILE =
Puppet::Util::Windows::File
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

30
ERROR_SERVICE_DOES_NOT_EXIST =
0x00000424
SERVICE_CONTROL_STOP =
0x00000001
SERVICE_CONTROL_PAUSE =
0x00000002
SERVICE_CONTROL_CONTINUE =
0x00000003
SERVICE_CONTROL_INTERROGATE =
0x00000004
SERVICE_CONTROL_SHUTDOWN =
0x00000005
SERVICE_CONTROL_PARAMCHANGE =
0x00000006
SERVICE_CONTROL_NETBINDADD =
0x00000007
SERVICE_CONTROL_NETBINDREMOVE =
0x00000008
SERVICE_CONTROL_NETBINDENABLE =
0x00000009
SERVICE_CONTROL_NETBINDDISABLE =
0x0000000A
SERVICE_CONTROL_DEVICEEVENT =
0x0000000B
SERVICE_CONTROL_HARDWAREPROFILECHANGE =
0x0000000C
SERVICE_CONTROL_POWEREVENT =
0x0000000D
SERVICE_CONTROL_SESSIONCHANGE =
0x0000000E
SERVICE_CONTROL_PRESHUTDOWN =
0x0000000F
SERVICE_CONTROL_TIMECHANGE =
0x00000010
SERVICE_CONTROL_TRIGGEREVENT =
0x00000020
SERVICE_CONTROL_SIGNALS =
{
  SERVICE_CONTROL_STOP                  => :SERVICE_CONTROL_STOP,
  SERVICE_CONTROL_PAUSE                 => :SERVICE_CONTROL_PAUSE,
  SERVICE_CONTROL_CONTINUE              => :SERVICE_CONTROL_CONTINUE,
  SERVICE_CONTROL_INTERROGATE           => :SERVICE_CONTROL_INTERROGATE,
  SERVICE_CONTROL_SHUTDOWN              => :SERVICE_CONTROL_SHUTDOWN,
  SERVICE_CONTROL_PARAMCHANGE           => :SERVICE_CONTROL_PARAMCHANGE,
  SERVICE_CONTROL_NETBINDADD            => :SERVICE_CONTROL_NETBINDADD,
  SERVICE_CONTROL_NETBINDREMOVE         => :SERVICE_CONTROL_NETBINDREMOVE,
  SERVICE_CONTROL_NETBINDENABLE         => :SERVICE_CONTROL_NETBINDENABLE,
  SERVICE_CONTROL_NETBINDDISABLE        => :SERVICE_CONTROL_NETBINDDISABLE,
  SERVICE_CONTROL_DEVICEEVENT           => :SERVICE_CONTROL_DEVICEEVENT,
  SERVICE_CONTROL_HARDWAREPROFILECHANGE => :SERVICE_CONTROL_HARDWAREPROFILECHANGE,
  SERVICE_CONTROL_POWEREVENT            => :SERVICE_CONTROL_POWEREVENT,
  SERVICE_CONTROL_SESSIONCHANGE         => :SERVICE_CONTROL_SESSIONCHANGE,
  SERVICE_CONTROL_PRESHUTDOWN           => :SERVICE_CONTROL_PRESHUTDOWN,
  SERVICE_CONTROL_TIMECHANGE            => :SERVICE_CONTROL_TIMECHANGE,
  SERVICE_CONTROL_TRIGGEREVENT          => :SERVICE_CONTROL_TRIGGEREVENT
}
SERVICE_AUTO_START =
0x00000002
SERVICE_BOOT_START =
0x00000000
SERVICE_DEMAND_START =
0x00000003
SERVICE_DISABLED =
0x00000004
SERVICE_SYSTEM_START =
0x00000001
SERVICE_START_TYPES =
{
  SERVICE_AUTO_START => :SERVICE_AUTO_START,
  SERVICE_BOOT_START => :SERVICE_BOOT_START,
  SERVICE_DEMAND_START => :SERVICE_DEMAND_START,
  SERVICE_DISABLED => :SERVICE_DISABLED,
  SERVICE_SYSTEM_START => :SERVICE_SYSTEM_START,
}
SERVICE_FILE_SYSTEM_DRIVER =
0x00000002
SERVICE_KERNEL_DRIVER =
0x00000001
SERVICE_WIN32_OWN_PROCESS =
0x00000010
SERVICE_WIN32_SHARE_PROCESS =
0x00000020
SERVICE_USER_OWN_PROCESS =
0x00000050
SERVICE_USER_SHARE_PROCESS =
0x00000060
SERVICE_INTERACTIVE_PROCESS =

Available only if service is also SERVICE_WIN32_OWN_PROCESS or SERVICE_WIN32_SHARE_PROCESS

0x00000100
ALL_SERVICE_TYPES =
SERVICE_FILE_SYSTEM_DRIVER |
SERVICE_KERNEL_DRIVER |
SERVICE_WIN32_OWN_PROCESS |
SERVICE_WIN32_SHARE_PROCESS
SERVICE_CONTINUE_PENDING =
0x00000005
SERVICE_PAUSE_PENDING =
0x00000006
SERVICE_PAUSED =
0x00000007
SERVICE_RUNNING =
0x00000004
SERVICE_START_PENDING =
0x00000002
SERVICE_STOP_PENDING =
0x00000003
SERVICE_STOPPED =
0x00000001
UNSAFE_PENDING_STATES =
[SERVICE_START_PENDING, SERVICE_STOP_PENDING]
FINAL_STATES =
{
  SERVICE_CONTINUE_PENDING => SERVICE_RUNNING,
  SERVICE_PAUSE_PENDING    => SERVICE_PAUSED,
  SERVICE_START_PENDING    => SERVICE_RUNNING,
  SERVICE_STOP_PENDING     => SERVICE_STOPPED
}
SERVICE_STATES =
{
  SERVICE_CONTINUE_PENDING => :SERVICE_CONTINUE_PENDING,
  SERVICE_PAUSE_PENDING => :SERVICE_PAUSE_PENDING,
  SERVICE_PAUSED => :SERVICE_PAUSED,
  SERVICE_RUNNING => :SERVICE_RUNNING,
  SERVICE_START_PENDING => :SERVICE_START_PENDING,
  SERVICE_STOP_PENDING => :SERVICE_STOP_PENDING,
  SERVICE_STOPPED => :SERVICE_STOPPED,
}
SERVICE_ACCEPT_STOP =
0x00000001
SERVICE_ACCEPT_PAUSE_CONTINUE =
0x00000002
SERVICE_ACCEPT_SHUTDOWN =
0x00000004
SERVICE_ACCEPT_PARAMCHANGE =
0x00000008
SERVICE_ACCEPT_NETBINDCHANGE =
0x00000010
SERVICE_ACCEPT_HARDWAREPROFILECHANGE =
0x00000020
SERVICE_ACCEPT_POWEREVENT =
0x00000040
SERVICE_ACCEPT_SESSIONCHANGE =
0x00000080
SERVICE_ACCEPT_PRESHUTDOWN =
0x00000100
SERVICE_ACCEPT_TIMECHANGE =
0x00000200
SERVICE_ACCEPT_TRIGGEREVENT =
0x00000400
SERVICE_ACCEPT_USER_LOGOFF =
0x00000800
SC_MANAGER_CREATE_SERVICE =
0x00000002
SC_MANAGER_CONNECT =
0x00000001
SC_MANAGER_ENUMERATE_SERVICE =
0x00000004
SC_MANAGER_LOCK =
0x00000008
SC_MANAGER_MODIFY_BOOT_CONFIG =
0x00000020
SC_MANAGER_QUERY_LOCK_STATUS =
0x00000010
SC_MANAGER_ALL_ACCESS =
FILE::STANDARD_RIGHTS_REQUIRED |
SC_MANAGER_CREATE_SERVICE      |
SC_MANAGER_CONNECT             |
SC_MANAGER_ENUMERATE_SERVICE   |
SC_MANAGER_LOCK                |
SC_MANAGER_MODIFY_BOOT_CONFIG  |
SC_MANAGER_QUERY_LOCK_STATUS
SERVICE_CHANGE_CONFIG =
0x0002
SERVICE_ENUMERATE_DEPENDENTS =
0x0008
SERVICE_INTERROGATE =
0x0080
SERVICE_PAUSE_CONTINUE =
0x0040
SERVICE_QUERY_STATUS =
0x0004
SERVICE_QUERY_CONFIG =
0x0001
SERVICE_START =
0x0010
SERVICE_STOP =
0x0020
SERVICE_USER_DEFINED_CONTROL =
0x0100
SERVICE_ALL_ACCESS =
FILE::STANDARD_RIGHTS_REQUIRED |
SERVICE_CHANGE_CONFIG          |
SERVICE_ENUMERATE_DEPENDENTS   |
SERVICE_INTERROGATE            |
SERVICE_PAUSE_CONTINUE         |
SERVICE_QUERY_STATUS           |
SERVICE_QUERY_CONFIG           |
SERVICE_START                  |
SERVICE_STOP                   |
SERVICE_USER_DEFINED_CONTROL
SERVICE_CONFIG_DESCRIPTION =

Service config codes From the windows 10 SDK: // // Value to indicate no change to an optional parameter // #define SERVICE_NO_CHANGE 0xffffffff docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-changeserviceconfig2w

0x00000001
SERVICE_CONFIG_FAILURE_ACTIONS =
0x00000002
SERVICE_CONFIG_DELAYED_AUTO_START_INFO =
0x00000003
SERVICE_CONFIG_FAILURE_ACTIONS_FLAG =
0x00000004
SERVICE_CONFIG_SERVICE_SID_INFO =
0x00000005
SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO =
0x00000006
SERVICE_CONFIG_PRESHUTDOWN_INFO =
0x00000007
SERVICE_CONFIG_TRIGGER_INFO =
0x00000008
SERVICE_CONFIG_PREFERRED_NODE =
0x00000009
SERVICE_CONFIG_LAUNCH_PROTECTED =
0x00000012
SERVICE_NO_CHANGE =
0xffffffff
SERVICE_CONFIG_TYPES =
{
  SERVICE_CONFIG_DESCRIPTION => :SERVICE_CONFIG_DESCRIPTION,
  SERVICE_CONFIG_FAILURE_ACTIONS => :SERVICE_CONFIG_FAILURE_ACTIONS,
  SERVICE_CONFIG_DELAYED_AUTO_START_INFO => :SERVICE_CONFIG_DELAYED_AUTO_START_INFO,
  SERVICE_CONFIG_FAILURE_ACTIONS_FLAG => :SERVICE_CONFIG_FAILURE_ACTIONS_FLAG,
  SERVICE_CONFIG_SERVICE_SID_INFO => :SERVICE_CONFIG_SERVICE_SID_INFO,
  SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO => :SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO,
  SERVICE_CONFIG_PRESHUTDOWN_INFO => :SERVICE_CONFIG_PRESHUTDOWN_INFO,
  SERVICE_CONFIG_TRIGGER_INFO => :SERVICE_CONFIG_TRIGGER_INFO,
  SERVICE_CONFIG_PREFERRED_NODE => :SERVICE_CONFIG_PREFERRED_NODE,
  SERVICE_CONFIG_LAUNCH_PROTECTED => :SERVICE_CONFIG_LAUNCH_PROTECTED,
}
SERVICE_ACTIVE =
0x00000001
SERVICE_INACTIVE =
0x00000002
SERVICE_STATE_ALL =
SERVICE_ACTIVE |
SERVICE_INACTIVE
SERVICENAME_MAX =
256
SC_STATUS_TYPE =

docs.microsoft.com/en-us/windows/desktop/api/winsvc/nf-winsvc-queryservicestatusex BOOL QueryServiceStatusEx(

SC_HANDLE      hService,
SC_STATUS_TYPE InfoLevel,
LPBYTE         lpBuffer,
DWORD          cbBufSize,
LPDWORD        pcbBytesNeeded

);

enum(
  :SC_STATUS_PROCESS_INFO, 0,
)
SC_ENUM_TYPE =

docs.microsoft.com/en-us/windows/desktop/api/winsvc/nf-winsvc-enumservicesstatusexw BOOL EnumServicesStatusExW(

SC_HANDLE    hSCManager,
SC_ENUM_TYPE InfoLevel,
DWORD        dwServiceType,
DWORD        dwServiceState,
LPBYTE       lpServices,
DWORD        cbBufSize,
LPDWORD      pcbBytesNeeded,
LPDWORD      lpServicesReturned,
LPDWORD      lpResumeHandle,
LPCWSTR      pszGroupName

);

enum(
  :SC_ENUM_PROCESS_INFO, 0,
)

Class Method Summary collapse

Methods included from String

wide_string

Methods included from FFI::Library

attach_function_private

Class Method Details

.exists?(service_name) ⇒ Boolean

Returns true if the service exists, false otherwise.


323
324
325
326
327
328
329
330
# File 'lib/puppet/util/windows/service.rb', line 323

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

.milliseconds_to_seconds(wait_hint) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

process the wait hint listed by a service to something usable by ruby sleep


972
973
974
# File 'lib/puppet/util/windows/service.rb', line 972

def milliseconds_to_seconds(wait_hint)
  wait_hint / 1000;
end

.open_scm(scm_access, &block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Opens a handle to the service control manager


599
600
601
602
603
604
605
# File 'lib/puppet/util/windows/service.rb', line 599

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

.open_service(service_name, scm_access, service_access) {|service| ... } ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Yield Parameters:

  • service (:handle)

    the windows native handle used to access the service


578
579
580
581
582
583
584
585
586
587
588
589
590
591
# File 'lib/puppet/util/windows/service.rb', line 578

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

.query_config(service, &block) ⇒ QUERY_SERVICE_CONFIGW struct

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

perform QueryServiceConfigW on a windows service and return the result


730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
# File 'lib/puppet/util/windows/service.rb', line 730

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

.query_config2(service, info_level, &block) ⇒ QUERY_SERVICE_CONFIG2W struct

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

perform QueryServiceConfig2W on a windows service and return the result


767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
# File 'lib/puppet/util/windows/service.rb', line 767

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

.query_status(service) ⇒ SERVICE_STATUS_PROCESS struct

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

perform QueryServiceStatusEx on a windows service and return the result


687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
# File 'lib/puppet/util/windows/service.rb', line 687

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

.resume(service_name, timeout: DEFAULT_TIMEOUT) ⇒ Object

Resume a paused windows service


377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/puppet/util/windows/service.rb', line 377

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

.send_service_control_signal(service, signal) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Sends a service control signal to a service


841
842
843
844
845
846
847
848
# File 'lib/puppet/util/windows/service.rb', line 841

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

.service_start_type(service_name) ⇒ QUERY_SERVICE_CONFIGW.struct

Query the configuration of a service using QueryServiceConfigW or QueryServiceConfig2W


421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/puppet/util/windows/service.rb', line 421

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

.service_state(service_name) ⇒ string

Query the state of a service using QueryServiceStatusEx


402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/puppet/util/windows/service.rb', line 402

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

.servicesHash

enumerate over all services in all states and return them as a hash


489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/puppet/util/windows/service.rb', line 489

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

.set_optional_parameter(service_name, change, value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Sets an optional parameter on a service by calling ChangeServiceConfig2W


810
811
812
813
814
815
816
817
818
819
820
821
# File 'lib/puppet/util/windows/service.rb', line 810

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

.set_startup_mode(service_name, startup_type, delayed = false) ⇒ Object

Change the startup mode of a windows service


450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/puppet/util/windows/service.rb', line 450

def set_startup_mode(service_name, startup_type, delayed=false)
  startup_code = SERVICE_START_TYPES.key(startup_type)
  if startup_code.nil?
    raise Puppet::Error.new(_("Unknown start type %{start_type}") % {startup_type: startup_type.to_s})
  end
  open_service(service_name, SC_MANAGER_CONNECT, SERVICE_CHANGE_CONFIG) do |service|
    # Currently the only thing puppet's API can really manage
    # in this list is dwStartType (the third param). Thus no
    # generic function was written to make use of all the params
    # since the API as-is couldn't use them anyway
    success = ChangeServiceConfigW(
      service,
      SERVICE_NO_CHANGE,  # dwServiceType
      startup_code,       # dwStartType
      SERVICE_NO_CHANGE,  # dwErrorControl
      FFI::Pointer::NULL, # lpBinaryPathName
      FFI::Pointer::NULL, # lpLoadOrderGroup
      FFI::Pointer::NULL, # lpdwTagId
      FFI::Pointer::NULL, # lpDependencies
      FFI::Pointer::NULL, # lpServiceStartName
      FFI::Pointer::NULL, # lpPassword
      FFI::Pointer::NULL  # lpDisplayName
    )
    if success == FFI::WIN32_FALSE
      raise Puppet::Util::Windows::Error.new(_("Failed to update service configuration"))
    end
  end
  set_startup_mode_delayed(service_name, delayed)
end

.set_startup_mode_delayed(service_name, delayed) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Controls the delayed auto-start setting of a service


829
830
831
832
833
# File 'lib/puppet/util/windows/service.rb', line 829

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

.start(service_name, timeout: DEFAULT_TIMEOUT) ⇒ Object

Start a windows service


337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/puppet/util/windows/service.rb', line 337

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

.stop(service_name, timeout: DEFAULT_TIMEOUT) ⇒ Object

Stop a windows service


360
361
362
363
364
365
366
367
368
369
370
# File 'lib/puppet/util/windows/service.rb', line 360

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

.transition_service_state(service_name, valid_initial_states, final_state, timeout, &block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Transition the service to the specified state. The block should perform the actual transition.


616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
# File 'lib/puppet/util/windows/service.rb', line 616

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

.wait_hint_to_wait_time(wait_hint) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

create a usable wait time to wait between querying the service.


955
956
957
958
959
960
961
962
# File 'lib/puppet/util/windows/service.rb', line 955

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

.wait_on_pending_state(service, pending_state, timeout) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Waits for a service to finish transitioning from a pending state. The service must be in the pending state before invoking this routine.


906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
# File 'lib/puppet/util/windows/service.rb', line 906

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

.wait_on_state_transition(service, initial_state, final_state, timeout) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Waits for a service to transition from one state to another state.

Raises:


858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
# File 'lib/puppet/util/windows/service.rb', line 858

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