See WIP.md for the cross-package maintenance guide. Sister packages: WinEventLogLib, WinNamedPipesLib.
A thin OS-services wrapper. The [PredeclaredId] singleton Services is the entry point; ServiceManager carries per-service configuration; ServiceCreator(Of T) is the generic factory; ServiceState is a read-only snapshot. The Win32 API wrappers, the Private Module ServicesConstants half of the constants file, the ServiceControlHandlerCallback_Trampoline helper, and the IServiceCreator / IServiceManagerInternal private interfaces are plumbing and get no doc page. The user-facing enums live in Public Module ServicesConstantsPublic.
Public user-facing surface (three concrete classes + one generic class + one interface + four enums):
| Symbol | Kind | Role |
|---|---|---|
Services |
[PredeclaredId] Class |
The singleton coordinator: ConfigureNew, RunServiceDispatcher, InstallAll, UninstallAll, LaunchService, ControlService, QueryStateOfService, GetConfiguredService, _NewEnum. Used as Services.X without New. |
ServiceManager |
Class | Per-service configuration + runtime status reporting. Returned by Services.ConfigureNew(). |
ServiceCreator(Of T) |
Generic class | The dispatcher's factory: T must implement ITbService; CreateInstance returns New T. |
ServiceState |
Class | Read-only state snapshot. Constructor (called via Services.QueryStateOfService(Name)) queries the SCM. |
ITbService |
Public Interface | The contract every service class implements: EntryPoint, StartupFailed, ChangeState. |
ServiceTypeConstants |
Enum | tbServiceTypeOwnProcess, tbServiceTypeShareProcess, etc. |
ServiceStartConstants |
Enum | tbServiceStartAuto, tbServiceStartOnDemand, etc. |
ServiceControlCodeConstants |
Enum | vbServiceControlStop, vbServiceControlPause, vbServiceControlContinue, etc. |
ServiceStatusConstants |
Enum | vbServiceStatusRunning, vbServiceStatusStartPending, vbServiceStatusStopped, etc. |
The two private interfaces (IServiceCreator, IServiceManagerInternal) are pure implementation detail — same situation as WinNamedPipesLib's INamedPipe*Internal interfaces. No doc page, and don't surface the underscored implementing members on the concrete classes either.
The two Private Module declarations (ServicesAPIs, ServicesConstants) and the Private Module ServicesHelper are all internal — no doc page.
[PredeclaredId] Class. The compiler instantiates a singleton named Services at program start; user code calls Services.X directly without New. The class also doubles as an enumerable collection of the ServiceManager instances that have been configured (For Each manager In Services).
Public methods:
Function ConfigureNew() As ServiceManager— "Use this method to configure a service. Usually used during app startup." Allocates a newServiceManager, adds it to the internal collection, returns it. Typical use:With Services.ConfigureNew : .Name = "MyService" : .InstanceCreator = New ServiceCreator(Of MyServiceClass) : End With.Sub RunServiceDispatcher()— "This method hands over to the OS for managing the starting/stopping of services via the main thread. This is a BLOCKING call, until the OS wants to shutdown the service EXE." Builds aSERVICE_TABLE_ENTRYWfrom every configuredServiceManagerand callsStartServiceCtrlDispatcherW. Returns only when the OS terminates the service host. Raises run-time error 5 if the dispatcher cannot start (typically when the EXE was launched normally rather than by the SCM).Sub InstallAll()— "This method tries to register ALL of the configured services onto the system." Iterates the configuredServiceManagers and calls.Install()on each. Requires admin.Sub UninstallAll()— "This method tries to unregister ALL of the configured services off the system." Iterates and calls.Uninstall()on each. Requires admin.Function QueryStateOfService(ByVal ServiceName As String) As ServiceState— returns a freshServiceStatesnapshot. Raises run-time error 5 if the service isn't installed.Sub LaunchService(ByVal ServiceName As String, ParamArray LaunchArgs())— start an installed service by name, optionally passing launch arguments through to itsServiceManager.LaunchArgs()field. WrapsOpenServiceW(SERVICE_START)+StartServiceW. Raises run-time error 5 on permission / not-installed / already-running.Sub ControlService(ByVal ServiceName As String, ByVal ControlCode As ServiceControlCodeConstants)— send an SCM control code to a running service. The required SCM permission is derived from the control code automatically (SERVICE_STOPforvbServiceControlStop,SERVICE_PAUSE_CONTINUEfor the pause / continue / netbind / paramchange family,SERVICE_INTERROGATEforvbServiceControlInterrogate,SERVICE_USER_DEFINED_CONTROLfor codes 128–255,SERVICE_ALL_ACCESSotherwise). ForvbServiceControlStopthe wrapper fillsSERVICE_CONTROL_STATUS_REASON_PARAMSWwithSERVICE_STOP_REASON_FLAG_PLANNED | MAJOR_NONE | MINOR_NONE— there is aFIXMEto allow customising the reason code.
Public properties:
Property Get GetConfiguredService(ByVal Name As String) As ServiceManager— look up a previously-configuredServiceManagerby itsName. Raises run-time error 5 if not found. (Despite theGetsyntax the lookup is parameterised by name; it's a property in name only.)
Public enumerator:
Property Get _NewEnum() As Variant—[Enumerator]-tagged; enablesFor Each manager In Servicesover the configuredServiceManagers. "Provides For-Each support for the services collection, exposing each configured service as a ServiceManager instance."
[COMCreatable(False)]. User code never instantiates this directly — Services.ConfigureNew() returns it. The source-side constructor carries [Description("For internal use. Dont create instances of ServiceManager manually, use Services.ConfigureNew instead")] — surface that on the page intro.
Public field (one):
LaunchArgs() As String— populated byServiceEntryPointfrom theargvthe SCM hands over.LaunchArgs(0)is the first user-supplied argument (the SCM-supplied service name atargv[0]is dropped). The example uses it to gate startup:If Join(ServiceManager.LaunchArgs) <> "MySecretPassword" Then ….
Public properties (each carries a [Description("...")]):
InstanceCreator As IServiceCreator(Get / Let / Set) — "Set this to an instance of the ServiceCreator class to allow the OS to launch the instance of your service." Typically.InstanceCreator = New ServiceCreator(Of MyServiceClass).Name As String(Get / Let) — "The name of the service, as listed in the OS services database."Description As String(Get / Let) — "The description of the service, as listed in the OS services database." Applied viaChangeServiceConfig2W(SERVICE_CONFIG_DESCRIPTION)on every successfulInstall().Type As ServiceTypeConstants(Get / Let) — "The type of the service, typicallytbServiceTypeOwnProcessortbServiceTypeShareProcess." Defaults totbServiceTypeOwnProcess.InstallStartMode As ServiceStartConstants(Get / Let) — "The start-mode of the service, typicallytbServiceStartOnDemandortbServiceStartAuto." Defaults totbServiceStartOnDemand.InstallCmdLine As String(Get / Let) — "The command line arguments passed to the service EXE when the OS launches the service." Defaults to"""<App.ModulePath>""". Usually overridden to add a discriminator argument like-startServiceso the EXE knows whether it was launched by the SCM (run dispatcher) or by a user (show UI). Example:.InstallCmdLine = """" & App.ModulePath & """ -startService".DependentServices() As Variant(Get / Let) — "A list of dependent services that this service requires to be started before this service is launched (dependent services are auto-launched by the OS)." Pass anArray("OtherSvc1", "OtherSvc2"). The setter stashes it;Install()packs it into a double-null-terminated string and hands it toCreateServiceW.AutoInitializeCOM As Boolean(Get / Let) — "When TRUE, COM will be initialized for you on the new service thread in STA mode." Defaults toTrue. Set toFalseif your service needs a different apartment model (callCoInitializeExyourself fromEntryPoint).SupportsPausing As Boolean(Get / Let) — "When TRUE, the SCM will sendSERVICE_CONTROL_PAUSE/SERVICE_CONTROL_CONTINUEnotifications." Defaults toFalse. The setter callsResyncStatus()so toggling it mid-run takes effect immediately. (Most services set this toTrueonce insideEntryPointand then handlevbServiceControlPause/vbServiceControlContinueinChangeState.)
Public methods:
Sub Install()— "This method attempts to install the configured service on the system." Opens the SCM withSC_MANAGER_CONNECT Or SC_MANAGER_CREATE_SERVICE, callsCreateServiceW. If the service already exists, deletes it (viaOpenServiceW(SERVICE_DELETE)+DeleteService) and retries the create — soInstall()is effectively re-entrant / safe to call multiple times. On successful create, sets the description viaChangeServiceConfig2W. Raises run-time error 5 on permissions failure or unrecoverable create failure. Requires admin elevation.Sub Uninstall()— "This method attempts to uninstall the configured service on the system." Opens the SCM, opens the service withSERVICE_DELETE, callsDeleteService. Raises run-time error 5 if the service isn't registered or on permissions failure. Requires admin elevation.Sub ReportStatus(ByVal dwCurrentState As ServiceStatusConstants, Optional ByVal dwWin32ExitCode As Long = ERRORCODE_NO_ERROR, Optional ByVal dwWaitHint As Long = 0)— "This method informs the OS of the current state of the service." The user'sEntryPointis required to callReportStatus(vbServiceStatusRunning)once steady-state is reached andReportStatus(vbServiceStatusStopped)once shut-down completes; long start-up sequences should also callReportStatus(vbServiceStatusStartPending, , <waitHint_ms>)periodically to keep the SCM from killing the service. ThedwControlsAcceptedfield ofSERVICE_STATUSis filled automatically from the state and fromSupportsPausing(Stop is always accepted except duringStartPending; Pause/Continue is gated onSupportsPausing). ThedwCheckPointfield auto-increments for pending states and resets onRunning/Stopped.Sub ResyncStatus()— re-applies the cachedSERVICE_STATUSto the SCM viaSetServiceStatus. Called automatically fromReportStatusand from theSupportsPausingsetter. User code rarely needs to call this directly; mention it for completeness.
The class also carries two methods that are technically Public-by-default (no modifier) but are invoked only by the OS dispatcher / the package's own trampoline — ServiceEntryPoint(ByVal dwArgc As Long, ByVal lpszArgv As LongPtr) and ServiceControlHandlerCallback(ByVal dwControl As Long, ByVal dwEventType As Long, ByVal lpEventData As LongPtr). Do not list these as user-facing methods; mention them at the very end of the page under "Internal hooks" with a > [!NOTE] saying the OS / package infrastructure invokes them and user code never calls them.
Generic class. [COMCreatable(False)]. [Description("This class allows the service manager to create an instance of a particular service on-demand as needed")] is the source intro. Tagged with the EA magic-byte [ClassId("66170220-FEF3-4257-8FBA-EAEAEAEAEAEA")] — same compiler-special-handling treatment as WinEventLogLib's EventLog(Of T1, T2). Do not surface the ClassId on the page.
Type parameter constraint: T must implement ITbService. There is no syntactic Where T : ITbService constraint expressed in the source, but Function CreateInstance() As ITbService returning New T only compiles when T implements ITbService — flag this as the practical constraint on the page.
Public method:
Function CreateInstance() As ITbService—Implements IServiceCreator.CreateInstance. ReturnsNew T. Called once per service start by the package's dispatcher trampoline. User code never calls this directly; the typical usage is.InstanceCreator = New ServiceCreator(Of MyServiceClass)on a freshly-allocatedServiceManager.
The page should be small (the surface is one method) and largely focused on explaining the Of T parameterisation + the T : ITbService constraint + how it slots into ServiceManager.InstanceCreator.
[COMCreatable(False)]. Returned by Services.QueryStateOfService(Name). The constructor takes the service name, opens the SCM with SC_MANAGER_CONNECT, opens the service with SERVICE_QUERY_STATUS, calls QueryServiceStatusEx(SC_STATUS_PROCESS_INFO, ...), and snapshots a SERVICE_STATUS_PROCESS struct. The snapshot is taken once at construction time and never refreshed — to see updated state, call Services.QueryStateOfService again.
The constructor raises run-time error 5 with descriptive messages on three failure modes: SCM open failed ("Unable to open the Service manager..."), service not installed ("Service '' is not installed on this system"), status query failed ("Unable to query the service state").
Public properties (all read-only Get):
Type As ServiceTypeConstants— the SCM-reported service type.CurrentState As Long— the SCM-reported state, but typedLongrather thanServiceStatusConstants. Source carries a' FIXMEcomment — surface as a> [!NOTE]that this returns the underlyingLongvalue (which happens to match theServiceStatusConstantsenum values), and that callers wanting type-safety canCType(state.CurrentState, ServiceStatusConstants).CurrentStateText As String— human-readable text:"RUNNING","STOPPED","STARTING","STOPPING","PAUSED","PAUSING","CONTINUING","UNKNOWN STATE (<n>)".ControlsAccepted As Long— bitmask ofSERVICE_ACCEPT_*flags. Source carries a' FIXMEcomment — surface the same way asCurrentState.ExitCode As Long— thedwWin32ExitCodefield. The Win32 documented sentinelERROR_SERVICE_SPECIFIC_ERROR(1066) means "seeServiceSpecificExitCode".ServiceSpecificExitCode As Long— the service-defined exit code whenExitCode = ERROR_SERVICE_SPECIFIC_ERROR. Otherwise meaningless.CheckPoint As Long— thedwCheckPointfield; increments while the service is in a pending state and resets at steady state.WaitHint As Long— thedwWaitHintmilliseconds field.ProcessId As Long— the OS process ID hosting the service (0 if not running).Flags As Long— thedwServiceFlagsfield (currentlySERVICE_RUNS_IN_SYSTEM_PROCESS = 1is the only documented bit).
Public Interface. Tagged [InterfaceId("5F137E12-5164-452E-911A-6FD9BF20EC81")]. Description: "All services must implement ITbService." The contract is three subs:
Sub EntryPoint(ByVal ServiceContext As ServiceManager)— the main service body. Called by the package's dispatcher trampoline once the SCM has finished start-up handshaking. Runs on the service thread (a separate thread from the dispatcher). Inside this sub the implementor:- Optionally validates startup conditions (e.g. checks
ServiceContext.LaunchArgs). - Calls
ServiceContext.ReportStatus(vbServiceStatusRunning)once steady-state is reached (the dispatcher trampoline reportsvbServiceStatusStartPendingautomatically before callingEntryPoint). - Runs the long-running work loop. For pipe-server services this is the
NamedPipeServer.ManualMessageLoopEnter()blocking call; for other services it might be aDo While IsStopping = Falseloop with a wait primitive. - Calls
ServiceContext.ReportStatus(vbServiceStatusStopped)before returning.
- Optionally validates startup conditions (e.g. checks
Sub StartupFailed(ByVal ServiceContext As ServiceManager)— called ifRegisterServiceCtrlHandlerExWfailed (the control handler couldn't be hooked, e.g. the service was launched outside the SCM context). Typical implementation: log a failure event. Don't try toReportStatusfrom here — the status handle is invalid.Sub ChangeState(ByVal ServiceContext As ServiceManager, ByVal dwControl As ServiceControlCodeConstants, ByVal dwEventType As Long, ByVal lpEventData As LongPtr)— the control-code dispatcher. Runs on the main (dispatcher) thread, not on the service thread. Typical pattern:Select Case dwControlovervbServiceControlStop/vbServiceControlShutdown/vbServiceControlPause/vbServiceControlContinue, set sharedPublicflags (IsStopping,IsPaused), callServiceContext.ReportStatusto acknowledge the transition, signal the service thread to react (e.g.NamedPipeServer.ManualMessageLoopLeave()). ThedwEventType+lpEventDataparameters carry the event-specific payload for the codes that need it (SERVICE_CONTROL_DEVICEEVENT,SERVICE_CONTROL_POWEREVENT,SERVICE_CONTROL_SESSIONCHANGE,SERVICE_CONTROL_HARDWAREPROFILECHANGE— see Microsoft'sHandlerExdocumentation for the data layouts).
The two-thread split is the single most important fact about the interface — every page entry should reinforce it. The example uses Public IsPaused As Boolean + Public IsStopping As Boolean shared fields on the service class to ferry state between the two threads, which is the documented pattern.
Public enums (in Public Module ServicesConstantsPublic), one page each under docs/Reference/WinServicesLib/Enumerations/:
ServiceTypeConstants—tbServiceTypeAdapter,tbServiceTypeSystemDriver,tbServiceTypeKernelDriver,tbServiceTypeRecognizerDriver,tbServiceTypeOwnProcess,tbServiceTypeShareProcess,tbServiceTypeOwnProcessInteractive,tbServiceTypeShareProcessInteractive. The driver values (tbServiceTypeSystemDriver,tbServiceTypeKernelDriver,tbServiceTypeRecognizerDriver,tbServiceTypeAdapter) are only meaningful when registering a kernel-mode driver — twinBASIC services compile to a user-mode EXE and should usetbServiceTypeOwnProcess(one service per EXE) ortbServiceTypeShareProcess(multiple services hosted in one EXE; the example uses this). TheInteractivevariants are kept for compatibility but Windows Vista and later disallow them; flag with a> [!NOTE].ServiceStartConstants—tbServiceStartAuto,tbServiceStartBoot,tbServiceStartOnDemand,tbServiceStartDisabled,tbServiceStartDriverSystem.tbServiceStartBootandtbServiceStartDriverSystemonly apply to kernel drivers.ServiceControlCodeConstants— 18 values mirroring the Win32SERVICE_CONTROL_*constants. Source-side prefix isvbServiceControl*(carried over from VB6 — note the prefix isvb, nottb, in this enum; surface as-is, don't try to rationalise).ServiceStatusConstants—vbServiceStatusStopped,vbServiceStatusStartPending,vbServiceStatusStopPending,vbServiceStatusRunning,vbServiceStatusContinuePending,vbServiceStatusPausePending,vbServiceStatusPaused. Samevbprefix.
Format pages like WebView2/Enumerations/wv2PrintOrientation.md — single intro paragraph, a value table with {: #vbServiceXxx } anchors per row for deep-linking.
The package's index.md walks the reader through: (1) what a Windows service is; (2) the lifecycle — configure (Services.ConfigureNew) → install (elevated) → run (Services.RunServiceDispatcher blocks the main thread, SCM launches the service thread on demand); (3) the two-thread split between EntryPoint and ChangeState; (4) integration cross-links to WinEventLogLib (composition-delegation idiom) and WinNamedPipesLib (the ManualMessageLoopEnter / Leave service-hosting idiom).