From 802d77af9748ca585075465925d36f8db8b0d41f Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 13 May 2026 23:15:49 +0200 Subject: [PATCH 1/3] Update the WIP. --- WIP.md | 377 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 372 insertions(+), 5 deletions(-) diff --git a/WIP.md b/WIP.md index 70e86d6..adfcd6f 100644 --- a/WIP.md +++ b/WIP.md @@ -4,7 +4,7 @@ Jekyll site (`just-the-docs` theme) deploying to `docs.twinbasic.com`. Source un ## Status -Initial reference documentation is **complete**. All nine packages have full reference coverage adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, `.twin` source for the twinBASIC-specific packages); the CEF and WebView2 packages also carry a tutorial set. +Reference documentation is **in progress**. Nine packages have full reference coverage adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, `.twin` source for the twinBASIC-specific packages); the CEF and WebView2 packages also carry a tutorial set. The WinServicesLib package is being added now. | Package | Reference | Tutorials | |--------------------------------------|-----------|-----------| @@ -17,6 +17,7 @@ Initial reference documentation is **complete**. All nine packages have full ref | cefPackage (CEF) | done | done | | WinEventLogLib | done | — | | WinNamedPipesLib | done | — | +| WinServicesLib | **WIP** | — | The rest of this file is the maintenance guide for adding new pages or updating existing ones — primary-source paths, page templates, cross-section linking conventions, the per-symbol workflow, and the integrity check. @@ -34,6 +35,7 @@ When working from a primary source: always read it first — **never paraphrase - `docs/Reference/CEF/` — CEF (Chromium Embedded Framework) package: the **CefBrowser** control, its `EnvironmentOptions` sub-page, and the two user-facing enumerations (`CefLogSeverity`, `cefPrintOrientation`). This is a much smaller surface than WebView2 — the package is currently BETA and many WebView2-equivalent features are not yet exposed. - `docs/Reference/WinEventLogLib/` — Windows Event Log package: the generic `EventLog(Of T1, T2)` class and the `EventLogHelperPublic` module with its single `RegisterEventLogInternal` helper. Three pages total — `index.md`, `EventLog.md`, `EventLogHelperPublic.md`. - `docs/Reference/WinNamedPipesLib/` — Windows Named Pipes package: the IOCP-based async pipe framework — `NamedPipeServer` + `NamedPipeServerConnection` on the server side, `NamedPipeClientManager` + `NamedPipeClientConnection` on the client side. Five pages total (`index.md` + one per class). +- `docs/Reference/WinServicesLib/` — Windows Services package: a thin OS-services wrapper. `Services` (predeclared singleton) coordinates one or more `ServiceManager` configurations; `ServiceCreator(Of T)` is the generic factory the dispatcher uses to instantiate each user-defined `ITbService` class; `ServiceState` is a read-only state snapshot for an installed service. Four public enums (`ServiceTypeConstants`, `ServiceStartConstants`, `ServiceControlCodeConstants`, `ServiceStatusConstants`) live under `Enumerations/`. - `docs/Reference/Statements.md` — alphabetical index of language statements. - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions. - `docs/_includes/footer_custom.html` — overrides the theme's footer slot; renders the copyright line and, when `vba_attribution: true` is set in a page's frontmatter, an additional CC-BY-4.0 attribution line beneath it. @@ -67,6 +69,7 @@ All of twinbasic's package sources are at: ..\tb-export\NewProject\Packages\cefPackages\Sources\ ..\tb-export\NewProject\Packages\WinEventLogLib\Sources\ ..\tb-export\NewProject\Packages\WinNamedPipesLib\Sources\ +..\tb-export\NewProject\Packages\WinServicesLib\Sources\ etc. ``` @@ -76,6 +79,20 @@ For the CEF package, the examples live in a different folder: ..\tbrepro\cef\CEFSampleProject\Sources\ ← four worked examples + MainForm ``` +For the WinServicesLib package — and the canonical integration story across **all three** "winlibs" packages (services + event log + named pipes wired together end-to-end) — the worked example lives at: + +``` +..\tbrepro\winlibs\tbServiceTest2\Sources\ + Startup.twin ← Sub Main: configures two services + dispatches + SERVICES\TBSERVICE001.twin, TBSERVICE002.twin ← user-implemented ITbService classes + FORMS\MainForm.twin ← non-service mode: control-panel UI + FORMS\InProcessNamedPipeServerForm.twin ← in-process pipe server (no service) + MISC\MESSAGETABLE.twin ← [PopulateFrom("json",...)] enums for the event log +..\tbrepro\winlibs\tbServiceTest2\Resources\MESSAGETABLE\Strings.json ← message-table backing JSON +``` + +Read this project end-to-end before extending the docs for any of WinServicesLib, WinEventLogLib, or WinNamedPipesLib — the three packages share a load-bearing set of idioms (composition-delegation on `EventLog(Of T1, T2)`, the manual-message-loop pattern coupling `NamedPipeServer` to a service's `ChangeState` handler, `PropertyBag` as the canonical pipe payload) that only become visible when you see them used together. + ### VB Controls The `STANDARD/` folder holds the leaf control classes. The `BASE/` folder defines the inheritance chain (e.g. `BaseControlWindowlessNoFocus` → `BaseControlRectDockable` → `BaseControlRect` → `BaseControl`); read those alongside the leaf class to know which `Public` members are actually visible. Members marked `Protected` or hidden behind `[Unimplemented]` should be flagged with a `> [!NOTE]` callout. @@ -439,6 +456,75 @@ Class-level decoration on `EventLog`: `[COMCreatable(False)]`, `[ClassId("4AEA12 - `Register()` requires elevation. Normal usage is to call it once during install (from an elevated installer), then call `LogSuccess` / `LogFailure` at runtime without elevation. - The package is described in `_README.txt` with a copy-pasted "NAMED PIPES PACKAGE" header (clearly an unintentional carry-over from another sister package); the body correctly says *"A simple framework for creating Windows event log entries from twinBASIC"*. Use the body, not the header. +#### Canonical usage idiom — composition-delegation onto a service class + +`tbServiceTest2` (`..\tbrepro\winlibs\tbServiceTest2\Sources\`) shows the package's intended usage pattern, which is *not* obvious from the bare API: + +```tb +Class TBSERVICE001 + Implements ITbService + Implements EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES) Via _ + EventLog = New EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES)("Application\" & CurrentComponentName) + … + LogSuccess(service_started, status_changed, CurrentComponentName) ' surfaces directly +End Class +``` + +The `Implements Via = ` form is twinBASIC's composition-delegation syntax (see [`docs/Features/Language/Delegation.md`](docs/Features/Language/Delegation.md) if/once that page exists, or the [`CustomControls` mixin pattern](docs/Reference/CustomControls/index.md) for an analogous use). The class declares it `Implements EventLog(Of …)` and gives the compiler a private field plus a constructor expression; the compiler then auto-forwards every `Public` member of `EventLog` (`LogSuccess`, `LogFailure`, `Register`) through that field. The result: a service class that *contains* an `EventLog` instance and exposes its logging methods as if they were its own. + +Surface this on the `EventLog` page (and on the package index) as the **recommended pattern** for service / long-running classes. Spell out: + +- The constructor expression evaluates *once* (the first time the delegating class is instantiated, per twinBASIC's `Implements ... Via` semantics). +- The `T1` / `T2` type arguments must be identical at the `Implements` declaration and the constructor (the compiler enforces this). +- The `LogPath` is typically `"Application\" & CurrentComponentName` — `CurrentComponentName` is the compile-time class name, so the log path automatically tracks renames. +- The delegating class transparently inherits all three of `LogSuccess` / `LogFailure` / `Register`. Calling code can use them unqualified. + +#### Message-table backing: `[PopulateFrom("json", …)]` on the enums + +The `T1` / `T2` enums are typically auto-populated from a JSON resource via the `[PopulateFrom]` attribute. `tbServiceTest2`'s `Sources\MISC\MESSAGETABLE.twin`: + +```tb +Module MESSAGETABLE + [PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "events", "name", "id")] + Enum EVENTS + End Enum + + [PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "categories", "name", "id")] + Enum CATEGORIES + End Enum +End Module +``` + +…with `Resources\MESSAGETABLE\Strings.json`: + +```json +{ + "events": [ + { "id": -1073610751, "name": "service_started", "LCID_0000": "%1 service started" }, + { "id": -1073610750, "name": "service_startup_failed", "LCID_0000": "%1 service startup failed" }, + … + ], + "categories": [ + { "id": 1, "name": "status_changed", "LCID_0000": "Status Changed" } + ] +} +``` + +Two things are happening here: + +1. **The enum bodies are populated at compile time** — `Enum EVENTS` starts empty in the source, but after compilation it has members `service_started = -1073610751`, `service_startup_failed = -1073610750`, … (one per `"events"` entry in the JSON, keyed `name → id`). +2. **The same JSON is consumed by the compiler's `mc.exe`-equivalent** that emits the message-table resource into `App.ModulePath`. The `LCID_0000` strings are the message-table entries, and the `%1`, `%2`, … placeholders are filled at log time from the `AdditionalStrings` `ParamArray` to `LogSuccess` / `LogFailure`. The `CategoryCount` registry value (written by `Register()`) is the highest declared `id` in the `categories` block, which is what `GetDeclaredMaxEnumValue(Of T2)` recovers at compile time. + +So the round-trip is: JSON → compile-time enum population + message-table resource emission → registry entries that point Windows at the EXE → runtime `LogSuccess(EventId, CategoryId, …)` writes an event the Event Viewer can format using the embedded message-table strings. + +Surface this on the index page (under "Setting up message resources" or similar) with the JSON skeleton and the cross-reference to `[PopulateFrom]` (which is documented under `docs/Features/`, not in the reference set — link to that page if it exists, otherwise describe in-place). + +The negative event-ID values in the JSON (`-1073610751`) are the standard Win32 event-ID encoding: the high bits encode severity (`0xC0000000` = Error), facility (`0x...`), and customer bit. Don't unpack this on the docs; just note that *"event IDs follow the Win32 documented encoding — see Microsoft's 'Event Identifiers' reference"*. + +#### Why `T1` / `T2` and not separate `EventIds` / `Categories` classes + +A class can only `Implements EventLog(Of T1, T2) Via …` *once*. If a service needs events from multiple unrelated message tables, it can compose multiple `EventLog` instances **as named fields** (no `Via`), accepting a small loss of ergonomics (calls become `MyEventLog.LogSuccess(…)` instead of `LogSuccess(…)`). Surface this as a one-line note on the index — most services share a single `MESSAGETABLE` module across all their classes (as the example does), so the limitation rarely bites. + **Layout decision** — three pages total, mirroring the small-package approach used by Assert: - `docs/Reference/WinEventLogLib/index.md` — landing page: intro, lifecycle (define enums → instantiate → Register once → LogSuccess / LogFailure), gaps and quirks, the class and module lists. @@ -586,6 +672,82 @@ Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[ClassId(...)]`, `[EventI **Hidden message window.** Each `NamedPipeServer` and `NamedPipeClientManager` instance creates an invisible `STATIC`-class window with a subclassed `WndProc`, used to marshal IOCP-thread completions back to the UI thread when `FreeThreadingEvents = False`. Mention this on each class's intro paragraph — it explains why the consumer's process must be pumping a message loop for the default event-delivery semantics to work, and why `ManualMessageLoopEnter` / `ManualMessageLoopLeave` exist on `NamedPipeServer` for service / console hosts. +#### Canonical service-host idiom — `ManualMessageLoopEnter` paired with `ChangeState` + +`tbServiceTest2`'s `Sources\SERVICES\TBSERVICE001.twin` shows the standard pattern for a Windows service that hosts a `NamedPipeServer`. Surface this on the `NamedPipeServer.md` page (under a "Hosting inside a Windows service" sub-heading) and on the index landing: + +```tb +' On the service thread (ITbService.EntryPoint): +Set NamedPipeServer = New NamedPipeServer +NamedPipeServer.PipeName = "WaynesPipe_" & CurrentComponentName +ServiceManager.ReportStatus(vbServiceStatusRunning) + +NamedPipeServer.Start() +NamedPipeServer.ManualMessageLoopEnter() ' blocks until ManualMessageLoopLeave +NamedPipeServer.Stop() + +ServiceManager.ReportStatus(vbServiceStatusStopped) + +' On the dispatcher thread (ITbService.ChangeState): +Select Case dwControl + Case vbServiceControlStop, vbServiceControlShutdown + ServiceManager.ReportStatus(vbServiceStatusStopPending) + NamedPipeServer.ManualMessageLoopLeave() ' wakes the service thread +End Select +``` + +Key facts that aren't obvious from the per-method `[Description]`s: + +- The service-thread `EntryPoint` and the dispatcher-thread `ChangeState` are **different threads**. The `NamedPipeServer` member field is shared between them; the dispatcher-thread `ChangeState` calls `ManualMessageLoopLeave` on it to wake the service thread out of `ManualMessageLoopEnter`. +- `ManualMessageLoopLeave` is the **only** way to wake `ManualMessageLoopEnter` cleanly. There is no timeout, no second blocking primitive. If the service needs to react to other wake-up sources (paused state, custom control codes), it sets a shared flag *then* calls `ManualMessageLoopLeave` to break out, inspects the flag, and decides whether to re-enter the loop or proceed to shutdown. The `TBSERVICE002` variant in the same example demonstrates this with `IsPaused` / `IsStopping` shared `Public` fields and a `While IsStopping = False` outer loop. +- Pause / continue support uses the same pattern: `ChangeState` flips `IsPaused = True` and calls `ManualMessageLoopLeave`; the service thread sees the flag, reports `vbServiceStatusPaused`, enters a `Do While IsPaused : Sleep(500) : Loop`, then re-enters `ManualMessageLoopEnter` once `Continue` flips the flag back. +- `FreeThreadingEvents = False` (the default) is **required** for this pattern — events are marshalled to whichever thread is currently inside `ManualMessageLoopEnter`. Setting `FreeThreadingEvents = True` would deliver events on the IOCP worker thread instead and bypass the manual loop entirely (advanced; not the documented service idiom). + +The non-service equivalent — hosting the same `NamedPipeServer` inside a Form — is in `Sources\FORMS\InProcessNamedPipeServerForm.twin`: the Form's regular message loop pumps the marshalling window automatically, so the Form just calls `Server.Start()` in `Form_Load` and `Server.Stop` in `Form_Unload` without ever touching `ManualMessageLoopEnter` / `Leave`. Cross-reference both patterns on the `NamedPipeServer.md` page so the reader sees the choice point. + +#### PropertyBag as the canonical message carrier + +Every example serialises structured payloads through the pipe as a `PropertyBag.Contents` `Byte()`: + +```tb +' Sender: +Dim propertyBag As New PropertyBag +propertyBag.WriteProperty("CommandID", "WHAT_TIME_IS_IT") +propertyBag.WriteProperty("Data", payload) +SelectedNamedPipe.AsyncWrite propertyBag.Contents + +' Receiver (inside MessageReceived event): +Dim propertyBag As New PropertyBag +propertyBag.Contents = Data ' deep-copies the bytes; safe past the event handler +Dim commandID As String = propertyBag.ReadProperty("CommandID") +… +``` + +Two reasons this pattern matters and should be surfaced on the docs: + +1. **The transient-`Data()` problem is solved by `PropertyBag`.** Assigning to `PropertyBag.Contents` deep-copies the byte buffer; once the assignment returns, the original IOCP buffer can be recycled without invalidating the data. This is the cleanest answer to *"how do I keep the data past the event handler?"* — call out on every `MessageReceived` / `ClientMessageReceived` page entry as the recommended capture mechanism. +2. **`PropertyBag` provides typed multi-field payloads** without the consumer having to design a wire protocol. Both sides agree on the property names (`"CommandID"`, `"ResponseCommandID"`, `"ResponseData"`, `"Data"`) and `PropertyBag` handles the encoding / decoding. Cross-link [`PropertyBag` reference](docs/Reference/VBRUN/PropertyBag/index.md) from the index landing. + +Surface as the **recommended** carrier; nothing in the package mandates it, raw `Byte()` works too, but every worked example uses `PropertyBag` and the integration story reads much more cleanly with it. + +#### Discovery loop — `FindNamedPipes` + +`tbServiceTest2`'s `MainForm` shows the canonical client-side discovery pattern: a low-frequency `Timer` (the form uses `timerRefreshNamedPipes` with a multi-second interval) that calls `NamedPipeClients.FindNamedPipes("WaynesPipe_*")`, repopulates a `ListBox`, and preserves the user's current selection: + +```tb +For Each namePipeName In NamePipeClients.FindNamedPipes("WaynesPipe_*") + If namePipeName = NamedPipeSelected Then namedPipeSelectedIndex = Index + lstNamedPipes.AddItem(namePipeName) + Index += 1 +Next +``` + +Surface on the `NamedPipeClientManager.md` page (under the `FindNamedPipes` entry) as the recommended polling loop — the underlying `FindFirstFileW("\\.\pipe\…")` call is cheap enough to invoke every few seconds without measurable cost, and pipes appear / disappear too quickly for any event-driven discovery to be reliable. Don't claim there's no faster API; just say *"polling is the documented approach"*. + +#### Service-side broadcast + +`InProcessNamedPipeServerForm.twin` demonstrates `Server.AsyncBroadcast("BROADCAST")` (string coerced to `Byte()` via twinBASIC's implicit `String → Byte()` conversion). Useful when the same server has multiple concurrent connections and wants to push an update to all of them — the alternative is iterating over a user-maintained list of `NamedPipeServerConnection`s and calling `AsyncWrite` on each. The package handles the iteration internally. Mention on the `NamedPipeServer.AsyncBroadcast` entry. + **Layout decision** — five pages total, one per public class plus the landing page: ``` @@ -608,6 +770,181 @@ All four class pages are single-file (no folder-style — no natural sub-pages; **License:** MIT (copyright Wayne Phillips T/A iTech Masters, 2025; first release v0.1, 04-FEB-2025) — same situation as WebView2Package, Assert, CustomControls, CEF, and WinEventLogLib. Pages are fully original content; **omit** the `vba_attribution: true` flag. +### WinServicesLib + +Layout of `..\tb-export\NewProject\Packages\WinServicesLib\Sources\` is flat — eight `.twin` files plus three text files (`_README.txt`, `_LICENCE.txt`, `_RELEASE_HISTORY.txt`): + +- `APIs.twin` — `Private Module ServicesAPIs` wrapping fourteen `advapi32.dll` / `kernel32.dll` / `ole32.dll` / `oleaut32.dll` entry points (`StartServiceCtrlDispatcherW`, `OpenSCManagerW`, `CreateServiceW`, `RegisterServiceCtrlHandlerExW`, `SetServiceStatus`, `OpenServiceW`, `DeleteService`, `CloseServiceHandle`, `QueryServiceStatusEx`, `StartServiceW`, `ControlServiceExW`, `ChangeServiceConfig2W`, `CoInitializeEx`, `SysAllocStringPtr`) plus the supporting `Type` declarations (`SERVICE_STATUS`, `SERVICE_STATUS_PROCESS`, `SERVICE_CONTROL_STATUS_REASON_PARAMSW`, `SERVICE_CONFIG_DESCRIPTION`). No doc page. +- `Constants.twin` — two modules. `Private Module ServicesConstants` carries the Win32 access-flag constants (`SC_MANAGER_*`, `SERVICE_*` permission bits, `SERVICE_CONTROL_*` control codes, `SERVICE_ACCEPT_*` accepted-controls flags, etc.) plus the `SC_STATUS_TYPE` enum. **Public** module `ServicesConstantsPublic` carries the four user-facing enumerations (`ServiceTypeConstants`, `ServiceStartConstants`, `ServiceControlCodeConstants`, `ServiceStatusConstants`). The private module is internal; the public module's enums surface and need their own doc pages. +- `Helper.twin` — `Private Module ServicesHelper` with the IOCP-style trampoline (`ServiceControlHandlerCallback_Trampoline`) used in place of class-`AddressOf` plus a `VariantArrayToStringArray` helper. No doc page. +- `Interfaces.twin` — three interfaces: `Public Interface ITbService` (user-implemented), `Private Interface IServiceCreator` (internal — the public `ServiceCreator(Of T)` class implements it), `Private Interface IServiceManagerInternal` (internal). Only `ITbService` gets a doc page. +- `Services.twin` — the predeclared `Class Services` (no `Public`/`Private` modifier — `Class` defaults to public; tagged `[PredeclaredId]`, so it's used singleton-style as `Services.ConfigureNew`). The package's main entry point. +- `ServiceManager.twin` — `Public Class ServiceManager`. Per-service configuration + runtime status reporting. `[COMCreatable(False)]`. User code never instantiates this directly — it's returned by `Services.ConfigureNew()`. +- `ServiceCreator.twin` — `Public Class ServiceCreator(Of T)`. Generic factory `T → New T As ITbService`. `[COMCreatable(False)]`. Has the EA magic-byte `ClassId("66170220-FEF3-4257-8FBA-EAEAEAEAEAEA")` pattern, same compiler-special-handling as `WinEventLogLib`'s `EventLog(Of T1, T2)`. +- `ServiceState.twin` — `Class ServiceState` (no modifier — public by default). Read-only state snapshot for an installed service. `[COMCreatable(False)]`. Returned by `Services.QueryStateOfService`. + +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**. + +#### `Services` public members + +`[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 new `ServiceManager`, 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 a `SERVICE_TABLE_ENTRYW` from every configured `ServiceManager` and calls `StartServiceCtrlDispatcherW`. 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 configured `ServiceManager`s 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 fresh `ServiceState` snapshot. 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 its `ServiceManager.LaunchArgs()` field. Wraps `OpenServiceW(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_STOP` for `vbServiceControlStop`, `SERVICE_PAUSE_CONTINUE` for the pause / continue / netbind / paramchange family, `SERVICE_INTERROGATE` for `vbServiceControlInterrogate`, `SERVICE_USER_DEFINED_CONTROL` for codes 128–255, `SERVICE_ALL_ACCESS` otherwise). For `vbServiceControlStop` the wrapper fills `SERVICE_CONTROL_STATUS_REASON_PARAMSW` with `SERVICE_STOP_REASON_FLAG_PLANNED | MAJOR_NONE | MINOR_NONE` — there is a `FIXME` to allow customising the reason code. + +**Public properties**: + +- `Property Get GetConfiguredService(ByVal Name As String) As ServiceManager` — look up a previously-configured `ServiceManager` by its `Name`. Raises run-time error 5 if not found. (Despite the `Get` syntax the lookup is parameterised by name; it's a property in name only.) + +**Public enumerator**: + +- `Property Get _NewEnum() As Variant` — `[Enumerator]`-tagged; enables `For Each manager In Services` over the configured `ServiceManager`s. *"Provides For-Each support for the services collection, exposing each configured service as a ServiceManager instance."* + +#### `ServiceManager` public members + +`[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 by `ServiceEntryPoint` from the `argv` the SCM hands over. `LaunchArgs(0)` is the *first user-supplied* argument (the SCM-supplied service name at `argv[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 via `ChangeServiceConfig2W(SERVICE_CONFIG_DESCRIPTION)` on every successful `Install()`. +- `Type As ServiceTypeConstants` (Get / Let) — *"The type of the service, typically `tbServiceTypeOwnProcess` or `tbServiceTypeShareProcess`."* Defaults to `tbServiceTypeOwnProcess`. +- `InstallStartMode As ServiceStartConstants` (Get / Let) — *"The start-mode of the service, typically `tbServiceStartOnDemand` or `tbServiceStartAuto`."* Defaults to `tbServiceStartOnDemand`. +- `InstallCmdLine As String` (Get / Let) — *"The command line arguments passed to the service EXE when the OS launches the service."* Defaults to `""""""`. **Usually overridden to add a discriminator argument** like `-startService` so 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 an `Array("OtherSvc1", "OtherSvc2")`. The setter stashes it; `Install()` packs it into a double-null-terminated string and hands it to `CreateServiceW`. +- `AutoInitializeCOM As Boolean` (Get / Let) — *"When TRUE, COM will be initialized for you on the new service thread in STA mode."* Defaults to `True`. Set to `False` if your service needs a different apartment model (call `CoInitializeEx` yourself from `EntryPoint`). +- `SupportsPausing As Boolean` (Get / Let) — *"When TRUE, the SCM will send `SERVICE_CONTROL_PAUSE` / `SERVICE_CONTROL_CONTINUE` notifications."* Defaults to `False`. The setter calls `ResyncStatus()` so toggling it mid-run takes effect immediately. (Most services set this to `True` once inside `EntryPoint` and then handle `vbServiceControlPause` / `vbServiceControlContinue` in `ChangeState`.) + +**Public methods**: + +- `Sub Install()` — *"This method attempts to install the configured service on the system."* Opens the SCM with `SC_MANAGER_CONNECT Or SC_MANAGER_CREATE_SERVICE`, calls `CreateServiceW`. If the service already exists, deletes it (via `OpenServiceW(SERVICE_DELETE)` + `DeleteService`) and **retries** the create — so `Install()` is effectively re-entrant / safe to call multiple times. On successful create, sets the description via `ChangeServiceConfig2W`. 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 with `SERVICE_DELETE`, calls `DeleteService`. 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's `EntryPoint` is **required** to call `ReportStatus(vbServiceStatusRunning)` once steady-state is reached and `ReportStatus(vbServiceStatusStopped)` once shut-down completes; long start-up sequences should also call `ReportStatus(vbServiceStatusStartPending, , )` periodically to keep the SCM from killing the service. The `dwControlsAccepted` field of `SERVICE_STATUS` is filled automatically from the state and from `SupportsPausing` (Stop is always accepted except during `StartPending`; Pause/Continue is gated on `SupportsPausing`). The `dwCheckPoint` field auto-increments for pending states and resets on `Running`/`Stopped`. +- `Sub ResyncStatus()` — re-applies the cached `SERVICE_STATUS` to the SCM via `SetServiceStatus`. Called automatically from `ReportStatus` and from the `SupportsPausing` setter. 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. + +#### `ServiceCreator(Of T)` public members + +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`. Returns `New 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-allocated `ServiceManager`. + +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`. + +#### `ServiceState` public members + +`[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 typed `Long` rather than `ServiceStatusConstants`. **Source carries a `' FIXME` comment** — surface as a `> [!NOTE]` that this returns the underlying `Long` value (which happens to match the `ServiceStatusConstants` enum values), and that callers wanting type-safety can `CType(state.CurrentState, ServiceStatusConstants)`. +- `CurrentStateText As String` — human-readable text: `"RUNNING"`, `"STOPPED"`, `"STARTING"`, `"STOPPING"`, `"PAUSED"`, `"PAUSING"`, `"CONTINUING"`, `"UNKNOWN STATE ()"`. +- `ControlsAccepted As Long` — bitmask of `SERVICE_ACCEPT_*` flags. **Source carries a `' FIXME` comment** — surface the same way as `CurrentState`. +- `ExitCode As Long` — the `dwWin32ExitCode` field. The Win32 documented sentinel `ERROR_SERVICE_SPECIFIC_ERROR` (`1066`) means "see `ServiceSpecificExitCode`". +- `ServiceSpecificExitCode As Long` — the service-defined exit code when `ExitCode = ERROR_SERVICE_SPECIFIC_ERROR`. Otherwise meaningless. +- `CheckPoint As Long` — the `dwCheckPoint` field; increments while the service is in a pending state and resets at steady state. +- `WaitHint As Long` — the `dwWaitHint` milliseconds field. +- `ProcessId As Long` — the OS process ID hosting the service (0 if not running). +- `Flags As Long` — the `dwServiceFlags` field (currently `SERVICE_RUNS_IN_SYSTEM_PROCESS = 1` is the only documented bit). + +#### `ITbService` public members + +`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: + 1. Optionally validates startup conditions (e.g. checks `ServiceContext.LaunchArgs`). + 2. Calls `ServiceContext.ReportStatus(vbServiceStatusRunning)` once steady-state is reached (the dispatcher trampoline reports `vbServiceStatusStartPending` automatically before calling `EntryPoint`). + 3. Runs the long-running work loop. For pipe-server services this is the `NamedPipeServer.ManualMessageLoopEnter()` blocking call; for other services it might be a `Do While IsStopping = False` loop with a wait primitive. + 4. Calls `ServiceContext.ReportStatus(vbServiceStatusStopped)` before returning. +- `Sub StartupFailed(ByVal ServiceContext As ServiceManager)` — called if `RegisterServiceCtrlHandlerExW` failed (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 to `ReportStatus` from 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 dwControl` over `vbServiceControlStop` / `vbServiceControlShutdown` / `vbServiceControlPause` / `vbServiceControlContinue`, set shared `Public` flags (`IsStopping`, `IsPaused`), call `ServiceContext.ReportStatus` to acknowledge the transition, signal the service thread to react (e.g. `NamedPipeServer.ManualMessageLoopLeave()`). The `dwEventType` + `lpEventData` parameters 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's `HandlerEx` documentation 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. + +#### Enumerations + +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 use `tbServiceTypeOwnProcess` (one service per EXE) or `tbServiceTypeShareProcess` (multiple services hosted in one EXE; the example uses this). The `Interactive` variants are kept for compatibility but Windows Vista and later disallow them; flag with a `> [!NOTE]`. +- `ServiceStartConstants` — `tbServiceStartAuto`, `tbServiceStartBoot`, `tbServiceStartOnDemand`, `tbServiceStartDisabled`, `tbServiceStartDriverSystem`. `tbServiceStartBoot` and `tbServiceStartDriverSystem` only apply to kernel drivers. +- `ServiceControlCodeConstants` — 18 values mirroring the Win32 `SERVICE_CONTROL_*` constants. Source-side prefix is `vbServiceControl*` (carried over from VB6 — note the prefix is `vb`, not `tb`, in this enum; surface as-is, don't try to rationalise). +- `ServiceStatusConstants` — `vbServiceStatusStopped`, `vbServiceStatusStartPending`, `vbServiceStatusStopPending`, `vbServiceStatusRunning`, `vbServiceStatusContinuePending`, `vbServiceStatusPausePending`, `vbServiceStatusPaused`. Same `vb` prefix. + +Format pages like `WebView2/Enumerations/wv2PrintOrientation.md` — single intro paragraph, a value table with `{: #vbServiceXxx }` anchors per row for deep-linking. + +#### Doc-side layout (folders / files) + +Ten pages total: + +``` +docs/Reference/WinServicesLib/ + index.md ← package landing; lifecycle + dual-thread model + install / launch flows + integration cross-links (event log + named pipes) + Services.md ← the predeclared singleton + ServiceManager.md ← per-service configuration + ReportStatus + ServiceCreator.md ← Of T generic factory + ServiceState.md ← read-only state snapshot + ITbService.md ← interface every service implements + Enumerations/index.md + Enumerations/ServiceTypeConstants.md + Enumerations/ServiceStartConstants.md + Enumerations/ServiceControlCodeConstants.md + Enumerations/ServiceStatusConstants.md +``` + +All five concrete pages are single-file (no folder-style — no natural sub-pages; each class's surface is medium-sized). + +The `index.md` should be substantial and walk the reader through: + +1. **What a Windows service is** (one paragraph: long-running background process supervised by the SCM, started before/independently of user logon, controlled via the Services control panel applet or `sc.exe`). +2. **Lifecycle**: configure (`Services.ConfigureNew`) → install (`Services.InstallAll` or per-manager `.Install`, **elevated**) → run (`Services.RunServiceDispatcher` blocks the EXE's main thread; SCM launches the service thread on demand). The example's `If InStr(Command, "-startService") > 0` branch is the canonical "same EXE for install-time UI and run-time service" pattern. +3. **The two-thread split**: `EntryPoint` and `ChangeState` run on different threads; surface this prominently with a small diagram or numbered explanation. +4. **Integration with the sister packages**: cross-link `Implements EventLog(Of …) Via EventLog = New EventLog(…)` (see `WinEventLogLib`) and the `NamedPipeServer.ManualMessageLoopEnter`/`Leave` service-hosting idiom (see `WinNamedPipesLib`). The worked example at `..\tbrepro\winlibs\tbServiceTest2\Sources\` ties all three together. + +**Naming:** + +- Folder / URL segment: `WinServicesLib/` (matches the source-side package name; no `Package` suffix to drop, same as `WinEventLogLib` and `WinNamedPipesLib`). +- Index title: `WinServicesLib Package` — the ` Package` convention. +- Permalinks: `/tB/Packages/WinServicesLib/` for the landing; `/tB/Packages/WinServicesLib/` for each class page; `/tB/Packages/WinServicesLib/Enumerations/` for each enum page. +- `parent: WinServicesLib Package` on each top-level child page. The four enum pages set `parent: Enumerations` and `grand_parent: WinServicesLib Package` (the grouped-page pattern; same shape as the WebView2 / CEF / CustomControls `Enumerations/` directories). + +**License:** MIT (copyright Wayne Phillips T/A iTech Masters, 2025; first release v0.1, 04-FEB-2025) — same situation as every other Wayne Phillips package. Pages are fully original content; **omit** the `vba_attribution: true` flag. + ## Page template Match the existing style. Worked examples to imitate: @@ -693,6 +1030,8 @@ The URL prefixes are *not* uniform across packages — VBA pages live one segmen - WinEventLogLib class → `/tB/Packages/WinEventLogLib/EventLog` (single-file; same depth as a single-file VB class) - WinEventLogLib module → `/tB/Packages/WinEventLogLib/EventLogHelperPublic` (single-file; same depth as an Assert module) - WinNamedPipesLib class → `/tB/Packages/WinNamedPipesLib/` (single-file; same depth as a single-file VB class) +- WinServicesLib class / interface → `/tB/Packages/WinServicesLib/` (single-file; same depth as a single-file VB class) +- WinServicesLib enumeration → `/tB/Packages/WinServicesLib/Enumerations/` (one segment deeper, parallel to WebView2 / CEF / CustomControls) Common patterns: @@ -766,6 +1105,19 @@ Common patterns: | WinNamedPipesLib `Packages/WinNamedPipesLib/X` | sibling `Packages/WinNamedPipesLib/Y` | `[Y](Y)` | | WinNamedPipesLib `Packages/WinNamedPipesLib/X` | VBA `Modules//Y` | `[Y](../../Modules//Y)` | | WinNamedPipesLib `Packages/WinNamedPipesLib/X` | `Core/Y` | `[Y](../../Core/Y)` | +| WinNamedPipesLib `Packages/WinNamedPipesLib/X` | WinServicesLib `Packages/WinServicesLib/Y` | `[Y](../WinServicesLib/Y)` | +| WinNamedPipesLib `Packages/WinNamedPipesLib/X` | WinEventLogLib `Packages/WinEventLogLib/Y` | `[Y](../WinEventLogLib/Y)` | +| WinServicesLib `Packages/WinServicesLib/X` (single-file) | sibling `Packages/WinServicesLib/Y` | `[Y](Y)` | +| WinServicesLib `Packages/WinServicesLib/X` (single-file) | `Packages/WinServicesLib/Enumerations/Y` | `[Y](Enumerations/Y)` | +| WinServicesLib `Packages/WinServicesLib/X` (single-file) | WinEventLogLib `Packages/WinEventLogLib/Y` | `[Y](../WinEventLogLib/Y)` | +| WinServicesLib `Packages/WinServicesLib/X` (single-file) | WinNamedPipesLib `Packages/WinNamedPipesLib/Y` | `[Y](../WinNamedPipesLib/Y)` | +| WinServicesLib `Packages/WinServicesLib/X` (single-file) | VBRUN `Packages/VBRUN//Y` | `[Y](../VBRUN//Y)` | +| WinServicesLib `Packages/WinServicesLib/X` (single-file) | `Core/Y` | `[Y](../../Core/Y)` | +| WinServicesLib `Packages/WinServicesLib/Enumerations/X` | sibling `Enumerations/Y` | `[Y](Y)` | +| WinServicesLib `Packages/WinServicesLib/Enumerations/X` | `Packages/WinServicesLib/` | `[Y](../)` | +| WinServicesLib `Packages/WinServicesLib/Enumerations/X` | WinEventLogLib `Packages/WinEventLogLib/Y` | `[Y](../../WinEventLogLib/Y)` | +| WinEventLogLib `Packages/WinEventLogLib/X` | WinServicesLib `Packages/WinServicesLib/Y` | `[Y](../WinServicesLib/Y)` | +| WinEventLogLib `Packages/WinEventLogLib/X` | WinNamedPipesLib `Packages/WinNamedPipesLib/Y` | `[Y](../WinNamedPipesLib/Y)` | | `Core/X` | VBA `Modules//Y` | `[Y](../Modules//Y)` | | `Core/X` | VBRUN `Packages/VBRUN//Y` | `[Y](../Packages/VBRUN//Y)` | | `Core/X` | VB `Packages/VB/Y` | `[Y](../Packages/VB/Y)` | @@ -775,6 +1127,7 @@ Common patterns: | `Core/X` | CEF `Packages/CEF/Y` | `[Y](../Packages/CEF/Y)` | | `Core/X` | WinEventLogLib `Packages/WinEventLogLib/Y` | `[Y](../Packages/WinEventLogLib/Y)` | | `Core/X` | WinNamedPipesLib `Packages/WinNamedPipesLib/Y` | `[Y](../Packages/WinNamedPipesLib/Y)` | +| `Core/X` | WinServicesLib `Packages/WinServicesLib/Y` | `[Y](../Packages/WinServicesLib/Y)` | | `Core/X` | `Core/Y` (sibling) | `[Y](Y)` | Always link to the **canonical** location (the page's `permalink:`), not to a `redirect_from` alias. Pages that have moved out of `Core/` retain a `redirect_from: /tB/Core/` so legacy links still work, but forward-style links should point at the new home. @@ -790,6 +1143,7 @@ Always link to the **canonical** location (the page's `permalink:`), not to a `r - CEF package → `..\tbrepro\cef\CEFSampleProject\Packages\cefPackage\Sources\CefControl.twin` for the whole public surface (the `CefBrowser` control, its `CefBrowserBaseCtl` base, and `CefEnvironmentOptions`). For the two surfaced enums: `CEF\Enums\_cef_log_severity_t.twin` (declares both the internal `cef_log_severity_t` and the user-facing `CefLogSeverity`) and `CEF\CrossProcessIPC\BrowserOM.twin` (declares `cefPrintOrientation` inline, around line 29). Everything else under `cefPackage\Sources\` and `cefPackage\Sources\CEF\` is `Private Class` / `Private Module` plumbing — skip. The sample project's `Sources\Example1..4.twin` are the source-of-truth for which features are *not* yet exposed (commented-out event handlers with *"Sorry, this feature is not yet available in the CEF package"*). - WinEventLogLib package → `..\tb-export\NewProject\Packages\WinEventLogLib\Sources\EventLog.twin` (the generic `EventLog(Of T1, T2)` class) and `Helper.twin` (`EventLogHelperPublic.RegisterEventLogInternal`). Skip `APIs.twin` (`Private Module`), `Constants.twin` (`Private Module` — the `EventLogTypeConstants` enum is unreachable from outside the package), and the `EventLogHelperPrivate` module in `Helper.twin` (named "Private" though declared `Public`; only used internally by `EventLog.LogArray`). - WinNamedPipesLib package → `..\tb-export\NewProject\Packages\WinNamedPipesLib\Sources\` — one `.twin` per public class: `NamedPipeServer.twin`, `NamedPipeServerConnection.twin`, `NamedPipeClientManager.twin`, `NamedPipeClientConnection.twin`. Each `.twin` also declares a `Private Interface INamedPipe*Internal` (refcount / dispatch helper used by the IOCP threads); skip those. Skip `APIs.twin`, `Constants.twin`, and `Helper.twin` (all `Private Module`). + - WinServicesLib package → `..\tb-export\NewProject\Packages\WinServicesLib\Sources\` — one `.twin` per public class: `Services.twin` (the `[PredeclaredId]` coordinator), `ServiceManager.twin`, `ServiceCreator.twin`, `ServiceState.twin`. Plus `Interfaces.twin` for the `Public Interface ITbService` (the file also declares `Private Interface IServiceCreator` and `Private Interface IServiceManagerInternal` — skip both). Enumerations live in `Constants.twin` under `Public Module ServicesConstantsPublic` (four enums: `ServiceTypeConstants`, `ServiceStartConstants`, `ServiceControlCodeConstants`, `ServiceStatusConstants`). Skip `APIs.twin`, `Helper.twin`, and the `Private Module ServicesConstants` half of `Constants.twin` (all package-internal). The worked integration example — services + event log + named pipes wired together — is at `..\tbrepro\winlibs\tbServiceTest2\Sources\` (read `Startup.twin` and `SERVICES\TBSERVICE001.twin` first; the latter is the canonical `ITbService` implementation). 2. **Decide placement**: - Pure language keyword (parsed by the compiler, no runtime call) → `docs/Reference/Core/`. - Runtime function/property → `docs/Reference///`. Add `redirect_from: /tB/Core/` so legacy `tB/Core/` links still work. @@ -805,6 +1159,7 @@ Always link to the **canonical** location (the page's `permalink:`), not to a `r - CEF control → `docs/Reference/CEF/CefBrowser/index.md` (folder-style; carries the `EnvironmentOptions` sub-page). Pre-creation options class → `docs/Reference/CEF/CefBrowser/EnvironmentOptions.md` (parallel to `WebView2/WebView2/EnvironmentOptions.md`). CEF enumeration → `docs/Reference/CEF/Enumerations/.md`. - WinEventLogLib generic class → `docs/Reference/WinEventLogLib/EventLog.md` (single-file; the surface is small — constructor + three methods). WinEventLogLib helper module → `docs/Reference/WinEventLogLib/EventLogHelperPublic.md` (single-file; one Sub). - WinNamedPipesLib class → `docs/Reference/WinNamedPipesLib/.md` (single-file; one page per public class — `NamedPipeServer.md`, `NamedPipeServerConnection.md`, `NamedPipeClientManager.md`, `NamedPipeClientConnection.md`). No folder-style — none of the four classes have sub-pages. + - WinServicesLib class → `docs/Reference/WinServicesLib/.md` (single-file; one page each for `Services.md`, `ServiceManager.md`, `ServiceCreator.md`, `ServiceState.md`, `ITbService.md`). WinServicesLib enumeration → `docs/Reference/WinServicesLib/Enumerations/.md` — one page each for `ServiceTypeConstants`, `ServiceStartConstants`, `ServiceControlCodeConstants`, `ServiceStatusConstants` (mirrors `WebView2/Enumerations/`, `CEF/Enumerations/`, `CustomControls/Enumerations/`, `VBRUN/Constants/`). - Pick `` from VBA's grouping (Information, Interaction, Strings, FileSystem, DateTime, Math, Financial, Conversion, ...) and the existing folders under `Reference//`. 3. **Adapt content** (VBA-Docs sources): - Strip MS frontmatter (`ms.assetid`, `f1_keywords`, `keywords`, `ms.date`, `ms.localizationpriority`). @@ -881,10 +1236,22 @@ Always link to the **canonical** location (the page's `permalink:`), not to a `r - `Handle` is `Public` on both connection classes — it is the underlying Win32 pipe handle. Document it as informational (useful for low-level / debugging access), not as something user code typically reads or writes. - The README TODO list lives on the **landing page** ("Known limitations" or "Roadmap" section), not on the per-class pages. Reproduce only the user-facing items (no method to drop a single client from the server side; no `Error` events on the IOCP worker thread; suspected hard cap on message size). Skip the *"currently a lot of duplicate code in server + client"* note — it's an internal-refactor concern. - Omit the `vba_attribution: true` frontmatter flag — these pages are fully original (the package is MIT-licensed). -11. **Flag tB deviations** with a `> [!NOTE]` callout (see next section). -12. **Update the parent index** (`//index.md`, `docs/Reference/VB/index.md`, `docs/Reference/WebView2/index.md`, `docs/Reference/Assert/index.md`, `docs/Reference/CustomControls/index.md` (and its `Styles/`, `Framework/`, `Enumerations/` sub-indices), `docs/Reference/CEF/index.md` (and its `Enumerations/` sub-index), `docs/Reference/WinEventLogLib/index.md`, `docs/Reference/WinNamedPipesLib/index.md`, `Reference/Statements.md`, or `Reference/Procedures and Functions.md`) — turn an unlinked bullet into a link with a short blurb. Match the existing style of the page. If a new package is being added, also extend `docs/Reference/Packages.md` to list it. -13. **Add the page** to `Reference/Statements.md` or `Reference/Procedures and Functions.md` if it's a statement or callable and not already listed there. -14. **Run the [site integrity check](#site-integrity-check)** after the batch and before committing. +11. **Adapt content** (WinServicesLib `.twin` sources): + - The five public classes are flat — `Services` (PredeclaredId), `ServiceManager`, `ServiceCreator(Of T)`, `ServiceState`, and the `ITbService` interface. List each class's surface in the order *Fields → Properties → Methods → Events* (the package has no events on the public classes; the `ITbService` interface defines callbacks that the user implements, not events on a class). + - `Services` is `[PredeclaredId]` — surface this on the page intro. The class is used singleton-style as `Services.X` without `New`, and also doubles as an enumerable collection (`For Each manager In Services`). The `[Description("...")]` attribute on each method is the IDE one-liner — use it as the basis for the entry, then expand. + - `ServiceManager` carries `[COMCreatable(False)]` plus an `[Description("For internal use. Dont create instances of ServiceManager manually, use Services.ConfigureNew instead")]` on its constructor. Surface that on the intro paragraph (*"Instantiate via `Services.ConfigureNew`, not directly"*) — same shape as the WinNamedPipesLib `Connection` classes. Two methods on `ServiceManager` are technically `Public`-by-default but only invoked by the OS / package infrastructure (`ServiceEntryPoint`, `ServiceControlHandlerCallback`) — list them at the bottom of the page under "Internal hooks" with a `> [!NOTE]` saying user code never calls them; do not list them at the top with the user-facing methods. `ResyncStatus` is borderline — list it under Methods but note that `ReportStatus` calls it automatically. + - `ServiceCreator(Of T)` is a single-method generic class. Surface the constraint clearly: `T` must implement `ITbService` (the source has no syntactic `Where T : ITbService` clause, but `Return New T As ITbService` only compiles when `T` does). Do not surface the `[ClassId("66170220-...-EAEAEAEAEAEA")]` — same compiler-special-handling rule as `WinEventLogLib`'s `EventLog(Of T1, T2)`. + - `ServiceState` is read-only and constructed via `Services.QueryStateOfService(Name)`. Surface that the snapshot is taken **once at construction time** and never refreshed — to see updated state, call `QueryStateOfService` again. Two properties (`CurrentState`, `ControlsAccepted`) carry `' FIXME` comments noting they return raw `Long` rather than the typed enum; surface as a `> [!NOTE]` on each, with a `CType` example. + - `ITbService` is the user-implemented contract. Three subs (`EntryPoint`, `StartupFailed`, `ChangeState`). The page **must** prominently flag the two-thread split: `EntryPoint` runs on the service thread (spawned by the SCM dispatcher), `ChangeState` runs on the main dispatcher thread. The canonical inter-thread coordination pattern uses shared `Public` flags (`IsStopping`, `IsPaused`) on the implementing class — surface this as the recommended idiom, with the `tbServiceTest2\Sources\SERVICES\TBSERVICE002.twin` `IsStopping` / `IsPaused` example. + - For the four enums under `ServicesConstantsPublic`: format pages like `WebView2/Enumerations/wv2PrintOrientation.md` — single intro paragraph, a value table with `{: #vbServiceXxx }` / `{: #tbServiceXxx }` anchors per row for deep-linking. The mixed `vb` / `tb` prefix across enums is source-side inconsistency — surface as-is, don't try to rationalise. Call out on `ServiceTypeConstants` that the driver values (`tbServiceTypeSystemDriver`, `tbServiceTypeKernelDriver`, `tbServiceTypeRecognizerDriver`, `tbServiceTypeAdapter`) are only meaningful for kernel-mode drivers (twinBASIC services compile to user-mode EXEs); the `Interactive` variants are kept for compatibility but Windows Vista and later disallow them (note with a `> [!NOTE]`). + - Do not list `[InterfaceId]`, `[ClassId]`, `[EventInterfaceId]` on any page; they are COM-plumbing decoration. + - The package-internal `IServiceCreator` and `IServiceManagerInternal` interfaces (both `Private Interface` in `Interfaces.twin`) are pure marshalling-and-trampoline plumbing — **no doc page**, and do not surface the underscored implementing members on the concrete classes either. + - The index landing page must walk the integration story: configure-during-`Sub Main` → install (elevated, one-time) → run-as-service (the `-startService` discriminator pattern) → service-thread `EntryPoint` reports running → `ChangeState` handles stop on the dispatcher thread. Cross-link the [`EventLog` composition-delegation idiom](docs/Reference/WinEventLogLib/EventLog.md) (the `Implements EventLog(Of …) Via …` pattern from `tbServiceTest2\Sources\SERVICES\TBSERVICE001.twin`) and the [`NamedPipeServer` service-host idiom](docs/Reference/WinNamedPipesLib/NamedPipeServer.md) (the `ManualMessageLoopEnter` / `Leave` pattern). + - Omit the `vba_attribution: true` frontmatter flag — these pages are fully original (the package is MIT-licensed, same as every other Wayne Phillips package). +12. **Flag tB deviations** with a `> [!NOTE]` callout (see next section). +13. **Update the parent index** (`//index.md`, `docs/Reference/VB/index.md`, `docs/Reference/WebView2/index.md`, `docs/Reference/Assert/index.md`, `docs/Reference/CustomControls/index.md` (and its `Styles/`, `Framework/`, `Enumerations/` sub-indices), `docs/Reference/CEF/index.md` (and its `Enumerations/` sub-index), `docs/Reference/WinEventLogLib/index.md`, `docs/Reference/WinNamedPipesLib/index.md`, `docs/Reference/WinServicesLib/index.md` (and its `Enumerations/` sub-index), `Reference/Statements.md`, or `Reference/Procedures and Functions.md`) — turn an unlinked bullet into a link with a short blurb. Match the existing style of the page. If a new package is being added, also extend `docs/Reference/Packages.md` to list it. +14. **Add the page** to `Reference/Statements.md` or `Reference/Procedures and Functions.md` if it's a statement or callable and not already listed there. +15. **Run the [site integrity check](#site-integrity-check)** after the batch and before committing. ## twinBASIC deviations from VBA to flag From 5aaf3c0abd0c2a43a0774155a3c003702502a83e Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Thu, 14 May 2026 16:12:34 +0200 Subject: [PATCH 2/3] Update the EventLog and NamedPipes docs based on example code. --- docs/Reference/WinEventLogLib/EventLog.md | 6 +- docs/Reference/WinEventLogLib/index.md | 79 +++++++++++++++- .../NamedPipeClientConnection.md | 5 +- .../NamedPipeClientManager.md | 3 + .../WinNamedPipesLib/NamedPipeServer.md | 10 +- .../NamedPipeServerConnection.md | 11 ++- docs/Reference/WinNamedPipesLib/index.md | 91 ++++++++++++++++++- 7 files changed, 192 insertions(+), 13 deletions(-) diff --git a/docs/Reference/WinEventLogLib/EventLog.md b/docs/Reference/WinEventLogLib/EventLog.md index e6290d4..b461d14 100644 --- a/docs/Reference/WinEventLogLib/EventLog.md +++ b/docs/Reference/WinEventLogLib/EventLog.md @@ -37,7 +37,9 @@ Dim Log As New EventLog(Of MyEventIds, MyCategories)("MyService") Both type arguments are required at instantiation — twinBASIC does not deduce them from the *LogName* constructor argument. See the [Generics](../../../Features/Language/Generics) page for the general rules. -The package [overview](.) covers the install-then-log lifecycle, registry layout, and message-resource generation that surround this class. +A class that needs to expose [**LogSuccess**](#logsuccess) / [**LogFailure**](#logfailure) / [**Register**](#register) as if those methods were its own can mix the **EventLog** surface in through [**Implements ... Via**](../../../Features/Language/Inheritance) composition — see the [composition-delegation idiom](.#composition-delegation-idiom) section on the package overview for the canonical service-class pattern. + +The package [overview](.) covers the install-then-log lifecycle, the [`[PopulateFrom("json", ...)]` message-resource convention](.#populatefrom-convention), registry layout, and the [composition-delegation idiom](.#composition-delegation-idiom). * TOC {:toc} @@ -112,7 +114,7 @@ Creates `HKLM\SYSTEM\CurrentControlSet\Services\EventLog\` (prepending > [!IMPORTANT] > **Register** requires administrator rights — it writes to `HKEY_LOCAL_MACHINE`. The usual pattern is to call it once from an elevated installer, not from the application's normal startup path. -The Event Viewer renders message strings by loading **EventMessageFile** and looking up the message resource keyed by *EventId*. Because **EventMessageFile** points at `App.ModulePath`, the same EXE that calls **Register** must be the one that later calls [**LogSuccess**](#logsuccess) / [**LogFailure**](#logfailure); otherwise the Event Viewer cannot find the message strings. See [Message resources](.#message-resources) on the package landing page for how the resource is expected to be populated. +The Event Viewer renders message strings by loading **EventMessageFile** and looking up the message resource keyed by *EventId*. Because **EventMessageFile** points at `App.ModulePath`, the same EXE that calls **Register** must be the one that later calls [**LogSuccess**](#logsuccess) / [**LogFailure**](#logfailure); otherwise the Event Viewer cannot find the message strings. See [Message resources](.#message-resources) and [The `[PopulateFrom("json", ...)]` convention](.#populatefrom-convention) on the package landing page for the recommended way to populate the resource. If the registry key cannot be opened for write, **Register** raises run-time error 5 *"Failed to register event log source (\)"*. Typical causes are insufficient privileges and a *LogPath* that points at a non-existent parent log. diff --git a/docs/Reference/WinEventLogLib/index.md b/docs/Reference/WinEventLogLib/index.md index 0a1dd56..6f12630 100644 --- a/docs/Reference/WinEventLogLib/index.md +++ b/docs/Reference/WinEventLogLib/index.md @@ -52,11 +52,88 @@ End Sub The same EXE that calls [**Register**](EventLog#register) must be the one that calls [**LogSuccess**](EventLog#logsuccess) / [**LogFailure**](EventLog#logfailure) — the registered **EventMessageFile** points at `App.ModulePath`, and the Event Viewer reads message strings out of that file when rendering entries. +For service / long-running classes that should expose [**LogSuccess**](EventLog#logsuccess) / [**LogFailure**](EventLog#logfailure) / [**Register**](EventLog#register) as if those methods were their own, see the [composition-delegation idiom](#composition-delegation-idiom) below. + +## Composition-delegation idiom + +A class can mix [**EventLog**](EventLog)`(Of T1, T2)` in through twinBASIC's [`Implements ... Via`](../../../Features/Language/Inheritance) composition syntax and inherit its public surface unqualified: + +```tb +Class MyService + Implements EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES) Via _ + EventLog = New EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES)("Application\" & CurrentComponentName) + + Sub Run() + LogSuccess service_started, status_changed, CurrentComponentName ' forwarded through the field above + ' ... + LogSuccess service_ended, status_changed, CurrentComponentName + End Sub +End Class +``` + +The `Implements Via = ` clause declares a private field, evaluates the constructor expression once on first use, and forwards every public member of [**EventLog**](EventLog) — [**LogSuccess**](EventLog#logsuccess), [**LogFailure**](EventLog#logfailure), and [**Register**](EventLog#register) — through that field. Inside `MyService` the three methods read as if they were declared on the class itself. + +Two things to remember: + +- The *T1* / *T2* type arguments must match between the `Implements` declaration and the constructor expression — the compiler enforces this. +- Using `"Application\" & CurrentComponentName` as the *LogName* makes the log path automatically track the class name at compile time; renaming the class renames the source it logs to. + +This is the canonical mix-in pattern for Windows-service classes (every service class in a project that shares one set of event IDs picks up logging methods without per-class boilerplate). The same pattern works for any class that wants the [**EventLog**](EventLog) surface inline. + +A class can use `Implements ... Via` on [**EventLog**](EventLog)`(Of T1, T2)` only **once**. When several classes in the same project need to share a logging surface, declare a single module with one event-ID enum and one category enum and `Implements ... Via` against that pair from every class. Multiple unrelated message tables are still possible — they just have to be reached through explicitly-named [**EventLog**](EventLog) fields rather than the `Implements ... Via` shortcut. + ## Message resources The Windows Event Log stores only numeric **Event ID** and **Category** values; the human-readable strings live in a message-table resource embedded in the EXE pointed to by the registered **EventMessageFile** / **CategoryMessageFile** entries. Without this resource the Event Viewer cannot render entries and instead shows *"The description for Event ID X cannot be found"*. -For the generic [**EventLog**](EventLog)`(Of T1, T2)` class, the *T1* (event IDs) and *T2* (categories) enum declarations are the source of those strings — the class points the registry at the running EXE and assumes the EXE carries a message-table resource keyed by the enum member values. Authoring the resource yourself (a `.mc` file fed to `mc.exe`) and embedding it in the EXE is one route; the [**EventLog**](EventLog) class is designed to interoperate with whatever mechanism populates that resource for the *T1* / *T2* member names. +For the generic [**EventLog**](EventLog)`(Of T1, T2)` class, the *T1* (event IDs) and *T2* (categories) enum declarations are the source of those strings — the class points the registry at the running EXE and the Event Viewer looks for a message-table resource keyed by the enum member values. Authoring the resource directly (a `.mc` file fed to `mc.exe`, embedded as a resource section) is one route; the convention shown below keeps the enums, the message strings, and the resource emission in lockstep from a single JSON file using twinBASIC's [`[PopulateFrom]`](../../Core/Attributes#populatefrom) enum-population attribute. + +### The `[PopulateFrom("json", ...)]` convention +{: #populatefrom-convention } + +Declare a module with two empty enum stubs, each tagged with [`[PopulateFrom]`](../../Core/Attributes#populatefrom) pointing at a project-relative JSON resource: + +```tb +Module MESSAGETABLE + [PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "events", "name", "id")] + Enum EVENTS + End Enum + + [PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "categories", "name", "id")] + Enum CATEGORIES + End Enum +End Module +``` + +The five [`[PopulateFrom]`](../../Core/Attributes#populatefrom) arguments are: the resource format (`"json"`), the project-relative path to the file, the JSON array to read entries from (`"events"` for the events stub, `"categories"` for the categories stub), the field name supplying each enum member's identifier, and the field name supplying its numeric value. + +`Resources/MESSAGETABLE/Strings.json` carries one entry per event and one per category. Each entry has three fields — a numeric `id`, an enum-member `name`, and the per-locale message text under an `LCID_XXXX` key: + +```json +{ + "events": [ + { "id": -1073610751, "name": "service_started", "LCID_0000": "%1 service started" }, + { "id": -1073610750, "name": "service_startup_failed", "LCID_0000": "%1 service startup failed" }, + { "id": -1073610749, "name": "service_ended", "LCID_0000": "%1 service ended" } + ], + "categories": [ + { "id": 1, "name": "status_changed", "LCID_0000": "Status Changed" } + ] +} +``` + +The compiler reads the JSON at build time and populates each enum body — `Enum EVENTS` ends up with `service_started = -1073610751`, `service_startup_failed = -1073610750`, … — while emitting the message-table resource into the produced EXE. The `LCID_0000` strings (locale-neutral) become the message templates; substitute / add `LCID_0409` (US English), `LCID_040C` (French), etc. for localised projects. + +Once the JSON, the enum stubs, and the registry entries written by [**Register**](EventLog#register) are in place, a runtime call + +```tb +Dim Log As New EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES)("Application\" & CurrentComponentName) +Log.LogSuccess service_started, status_changed, "MyService" +``` + +writes an event the Event Viewer renders as *"MyService service started"* — the `%1` placeholder filled from the [**LogSuccess**](EventLog#logsuccess) *AdditionalStrings* `ParamArray`, the `status_changed` category resolved against the message table, both keyed by the numeric values the enums carry. + +The negative event-ID values in the JSON (`-1073610751` etc.) follow the Win32 documented event-ID bit layout — the high bits encode severity, facility, and customer-defined flags. See Microsoft's *"Event Identifiers"* reference for the encoding; pick fresh IDs for new events and don't reuse identifiers across products. ## Log Type diff --git a/docs/Reference/WinNamedPipesLib/NamedPipeClientConnection.md b/docs/Reference/WinNamedPipesLib/NamedPipeClientConnection.md index a7ac650..7a0b6b1 100644 --- a/docs/Reference/WinNamedPipesLib/NamedPipeClientConnection.md +++ b/docs/Reference/WinNamedPipesLib/NamedPipeClientConnection.md @@ -86,7 +86,7 @@ Syntax: *connection*_**MessageReceived**(**ByRef** *Cookie* **As Variant**, **By : The opaque correlation value originally passed to the [**AsyncRead**](#asyncread) that produced this read — or **Empty** if the read came from the auto-issued reads driven by [**NamedPipeClientManager.ContinuouslyReadFromPipe**](NamedPipeClientManager#continuouslyreadfrompipe). *Data* -: The message payload. See [Working with `Data() As Byte` in events](.#working-with-data-as-byte-in-events) on the package overview for the transient-buffer lifetime caveat — copy the bytes out before the handler returns if you need them. +: The message payload. See [Working with `Data() As Byte` in events](.#working-with-data-as-byte-in-events) on the package overview for the transient-buffer lifetime caveat — copy the bytes out before the handler returns if you need them. The [recommended capture mechanism](.#propertybag-carrier) is to assign *Data* to a fresh [**PropertyBag**](../VBRUN/PropertyBag/)'s **Contents**, which deep-copies the bytes and gives you typed multi-field access in one step. ### MessageSent {: .no_toc } @@ -133,7 +133,7 @@ Sends a message to the server. Syntax: *connection*.**AsyncWrite** *Data*() [, *Cookie* ] *Data* -: *required* A **Byte()** array carrying the bytes to send. An uninitialised or zero-length array is a no-op. +: *required* A **Byte()** array carrying the bytes to send. An uninitialised or zero-length array is a no-op. For typed multi-field payloads the recommended carrier is [**PropertyBag**](../VBRUN/PropertyBag/) — see [Recommended payload encoding: `PropertyBag`](.#propertybag-carrier) on the package overview. *Cookie* : *optional* A **Variant** correlation value, surfaced as the *Cookie* parameter of the matching [**MessageSent**](#messagesent) event. Default **Empty**. @@ -143,5 +143,6 @@ Returns immediately; the actual transmission runs through the IOCP loop. The com ## See Also - [WinNamedPipesLib package](.) -- overview, IOCP / event-marshalling architecture, cookie pattern, `Data()` lifetime caveat, the **AsyncClose** rule +- [Recommended payload encoding: `PropertyBag`](.#propertybag-carrier) -- the deep-copy capture pattern for transient *Data* in events - [NamedPipeClientManager class](NamedPipeClientManager) -- the manager that produced this connection - [NamedPipeServerConnection class](NamedPipeServerConnection) -- the server-side counterpart diff --git a/docs/Reference/WinNamedPipesLib/NamedPipeClientManager.md b/docs/Reference/WinNamedPipesLib/NamedPipeClientManager.md index c90183d..1c2b8bd 100644 --- a/docs/Reference/WinNamedPipesLib/NamedPipeClientManager.md +++ b/docs/Reference/WinNamedPipesLib/NamedPipeClientManager.md @@ -97,6 +97,8 @@ For Each name In names Next ``` +The package does not publish an event when pipes appear or disappear, so dynamic UIs that list available servers typically refresh the list from a low-frequency [**Timer**](../VB/Timer/) — see [Discovering pipes](.#discovering-pipes) on the package overview for the polling-loop pattern that preserves the user's current selection across refreshes. + ### Stop {: .no_toc } @@ -116,5 +118,6 @@ Syntax: **New NamedPipeClientManager** ## See Also - [WinNamedPipesLib package](.) -- overview, IOCP / event-marshalling architecture, cookie pattern, `Data()` lifetime caveat, **AsyncClose** rule +- [Discovering pipes](.#discovering-pipes) -- the **Timer**-driven polling loop that powers dynamic pipe-listing UIs - [NamedPipeClientConnection class](NamedPipeClientConnection) -- the per-connection object returned by [**Connect**](#connect) - [NamedPipeServer class](NamedPipeServer) -- the server-side counterpart diff --git a/docs/Reference/WinNamedPipesLib/NamedPipeServer.md b/docs/Reference/WinNamedPipesLib/NamedPipeServer.md index ccbad72..28b1d26 100644 --- a/docs/Reference/WinNamedPipesLib/NamedPipeServer.md +++ b/docs/Reference/WinNamedPipesLib/NamedPipeServer.md @@ -106,7 +106,7 @@ Syntax: *server*_**ClientMessageReceived**(*Connection* **As NamedPipeServerConn : The opaque correlation value originally passed to the [**NamedPipeServerConnection.AsyncRead**](NamedPipeServerConnection#asyncread) that produced this read — or **Empty** if the read came from the auto-issued reads driven by [**ContinuouslyReadFromPipe**](#continuouslyreadfrompipe). *Data* -: The message payload. See [Working with `Data() As Byte` in events](.#working-with-data-as-byte-in-events) on the package overview for the transient-buffer lifetime caveat — copy the bytes out before the handler returns if you need them. +: The message payload. See [Working with `Data() As Byte` in events](.#working-with-data-as-byte-in-events) on the package overview for the transient-buffer lifetime caveat — copy the bytes out before the handler returns if you need them. The [recommended capture mechanism](.#propertybag-carrier) is to assign *Data* to a fresh [**PropertyBag**](../VBRUN/PropertyBag/)'s **Contents**, which deep-copies the bytes and gives you typed multi-field access in one step. ### ClientMessageSent {: .no_toc } @@ -138,7 +138,7 @@ Issues an [**AsyncWrite**](NamedPipeServerConnection#asyncwrite) against every c Syntax: *server*.**AsyncBroadcast** *Data*() [, *Cookie* ] *Data* -: *required* The message bytes to send. +: *required* The message bytes to send. twinBASIC will coerce a **String** literal to **Byte()** implicitly, so `server.AsyncBroadcast "shutting down"` works without a separate `StrConv` step — useful for protocol-less server-pushed notifications. *Cookie* : *optional* A **Variant** correlation value, attached to *each* per-client [**ClientMessageSent**](#clientmessagesent) event. Default **Empty**. @@ -154,6 +154,8 @@ Syntax: *server*.**ManualMessageLoopEnter** Intended for console / service hosts that do not have a Forms-style message pump of their own but want the default ([**FreeThreadingEvents**](#freethreadingevents) = **False**) marshalled-event semantics. UI hosts already pump messages naturally and do not need this method. +The canonical caller is a Windows service that owns this server: the service-thread entry-point opens the server, transitions the service to `Running`, calls **ManualMessageLoopEnter** to block while events flow, and a control-code handler running on the dispatcher thread calls [**ManualMessageLoopLeave**](#manualmessageloopleave) when the SCM signals stop. See [Hosting inside a Windows service](.#service-host-idiom) on the package overview for the complete pattern, including the two-thread coordination and the *Pause* / *Continue* extension. + ### ManualMessageLoopLeave {: .no_toc } @@ -161,6 +163,8 @@ Posts a `WM_USER_QUITTING` message to the hidden marshalling window, causing the Syntax: *server*.**ManualMessageLoopLeave** +The intended caller is a thread *other* than the one inside [**ManualMessageLoopEnter**](#manualmessageloopenter) — typically the Windows service's dispatcher thread waking the service-entry-point thread out of its blocked loop. See [Hosting inside a Windows service](.#service-host-idiom). + ### Start {: .no_toc } @@ -189,5 +193,7 @@ Syntax: **New NamedPipeServer** ## See Also - [WinNamedPipesLib package](.) -- overview, IOCP / event-marshalling architecture, cookie pattern, `Data()` lifetime caveat, known limitations +- [Hosting inside a Windows service](.#service-host-idiom) -- the **ManualMessageLoopEnter** / **ManualMessageLoopLeave** service-entry-point pattern +- [Recommended payload encoding: `PropertyBag`](.#propertybag-carrier) -- the deep-copy capture pattern for transient *Data* in events - [NamedPipeServerConnection class](NamedPipeServerConnection) -- the per-client connection passed to every event - [NamedPipeClientManager class](NamedPipeClientManager) -- the client-side counterpart diff --git a/docs/Reference/WinNamedPipesLib/NamedPipeServerConnection.md b/docs/Reference/WinNamedPipesLib/NamedPipeServerConnection.md index 32702be..c4af392 100644 --- a/docs/Reference/WinNamedPipesLib/NamedPipeServerConnection.md +++ b/docs/Reference/WinNamedPipesLib/NamedPipeServerConnection.md @@ -87,17 +87,26 @@ Sends a message back to this specific client. Syntax: *connection*.**AsyncWrite** *Data*() [, *Cookie* ] *Data* -: *required* A **Byte()** array carrying the bytes to send. An uninitialised or zero-length array is a no-op. +: *required* A **Byte()** array carrying the bytes to send. An uninitialised or zero-length array is a no-op. For typed multi-field payloads the recommended carrier is [**PropertyBag**](../VBRUN/PropertyBag/) — see [Recommended payload encoding: `PropertyBag`](.#propertybag-carrier) on the package overview. *Cookie* : *optional* A **Variant** correlation value, surfaced as the *Cookie* parameter of the matching [**ClientMessageSent**](NamedPipeServer#clientmessagesent) event. Default **Empty**. Returns immediately; the actual transmission runs through the IOCP loop. The completion fires [**ClientMessageSent**](NamedPipeServer#clientmessagesent) on the parent server. +```tb +' Reply to a request using the PropertyBag convention: +Dim reply As New PropertyBag +reply.WriteProperty "ResponseCommandID", "WHAT_TIME_IS_IT" +reply.WriteProperty "ResponseData", Time() +Connection.AsyncWrite reply.Contents +``` + To send the same message to every connected client at once, use [**NamedPipeServer.AsyncBroadcast**](NamedPipeServer#asyncbroadcast). ## See Also - [WinNamedPipesLib package](.) -- overview, IOCP / event-marshalling architecture, cookie pattern, `Data()` lifetime caveat +- [Recommended payload encoding: `PropertyBag`](.#propertybag-carrier) -- the deep-copy capture / typed-payload convention for messages - [NamedPipeServer class](NamedPipeServer) -- the parent server that owns this connection - [NamedPipeClientConnection class](NamedPipeClientConnection) -- the client-side counterpart diff --git a/docs/Reference/WinNamedPipesLib/index.md b/docs/Reference/WinNamedPipesLib/index.md index 01df3a8..a473276 100644 --- a/docs/Reference/WinNamedPipesLib/index.md +++ b/docs/Reference/WinNamedPipesLib/index.md @@ -38,6 +38,42 @@ Setting [**FreeThreadingEvents**](NamedPipeServer#freethreadingevents) to **True The flag must be set before [**Start**](NamedPipeServer#start) (server side) or before the first [**Connect**](NamedPipeClientManager#connect) call (client side); it is read once and propagated to every per-connection object. +## Hosting inside a Windows service +{: #service-host-idiom } + +Windows services that host a [**NamedPipeServer**](NamedPipeServer) run into the message-loop dependency described above: the service entry-point thread is not pumping messages by default, so the marshalled-event delivery has nothing to dispatch through. The package provides [**NamedPipeServer.ManualMessageLoopEnter**](NamedPipeServer#manualmessageloopenter) / [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave) for exactly this case. + +The canonical pattern: the service-thread entry-point opens the server, transitions the service to `Running`, blocks inside [**ManualMessageLoopEnter**](NamedPipeServer#manualmessageloopenter), and only leaves the loop when a control-code handler running on the *other* (dispatcher) thread calls [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave) on the same server instance. + +```tb +' On the service-entry-point thread: +Set NamedPipeServer = New NamedPipeServer +NamedPipeServer.PipeName = "MyServicePipe" + +' (tell the SCM we're running, then block on the message loop) +ServiceManager.ReportStatus vbServiceStatusRunning +NamedPipeServer.Start +NamedPipeServer.ManualMessageLoopEnter ' blocks until ManualMessageLoopLeave +NamedPipeServer.Stop + +ServiceManager.ReportStatus vbServiceStatusStopped + +' On the dispatcher thread (an ITbService.ChangeState handler): +Select Case dwControl + Case vbServiceControlStop, vbServiceControlShutdown + ServiceManager.ReportStatus vbServiceStatusStopPending + NamedPipeServer.ManualMessageLoopLeave ' wakes the service thread out of ManualMessageLoopEnter +End Select +``` + +Three facts worth pulling out: + +- The service entry-point and the control-code handler run on **different threads**. The shared [**NamedPipeServer**](NamedPipeServer) member field is what they coordinate through; the handler calls [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave) on it to wake the entry-point. +- [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave) is the only way to exit [**ManualMessageLoopEnter**](NamedPipeServer#manualmessageloopenter) cleanly. There is no timeout and no second blocking primitive. Services that need to react to other wake-up sources (e.g. a *Pause* control code) set a shared `Public` flag *then* call [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave) to break out, inspect the flag, and re-enter the loop or proceed to shutdown. +- [**FreeThreadingEvents**](NamedPipeServer#freethreadingevents) = **False** (the default) is **required** for this pattern. Setting it to **True** would deliver events directly on the IOCP worker thread and bypass the manual loop entirely — the pipe still works, but `ManualMessageLoopEnter` / `Leave` become irrelevant. Pick one mode and stay with it. + +The non-service equivalent — hosting the same [**NamedPipeServer**](NamedPipeServer) inside a Form — is simpler: the Form's regular message loop pumps the marshalling window automatically, so the Form calls [**Start**](NamedPipeServer#start) in `Form_Load`, [**Stop**](NamedPipeServer#stop) in `Form_Unload`, and never touches [**ManualMessageLoopEnter**](NamedPipeServer#manualmessageloopenter) / [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave). Either pattern works; the service-host pattern is the one that needs the manual pump. + ## Asynchronous reads When [**ContinuouslyReadFromPipe**](NamedPipeServer#continuouslyreadfrompipe) is **True** (the default), the package keeps a read pending against every connection at all times — every [**ClientMessageReceived**](NamedPipeServer#clientmessagereceived) / [**MessageReceived**](NamedPipeClientConnection#messagereceived) event is followed by another `AsyncRead` issued from inside the IOCP thread. Set the flag to **False** to handle reads one-at-a-time: each event handler must then call [**NamedPipeServerConnection.AsyncRead**](NamedPipeServerConnection#asyncread) / [**NamedPipeClientConnection.AsyncRead**](NamedPipeClientConnection#asyncread) to receive the next message. This is useful for back-pressure when the consumer can't process messages as fast as they arrive. @@ -79,6 +115,36 @@ ReDim Stored(UBound(Data)) For a text payload, `StrConv(Data, vbUnicode)` (UTF-8) or `CStr` over a `vbUnicode`-converted copy reads the bytes immediately and produces an owned **String** in one step. +## Recommended payload encoding: `PropertyBag` +{: #propertybag-carrier } + +The package transports raw bytes; it is agnostic about what is inside them. For non-trivial protocols the recommended carrier is [**PropertyBag**](../VBRUN/PropertyBag/) — twinBASIC's built-in keyed-property serialiser. Two reasons: + +1. **`PropertyBag.Contents` deep-copies the bytes**, which is the cleanest answer to the transient-`Data()` lifetime caveat above. Assigning *Data* to a fresh **PropertyBag**'s **Contents** captures the buffer in one step; the copy is safe to keep past the event handler. +2. **`PropertyBag` provides typed multi-field payloads** without the consumer having to design a wire protocol. Both sides agree on property names (e.g. `"CommandID"`, `"ResponseCommandID"`, `"Data"`) and **PropertyBag** handles the byte-level encoding. + +```tb +' Sender: +Dim request As New PropertyBag +request.WriteProperty "CommandID", "WHAT_TIME_IS_IT" +connection.AsyncWrite request.Contents + +' Receiver — inside ClientMessageReceived / MessageReceived: +Dim incoming As New PropertyBag +incoming.Contents = Data ' deep-copies the bytes; safe to use past the handler + +Dim cmd As String = incoming.ReadProperty("CommandID") +Select Case cmd + Case "WHAT_TIME_IS_IT" + Dim reply As New PropertyBag + reply.WriteProperty "ResponseCommandID", cmd + reply.WriteProperty "ResponseData", Time() + Connection.AsyncWrite reply.Contents +End Select +``` + +Nothing in the package mandates **PropertyBag** — raw `Byte()` works too, and a custom wire format may be the right answer for very high-throughput scenarios. But the everyday case is well served by the **PropertyBag** convention and it solves the transient-`Data()` problem for free. + ## Closing a client connection > [!IMPORTANT] @@ -90,13 +156,28 @@ Either let the [**NamedPipeClientConnection**](NamedPipeClientConnection) object [**NamedPipeClientManager.FindNamedPipes**](NamedPipeClientManager#findnamedpipes) enumerates the named pipes published on the local machine, returning a **Collection** of **String** names that match an optional `*`/`?` wildcard pattern. The implementation is `FindFirstFileW("\\.\pipe\")` — the package strips the leading namespace itself, so pass just the pipe name (`"MyService*"`, not `"\\.\pipe\MyService*"`). -## Known limitations +Named pipes can appear and disappear at any time as their server processes start and stop, and the package does not publish an event for this. The canonical discovery loop is a low-frequency [**Timer**](../VB/Timer/) that repopulates a list and preserves the user's current selection — a few seconds between polls is the typical interval; the underlying `FindFirstFileW` is cheap enough that nothing finer is required: -These are open TODOs documented in the package's `_README.txt`. They are user-visible — surface them when designing a protocol on top of the package: +```tb +Private Sub timerRefreshNamedPipes_Timer() + Dim previousSelection As String = lstNamedPipes.List(lstNamedPipes.ListIndex) + lstNamedPipes.Clear + Dim restoredIndex As Long = -1 + Dim index As Long = 0 + Dim pipeName As Variant + For Each pipeName In manager.FindNamedPipes("MyService_*") + If pipeName = previousSelection Then restoredIndex = index + lstNamedPipes.AddItem pipeName + index = index + 1 + Next + If restoredIndex <> -1 Then lstNamedPipes.ListIndex = restoredIndex +End Sub +``` + +## Known limitations -- **No server-side disconnect of a single client.** [**NamedPipeServer.Stop**](NamedPipeServer#stop) tears down the whole pipe. There is no per-[**NamedPipeServerConnection**](NamedPipeServerConnection) `Close` method on the public surface yet, so a server cannot drop one misbehaving client while keeping the others connected. -- **No `Error` event on the IOCP worker thread.** Errors inside the IOCP loop currently surface as VBA run-time errors on whichever worker thread is running, not as a marshalled `Error` event on any of the four classes. Wrap I/O calls in **On Error Resume Next** where you need to keep the worker thread running on failure. -- **Message-size cap.** The author's TODO list flags *"remove max size 131072 of messages"*. [**MessageBufferSize**](NamedPipeServer#messagebuffersize) is the initial buffer size and the IOCP loop *does* grow it on `ERROR_MORE_DATA`, so messages larger than 128 KiB will work — but the TODO suggests there is a hard cap somewhere the author wants to remove. Treat very large messages as a roadmap item, not a guarantee. +- **No `Error` event is raised.** None of the four classes raises an `Error` event. Recognised IOCP failures (`ERROR_BROKEN_PIPE`, `ERROR_OPERATION_ABORTED`) drop the connection silently through the normal [**ClientDisconnected**](NamedPipeServer#clientdisconnected) / [**Disconnected**](NamedPipeClientConnection#disconnected) path — the consumer cannot distinguish a deliberate close from a transport failure. Worse, the client-side IOCP loop (`IOCPThreadClient` in `NamedPipeClientManager.twin`) contains a literal **Stop** statement on the branch for *unrecognised* error codes, which halts execution rather than reporting the error to consumer code. +- **Send is hard-capped at** [**MessageBufferSize**](NamedPipeServer#messagebuffersize) **bytes.** The receive path grows its buffer dynamically on `ERROR_MORE_DATA`, so reads of arbitrary size work. The send path does not: [**AsyncWrite**](NamedPipeServerConnection#asyncwrite) (and the client-side [**AsyncWrite**](NamedPipeClientConnection#asyncwrite)) copies the caller's `Byte()` *without a bounds-check* into a per-completion buffer sized at [**MessageBufferSize**](NamedPipeServer#messagebuffersize) (default **131072** bytes); the same goes for [**AsyncBroadcast**](NamedPipeServer#asyncbroadcast). A larger message overruns the buffer — likely a crash or heap corruption rather than a clean error. Raise [**MessageBufferSize**](NamedPipeServer#messagebuffersize) above your largest expected message *before* the first [**Start**](NamedPipeServer#start) (server) or [**Connect**](NamedPipeClientManager#connect) (client); the value is read once at that point and propagated to every per-connection buffer. ## Classes From 5806001b7c945b61ffe9d7c0ece6be954dbd8c41 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Thu, 14 May 2026 16:41:27 +0200 Subject: [PATCH 3/3] Document WinServicesLib. --- WIP.md | 4 +- docs/Reference/Packages.md | 1 + docs/Reference/WinEventLogLib/index.md | 2 +- docs/Reference/WinNamedPipesLib/index.md | 4 +- .../ServiceControlCodeConstants.md | 36 +++ .../Enumerations/ServiceStartConstants.md | 20 ++ .../Enumerations/ServiceStatusConstants.md | 24 ++ .../Enumerations/ServiceTypeConstants.md | 26 +++ .../WinServicesLib/Enumerations/index.md | 19 ++ docs/Reference/WinServicesLib/ITbService.md | 146 ++++++++++++ .../WinServicesLib/ServiceCreator.md | 57 +++++ .../WinServicesLib/ServiceManager.md | 213 ++++++++++++++++++ docs/Reference/WinServicesLib/ServiceState.md | 131 +++++++++++ docs/Reference/WinServicesLib/Services.md | 209 +++++++++++++++++ docs/Reference/WinServicesLib/index.md | 133 +++++++++++ 15 files changed, 1020 insertions(+), 5 deletions(-) create mode 100644 docs/Reference/WinServicesLib/Enumerations/ServiceControlCodeConstants.md create mode 100644 docs/Reference/WinServicesLib/Enumerations/ServiceStartConstants.md create mode 100644 docs/Reference/WinServicesLib/Enumerations/ServiceStatusConstants.md create mode 100644 docs/Reference/WinServicesLib/Enumerations/ServiceTypeConstants.md create mode 100644 docs/Reference/WinServicesLib/Enumerations/index.md create mode 100644 docs/Reference/WinServicesLib/ITbService.md create mode 100644 docs/Reference/WinServicesLib/ServiceCreator.md create mode 100644 docs/Reference/WinServicesLib/ServiceManager.md create mode 100644 docs/Reference/WinServicesLib/ServiceState.md create mode 100644 docs/Reference/WinServicesLib/Services.md create mode 100644 docs/Reference/WinServicesLib/index.md diff --git a/WIP.md b/WIP.md index adfcd6f..fc9f49f 100644 --- a/WIP.md +++ b/WIP.md @@ -4,7 +4,7 @@ Jekyll site (`just-the-docs` theme) deploying to `docs.twinbasic.com`. Source un ## Status -Reference documentation is **in progress**. Nine packages have full reference coverage adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, `.twin` source for the twinBASIC-specific packages); the CEF and WebView2 packages also carry a tutorial set. The WinServicesLib package is being added now. +Reference documentation is **complete**. All ten packages have full reference coverage adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, `.twin` source for the twinBASIC-specific packages); the CEF and WebView2 packages also carry a tutorial set. | Package | Reference | Tutorials | |--------------------------------------|-----------|-----------| @@ -17,7 +17,7 @@ Reference documentation is **in progress**. Nine packages have full reference co | cefPackage (CEF) | done | done | | WinEventLogLib | done | — | | WinNamedPipesLib | done | — | -| WinServicesLib | **WIP** | — | +| WinServicesLib | done | — | The rest of this file is the maintenance guide for adding new pages or updating existing ones — primary-source paths, page templates, cross-section linking conventions, the per-symbol workflow, and the integrity check. diff --git a/docs/Reference/Packages.md b/docs/Reference/Packages.md index 5e94127..1cb3b92 100644 --- a/docs/Reference/Packages.md +++ b/docs/Reference/Packages.md @@ -28,3 +28,4 @@ These packages are built into twinBASIC and are always available, even offline. - [WebView2 Package](WebView2/) -- the **WebView2** control wrapping the Microsoft Edge runtime, plus its surrounding wrapper objects (request / response / headers / environment options) and the `wv2…` enumerations - [WinEventLogLib Package](WinEventLogLib/) -- writes Windows Event Log entries from twinBASIC; the generic **EventLog**(*Of EventIds, Categories*) class handles registration, registry setup, and the per-event `ReportEventW` call, with message-table resources for *EventIds* and *Categories* synthesised into the EXE at compile time - [WinNamedPipesLib Package](WinNamedPipesLib/) -- Windows named pipes as twinBASIC objects with an asynchronous IOCP-driven I/O model; **NamedPipeServer** + **NamedPipeServerConnection** on the host side, **NamedPipeClientManager** + **NamedPipeClientConnection** on the client side, with message-boundary semantics and a cookie-based correlation pattern across `AsyncRead` / `AsyncWrite` and their matching events +- [WinServicesLib Package](WinServicesLib/) -- runs a twinBASIC EXE as one or more Windows services; the **Services** singleton coordinates configuration, install / uninstall, and the SCM dispatcher loop, while user-implemented [**ITbService**](WinServicesLib/ITbService) classes are surfaced through [**ServiceCreator**](WinServicesLib/ServiceCreator)`(Of T)` diff --git a/docs/Reference/WinEventLogLib/index.md b/docs/Reference/WinEventLogLib/index.md index 6f12630..12b5f92 100644 --- a/docs/Reference/WinEventLogLib/index.md +++ b/docs/Reference/WinEventLogLib/index.md @@ -78,7 +78,7 @@ Two things to remember: - The *T1* / *T2* type arguments must match between the `Implements` declaration and the constructor expression — the compiler enforces this. - Using `"Application\" & CurrentComponentName` as the *LogName* makes the log path automatically track the class name at compile time; renaming the class renames the source it logs to. -This is the canonical mix-in pattern for Windows-service classes (every service class in a project that shares one set of event IDs picks up logging methods without per-class boilerplate). The same pattern works for any class that wants the [**EventLog**](EventLog) surface inline. +This is the canonical mix-in pattern for [**WinServicesLib**](../WinServicesLib/) service classes (every service class in a project that shares one set of event IDs picks up logging methods without per-class boilerplate). The same pattern works for any class that wants the [**EventLog**](EventLog) surface inline. A class can use `Implements ... Via` on [**EventLog**](EventLog)`(Of T1, T2)` only **once**. When several classes in the same project need to share a logging surface, declare a single module with one event-ID enum and one category enum and `Implements ... Via` against that pair from every class. Multiple unrelated message tables are still possible — they just have to be reached through explicitly-named [**EventLog**](EventLog) fields rather than the `Implements ... Via` shortcut. diff --git a/docs/Reference/WinNamedPipesLib/index.md b/docs/Reference/WinNamedPipesLib/index.md index a473276..5408cd8 100644 --- a/docs/Reference/WinNamedPipesLib/index.md +++ b/docs/Reference/WinNamedPipesLib/index.md @@ -41,9 +41,9 @@ The flag must be set before [**Start**](NamedPipeServer#start) (server side) or ## Hosting inside a Windows service {: #service-host-idiom } -Windows services that host a [**NamedPipeServer**](NamedPipeServer) run into the message-loop dependency described above: the service entry-point thread is not pumping messages by default, so the marshalled-event delivery has nothing to dispatch through. The package provides [**NamedPipeServer.ManualMessageLoopEnter**](NamedPipeServer#manualmessageloopenter) / [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave) for exactly this case. +Windows services hosted through the [**WinServicesLib**](../WinServicesLib/) package run into the message-loop dependency described above when they also host a [**NamedPipeServer**](NamedPipeServer): the [**ITbService.EntryPoint**](../WinServicesLib/ITbService#entrypoint) thread is not pumping messages by default, so the marshalled-event delivery has nothing to dispatch through. The package provides [**NamedPipeServer.ManualMessageLoopEnter**](NamedPipeServer#manualmessageloopenter) / [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave) for exactly this case. -The canonical pattern: the service-thread entry-point opens the server, transitions the service to `Running`, blocks inside [**ManualMessageLoopEnter**](NamedPipeServer#manualmessageloopenter), and only leaves the loop when a control-code handler running on the *other* (dispatcher) thread calls [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave) on the same server instance. +The canonical pattern: [**ITbService.EntryPoint**](../WinServicesLib/ITbService#entrypoint) opens the server, transitions the service to `Running`, blocks inside [**ManualMessageLoopEnter**](NamedPipeServer#manualmessageloopenter), and only leaves the loop when [**ITbService.ChangeState**](../WinServicesLib/ITbService#changestate) — running on the *other* (dispatcher) thread — calls [**ManualMessageLoopLeave**](NamedPipeServer#manualmessageloopleave) on the same server instance. ```tb ' On the service-entry-point thread: diff --git a/docs/Reference/WinServicesLib/Enumerations/ServiceControlCodeConstants.md b/docs/Reference/WinServicesLib/Enumerations/ServiceControlCodeConstants.md new file mode 100644 index 0000000..aedbb6a --- /dev/null +++ b/docs/Reference/WinServicesLib/Enumerations/ServiceControlCodeConstants.md @@ -0,0 +1,36 @@ +--- +title: ServiceControlCodeConstants +parent: Enumerations +grand_parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/Enumerations/ServiceControlCodeConstants +--- +# ServiceControlCodeConstants +{: .no_toc } + +The control codes the SCM can deliver to a running service. Used as the *dwControl* parameter of [**ITbService.ChangeState**](../ITbService#changestate) (where the service reacts to the request) and as the *ControlCode* parameter of [**Services.ControlService**](../Services#controlservice) (where the consumer issues the request to a service running elsewhere). + +The values mirror the Win32 `SERVICE_CONTROL_*` constants verbatim — the `vb` prefix is a historical hold-over from VB6's coding conventions and does not affect the numeric values. + +| Constant | Value | Description | +|----------|-------|-------------| +| **vbServiceControlStop**{: #vbServiceControlStop } | 0x01 | Request the service stop. The standard shutdown signal — every service should handle this in [**ChangeState**](../ITbService#changestate). | +| **vbServiceControlPause**{: #vbServiceControlPause } | 0x02 | Pause the service. Only delivered if [**ServiceManager.SupportsPausing**](../ServiceManager#supportspausing) is **True**. | +| **vbServiceControlContinue**{: #vbServiceControlContinue } | 0x03 | Resume a paused service. Paired with [**vbServiceControlPause**](#vbServiceControlPause). | +| **vbServiceControlInterrogate**{: #vbServiceControlInterrogate } | 0x04 | The SCM is asking the service to re-report its current state via [**ReportStatus**](../ServiceManager#reportstatus). A bare-minimum handler can ignore it — the SCM already has the most recent status from the previous call. | +| **vbServiceControlShutdown**{: #vbServiceControlShutdown } | 0x05 | The OS is shutting down. Most services treat this identically to [**vbServiceControlStop**](#vbServiceControlStop). | +| **vbServiceControlParamChange**{: #vbServiceControlParamChange } | 0x06 | An admin has changed the service's configuration via `sc.exe config`. Delivered only if the service registered `SERVICE_ACCEPT_PARAMCHANGE` — currently not exposed through the package. | +| **vbServiceControlNetBindAdd**{: #vbServiceControlNetBindAdd } | 0x07 | A new network binding is available. Delivered only if `SERVICE_ACCEPT_NETBINDCHANGE` is accepted — currently not exposed. | +| **vbServiceControlNetBindRemove**{: #vbServiceControlNetBindRemove } | 0x08 | A network binding has been removed. | +| **vbServiceControlNetBindEnable**{: #vbServiceControlNetBindEnable } | 0x09 | A previously disabled network binding has been enabled. | +| **vbServiceControlNetBindDisable**{: #vbServiceControlNetBindDisable } | 0x0A | A network binding has been disabled. | +| **vbServiceControlDeviceEvent**{: #vbServiceControlDeviceEvent } | 0x0B | A device-arrival / -removal event. The *dwEventType* and *lpEventData* parameters of [**ChangeState**](../ITbService#changestate) carry the `DBT_*` sub-code and `DEV_BROADCAST_HDR` data. Only delivered when the service has accepted `SERVICE_ACCEPT_HARDWAREPROFILECHANGE`. | +| **vbServiceControlHardwareProfileChange**{: #vbServiceControlHardwareProfileChange } | 0x0C | A hardware-profile change (laptop docking, …). | +| **vbServiceControlPowerEvent**{: #vbServiceControlPowerEvent } | 0x0D | A system power event (suspend, resume, battery low, …). The *dwEventType* parameter carries the `PBT_*` sub-code. | +| **vbServiceControlSessionChange**{: #vbServiceControlSessionChange } | 0x0E | A session-change event (user logon, RDP connect / disconnect, …). The *dwEventType* parameter carries the `WTS_*` sub-code. | +| **vbServiceControlPreShutdown**{: #vbServiceControlPreShutdown } | 0x0F | The OS is about to shut down — sent before [**vbServiceControlShutdown**](#vbServiceControlShutdown) to services that have registered for the longer pre-shutdown notification window. | +| **vbServiceControlTimeChange**{: #vbServiceControlTimeChange } | 0x10 | The system time has changed. | +| **vbServiceControlTriggerEvent**{: #vbServiceControlTriggerEvent } | 0x20 | A registered trigger event has fired (typically used by trigger-started services). | +| **vbServiceControlLowResources**{: #vbServiceControlLowResources } | 0x60 | The service should reduce its memory / CPU footprint. | +| **vbServiceControlSystemLowResources**{: #vbServiceControlSystemLowResources } | 0x61 | The whole system is low on resources. | + +Control codes in the range **128–255** are reserved for user-defined codes; pass any value in that range to [**Services.ControlService**](../Services#controlservice) and the package will request the matching `SERVICE_USER_DEFINED_CONTROL` SCM permission. Most services only need to handle [**vbServiceControlStop**](#vbServiceControlStop) and optionally the pause / continue pair. diff --git a/docs/Reference/WinServicesLib/Enumerations/ServiceStartConstants.md b/docs/Reference/WinServicesLib/Enumerations/ServiceStartConstants.md new file mode 100644 index 0000000..cb89f01 --- /dev/null +++ b/docs/Reference/WinServicesLib/Enumerations/ServiceStartConstants.md @@ -0,0 +1,20 @@ +--- +title: ServiceStartConstants +parent: Enumerations +grand_parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/Enumerations/ServiceStartConstants +--- +# ServiceStartConstants +{: .no_toc } + +When and how the SCM starts a service. Assigned to [**ServiceManager.InstallStartMode**](../ServiceManager#installstartmode) at configuration time; the value is captured into the SCM database by [**ServiceManager.Install**](../ServiceManager#install) and can be changed afterwards through the Services control-panel applet or `sc.exe config`. + +| Constant | Value | Description | +|----------|-------|-------------| +| **tbServiceStartBoot**{: #tbServiceStartBoot } | 0 | Started by the boot loader at OS boot. **Kernel drivers only** — not applicable to twinBASIC services. | +| **tbServiceStartDriverSystem**{: #tbServiceStartDriverSystem } | 1 | Started by `Ntldr` / `Winload` during system initialisation. **Kernel drivers only.** | +| **tbServiceStartAuto**{: #tbServiceStartAuto } | 2 | Automatically started by the SCM at system boot, before any user logs in. The typical setting for a background service that should always be running. | +| **tbServiceStartOnDemand**{: #tbServiceStartOnDemand } | 3 | Started by the SCM only when something explicitly requests it (control-panel applet, `sc.exe start`, [**Services.LaunchService**](../Services#launchservice), or a service that lists it in [**DependentServices**](../ServiceManager#dependentservices)). The default for new [**ServiceManager**](../ServiceManager) instances. | +| **tbServiceStartDisabled**{: #tbServiceStartDisabled } | 4 | The service cannot be started until an administrator changes its start mode. Use this to deactivate a service without uninstalling it. | + +For user-mode twinBASIC services, the only three values that matter in practice are [**tbServiceStartAuto**](#tbServiceStartAuto), [**tbServiceStartOnDemand**](#tbServiceStartOnDemand), and [**tbServiceStartDisabled**](#tbServiceStartDisabled). diff --git a/docs/Reference/WinServicesLib/Enumerations/ServiceStatusConstants.md b/docs/Reference/WinServicesLib/Enumerations/ServiceStatusConstants.md new file mode 100644 index 0000000..5e45397 --- /dev/null +++ b/docs/Reference/WinServicesLib/Enumerations/ServiceStatusConstants.md @@ -0,0 +1,24 @@ +--- +title: ServiceStatusConstants +parent: Enumerations +grand_parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/Enumerations/ServiceStatusConstants +--- +# ServiceStatusConstants +{: .no_toc } + +The runtime-state values a service reports to the SCM through [**ServiceManager.ReportStatus**](../ServiceManager#reportstatus). The same numeric values are returned by [**ServiceState.CurrentState**](../ServiceState#currentstate) (typed as a plain **Long**) and rendered as text by [**ServiceState.CurrentStateText**](../ServiceState#currentstatetext). + +The values mirror the Win32 `SERVICE_*` state constants. The `vb` prefix is a historical hold-over from VB6's coding conventions. + +| Constant | Value | Description | +|----------|-------|-------------| +| **vbServiceStatusStopped**{: #vbServiceStatusStopped } | 1 | The service is not running. Set by the service immediately before [**EntryPoint**](../ITbService#entrypoint) returns; also the initial state the SCM stores when the service is registered. | +| **vbServiceStatusStartPending**{: #vbServiceStatusStartPending } | 2 | The service is starting up. Set by the package's dispatcher trampoline before it calls [**EntryPoint**](../ITbService#entrypoint); services with long start-up sequences should also re-report this state periodically together with a `WaitHint` so the SCM does not declare the service hung. | +| **vbServiceStatusStopPending**{: #vbServiceStatusStopPending } | 3 | The service has acknowledged a stop request and is shutting down. Typically reported from [**ChangeState**](../ITbService#changestate) immediately on receipt of [**vbServiceControlStop**](ServiceControlCodeConstants#vbServiceControlStop). | +| **vbServiceStatusRunning**{: #vbServiceStatusRunning } | 4 | The service has reached steady state. Reported from [**EntryPoint**](../ITbService#entrypoint) once initialisation is complete; this is what `services.msc` shows as "Running". | +| **vbServiceStatusContinuePending**{: #vbServiceStatusContinuePending } | 5 | The service has acknowledged a continue request and is resuming. Reported from [**ChangeState**](../ITbService#changestate) on [**vbServiceControlContinue**](ServiceControlCodeConstants#vbServiceControlContinue). | +| **vbServiceStatusPausePending**{: #vbServiceStatusPausePending } | 6 | The service has acknowledged a pause request. Reported from [**ChangeState**](../ITbService#changestate) on [**vbServiceControlPause**](ServiceControlCodeConstants#vbServiceControlPause). | +| **vbServiceStatusPaused**{: #vbServiceStatusPaused } | 7 | The service has reached the paused state. Reported from [**EntryPoint**](../ITbService#entrypoint) once the pause loop is active. | + +The typical state sequence for a simple service: [**vbServiceStatusStartPending**](#vbServiceStatusStartPending) (package) → [**vbServiceStatusRunning**](#vbServiceStatusRunning) (from [**EntryPoint**](../ITbService#entrypoint)) → [**vbServiceStatusStopPending**](#vbServiceStatusStopPending) (from [**ChangeState**](../ITbService#changestate)) → [**vbServiceStatusStopped**](#vbServiceStatusStopped) (from [**EntryPoint**](../ITbService#entrypoint), before returning). diff --git a/docs/Reference/WinServicesLib/Enumerations/ServiceTypeConstants.md b/docs/Reference/WinServicesLib/Enumerations/ServiceTypeConstants.md new file mode 100644 index 0000000..439e5ec --- /dev/null +++ b/docs/Reference/WinServicesLib/Enumerations/ServiceTypeConstants.md @@ -0,0 +1,26 @@ +--- +title: ServiceTypeConstants +parent: Enumerations +grand_parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/Enumerations/ServiceTypeConstants +--- +# ServiceTypeConstants +{: .no_toc } + +The Win32 service-type values. Read into [**ServiceManager.Type**](../ServiceManager#type) at configuration time and reported back by the SCM through [**ServiceState.Type**](../ServiceState#type) at query time. Determines whether the service runs in its own process, in a shared host process, or is a kernel driver — and, historically, whether it can interact with the desktop. + +| Constant | Value | Description | +|----------|-------|-------------| +| **tbServiceTypeKernelDriver**{: #tbServiceTypeKernelDriver } | 1 | A kernel-mode driver. **Not applicable** to twinBASIC services — kernel drivers are written in C and built against the Windows DDK. | +| **tbServiceTypeSystemDriver**{: #tbServiceTypeSystemDriver } | 2 | A file-system kernel driver. Same caveat as above. | +| **tbServiceTypeAdapter**{: #tbServiceTypeAdapter } | 4 | Legacy adapter service (network-adapter binding). Not used by modern services. | +| **tbServiceTypeRecognizerDriver**{: #tbServiceTypeRecognizerDriver } | 8 | A file-system-recognizer driver. Kernel-only. | +| **tbServiceTypeOwnProcess**{: #tbServiceTypeOwnProcess } | 16 | The service runs in its own dedicated EXE process. The typical setting for a one-service EXE. | +| **tbServiceTypeShareProcess**{: #tbServiceTypeShareProcess } | 32 | The service runs alongside other services in a shared host EXE. Used when one EXE hosts multiple distinct services (`ConfigureNew` called more than once); the SCM keeps a single process alive that serves all of them. | +| **tbServiceTypeOwnProcessInteractive**{: #tbServiceTypeOwnProcessInteractive } | 272 | `tbServiceTypeOwnProcess` with the interactive bit (`SERVICE_INTERACTIVE_PROCESS`) set. **Not supported on Windows Vista and later** — see note below. | +| **tbServiceTypeShareProcessInteractive**{: #tbServiceTypeShareProcessInteractive } | 288 | `tbServiceTypeShareProcess` with the interactive bit set. Same caveat as above. | + +> [!NOTE] +> Windows Vista and later run services in *Session 0*, which has no user desktop and no message-loop interaction with logged-in users — the `SERVICE_INTERACTIVE_PROCESS` flag is **silently ignored**. The `Interactive` constants are kept in the enum for compatibility, but services that need user-interface elements should use a separate UI process (launched via the Services package or via inter-process communication) rather than the interactive-service mechanism. + +For user-mode twinBASIC services, the only two values that matter in practice are [**tbServiceTypeOwnProcess**](#tbServiceTypeOwnProcess) and [**tbServiceTypeShareProcess**](#tbServiceTypeShareProcess). diff --git a/docs/Reference/WinServicesLib/Enumerations/index.md b/docs/Reference/WinServicesLib/Enumerations/index.md new file mode 100644 index 0000000..231eb1c --- /dev/null +++ b/docs/Reference/WinServicesLib/Enumerations/index.md @@ -0,0 +1,19 @@ +--- +title: Enumerations +parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/Enumerations/ +has_toc: false +--- + +# Enumerations + +The four user-facing enumerations the **WinServicesLib** package exposes. All four come from the public `ServicesConstantsPublic` module in the package source; the larger surface of internal `SERVICE_*` constants the source uses to call into `advapi32.dll` lives in a `Private Module` and is not part of the public API. + +| Enumeration | Used by | +|-------------|---------| +| [ServiceTypeConstants](ServiceTypeConstants) | [**ServiceManager.Type**](../ServiceManager#type), [**ServiceState.Type**](../ServiceState#type) | +| [ServiceStartConstants](ServiceStartConstants) | [**ServiceManager.InstallStartMode**](../ServiceManager#installstartmode) | +| [ServiceControlCodeConstants](ServiceControlCodeConstants) | [**Services.ControlService**](../Services#controlservice), the *dwControl* parameter of [**ITbService.ChangeState**](../ITbService#changestate) | +| [ServiceStatusConstants](ServiceStatusConstants) | [**ServiceManager.ReportStatus**](../ServiceManager#reportstatus) | + +The member-name prefixes are inherited from the underlying Win32 SDK constants — `tb…` on the *configuration* enums ([**ServiceTypeConstants**](ServiceTypeConstants), [**ServiceStartConstants**](ServiceStartConstants)) and `vb…` on the *runtime* enums ([**ServiceControlCodeConstants**](ServiceControlCodeConstants), [**ServiceStatusConstants**](ServiceStatusConstants)). The split is not deliberate; treat the prefixes as part of the member names and ignore the asymmetry. diff --git a/docs/Reference/WinServicesLib/ITbService.md b/docs/Reference/WinServicesLib/ITbService.md new file mode 100644 index 0000000..f418676 --- /dev/null +++ b/docs/Reference/WinServicesLib/ITbService.md @@ -0,0 +1,146 @@ +--- +title: ITbService +parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/ITbService +has_toc: false +--- + +# ITbService interface +{: .no_toc } + +The contract every service class in a **WinServicesLib** project implements. Three subs, each invoked at a specific point in the service's lifecycle: + +- [**EntryPoint**](#entrypoint) — runs the service's actual work. +- [**StartupFailed**](#startupfailed) — invoked when the SCM handshake fails before [**EntryPoint**](#entrypoint) can run. +- [**ChangeState**](#changestate) — invoked when the SCM delivers a control code (*Stop*, *Pause*, *Continue*, …). + +The package's [**ServiceCreator**](ServiceCreator)`(Of T)` factory creates one instance per service start; the dispatcher trampoline holds the instance for the lifetime of the service and routes the three lifecycle subs to it. + +```tb +[COMCreatable(False)] +Class MyService + Implements ITbService + + Public IsStopping As Boolean + + Sub EntryPoint(ByVal ServiceManager As ServiceManager) _ + Implements ITbService.EntryPoint + ServiceManager.ReportStatus vbServiceStatusRunning + Do Until IsStopping + ' ...do work, then yield with WaitForSingleObject / Sleep / etc. + Loop + ServiceManager.ReportStatus vbServiceStatusStopped + End Sub + + Sub ChangeState(ByVal ServiceManager As ServiceManager, _ + ByVal dwControl As ServiceControlCodeConstants, _ + ByVal dwEventType As Long, _ + ByVal lpEventData As LongPtr) _ + Implements ITbService.ChangeState + Select Case dwControl + Case vbServiceControlStop, vbServiceControlShutdown + ServiceManager.ReportStatus vbServiceStatusStopPending + IsStopping = True + End Select + End Sub + + Sub StartupFailed(ByVal ServiceManager As ServiceManager) _ + Implements ITbService.StartupFailed + ' …optional failure-reporting hook + End Sub +End Class +``` + +> [!IMPORTANT] +> [**EntryPoint**](#entrypoint) runs on the **service thread**. [**ChangeState**](#changestate) runs on the **dispatcher thread** (the EXE's main thread). The two methods execute concurrently and must coordinate through shared `Public` flags on the class — see [The two-thread split](.#two-thread-split) on the package overview. + +* TOC +{:toc} + +## Methods + +### ChangeState +{: .no_toc } + +Invoked by the SCM dispatcher thread when a control code is delivered to the service. + +Syntax: *service*.**ChangeState** *ServiceManager*, *dwControl*, *dwEventType*, *lpEventData* + +*ServiceManager* +: The [**ServiceManager**](ServiceManager) for this service — the same instance passed to [**EntryPoint**](#entrypoint). Use it to call [**ReportStatus**](ServiceManager#reportstatus) acknowledging the pending transition. + +*dwControl* +: A [**ServiceControlCodeConstants**](Enumerations/ServiceControlCodeConstants) value identifying the control. Standard codes the SCM may deliver include [**vbServiceControlStop**](Enumerations/ServiceControlCodeConstants#vbServiceControlStop), [**vbServiceControlShutdown**](Enumerations/ServiceControlCodeConstants#vbServiceControlShutdown), [**vbServiceControlPause**](Enumerations/ServiceControlCodeConstants#vbServiceControlPause), [**vbServiceControlContinue**](Enumerations/ServiceControlCodeConstants#vbServiceControlContinue), [**vbServiceControlInterrogate**](Enumerations/ServiceControlCodeConstants#vbServiceControlInterrogate), and the event-bearing codes ([**vbServiceControlSessionChange**](Enumerations/ServiceControlCodeConstants#vbServiceControlSessionChange), [**vbServiceControlPowerEvent**](Enumerations/ServiceControlCodeConstants#vbServiceControlPowerEvent), [**vbServiceControlDeviceEvent**](Enumerations/ServiceControlCodeConstants#vbServiceControlDeviceEvent), [**vbServiceControlHardwareProfileChange**](Enumerations/ServiceControlCodeConstants#vbServiceControlHardwareProfileChange)). User-defined codes in the range 128–255 can also be delivered through [**Services.ControlService**](Services#controlservice). + +*dwEventType* +: A **Long** carrying the event-type sub-code for the codes that have one. **0** otherwise. See Microsoft's `HandlerEx` documentation for the per-code interpretation. + +*lpEventData* +: A **LongPtr** to an event-specific data structure for the codes that have one. `vbNullPtr` otherwise. + +The typical pattern is a `Select Case dwControl` that handles the codes the service cares about and ignores the rest. The minimum a service needs to handle is *Stop*: + +```tb +Select Case dwControl + Case vbServiceControlStop, vbServiceControlShutdown + ServiceManager.ReportStatus vbServiceStatusStopPending + IsStopping = True ' signal the service thread +End Select +``` + +[**ChangeState**](#changestate) **does not stop** [**EntryPoint**](#entrypoint) — it only delivers the SCM's request. The user's code is responsible for the actual shutdown logic, typically by setting a shared `Public` flag the service thread polls (`IsStopping`) or by calling a signal method on a blocking primitive that [**EntryPoint**](#entrypoint) owns (`NamedPipeServer.ManualMessageLoopLeave`, `SetEvent` on a Win32 event handle, ...). + +The method runs on a different thread than [**EntryPoint**](#entrypoint); see [The two-thread split](.#two-thread-split) for the coordination rules. + +### EntryPoint +{: .no_toc } + +The service's main routine. Invoked by the package's dispatcher trampoline on the SCM-spawned service thread once the SCM handshake has completed and the trampoline has reported [**vbServiceStatusStartPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStartPending). + +Syntax: *service*.**EntryPoint** *ServiceManager* + +*ServiceManager* +: The [**ServiceManager**](ServiceManager) for this service. Carries the configuration that was set during `Sub Main` plus the runtime [**LaunchArgs**](ServiceManager#launchargs) the SCM passed in. Use it to call [**ReportStatus**](ServiceManager#reportstatus) on every state transition. + +The body of **EntryPoint** is the service's actual work. The minimum responsibilities: + +1. Optionally validate startup conditions (typically by inspecting [**LaunchArgs**](ServiceManager#launchargs)). Failure paths should call `ServiceManager.ReportStatus vbServiceStatusStopped, ` and `Exit Sub`. +2. Call `ServiceManager.ReportStatus vbServiceStatusRunning` once steady-state is reached. +3. Run the service's long-running loop. The loop typically blocks on something (a `WaitForSingleObject` on a manual-reset event, a `NamedPipeServer.ManualMessageLoopEnter`, a custom message loop, ...) and breaks out when [**ChangeState**](#changestate) signals shutdown through a shared flag. +4. Call `ServiceManager.ReportStatus vbServiceStatusStopped` before returning. + +After the **EntryPoint** sub returns, the service thread exits and the SCM marks the service as stopped. + +> [!IMPORTANT] +> **EntryPoint** runs on the **service thread**, not the dispatcher thread. The two threads execute concurrently for the lifetime of the service. Use shared `Public` flags on the implementing class (`IsStopping`, `IsPaused`, …) to coordinate state changes triggered from [**ChangeState**](#changestate). + +### StartupFailed +{: .no_toc } + +Invoked when the SCM handshake fails before [**EntryPoint**](#entrypoint) can run. + +Syntax: *service*.**StartupFailed** *ServiceManager* + +*ServiceManager* +: The [**ServiceManager**](ServiceManager) for this service. + +This sub fires when `RegisterServiceCtrlHandlerExW` returns a zero handle — typically because the service was launched outside the SCM context, or the SCM's `RegisterServiceCtrlHandlerExW` rejected the registration. The service has no SCM status handle in this state, so [**ServiceManager.ReportStatus**](ServiceManager#reportstatus) cannot be called from inside **StartupFailed** — calling it raises run-time error 5. + +The typical implementation is a logging-only hook so the failure is recorded somewhere a developer can find it later: + +```tb +Sub StartupFailed(ByVal ServiceManager As ServiceManager) _ + Implements ITbService.StartupFailed + LogFailure service_startup_failed, status_changed, CurrentComponentName +End Sub +``` + +If you have no useful failure-reporting hook to add, an empty implementation is fine — the SCM has already given up at this point and no recovery is possible. + +## See Also + +- [WinServicesLib package](.) -- overview, lifecycle, [the two-thread split](.#two-thread-split) +- [ServiceManager class](ServiceManager) -- the per-service object passed into every method +- [ServiceCreator(Of T) class](ServiceCreator) -- the factory that creates an **ITbService** instance for each service start +- [ServiceControlCodeConstants enum](Enumerations/ServiceControlCodeConstants) -- the values **ChangeState** dispatches on +- [ServiceStatusConstants enum](Enumerations/ServiceStatusConstants) -- the values **EntryPoint** reports through [**ServiceManager.ReportStatus**](ServiceManager#reportstatus) diff --git a/docs/Reference/WinServicesLib/ServiceCreator.md b/docs/Reference/WinServicesLib/ServiceCreator.md new file mode 100644 index 0000000..56317ff --- /dev/null +++ b/docs/Reference/WinServicesLib/ServiceCreator.md @@ -0,0 +1,57 @@ +--- +title: ServiceCreator +parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/ServiceCreator +has_toc: false +--- + +# ServiceCreator(Of T) class +{: .no_toc } + +The generic factory the **WinServicesLib** dispatcher uses to instantiate the user's service class when the SCM launches a service. *T* is the user's [**ITbService**](ITbService) implementation; **ServiceCreator(Of T)** wraps `New T` in a factory the dispatcher can hold by interface. + +Syntax: **New ServiceCreator(Of** *T* **)** + +*T* +: *required* A user class that implements [**ITbService**](ITbService). The constraint is *practical* rather than syntactic — there is no `Where T : ITbService` clause in the source, but the factory's `CreateInstance` returns `New T As ITbService`, which the compiler only accepts when *T* implements [**ITbService**](ITbService). + +The class is the value typically assigned to [**ServiceManager.InstanceCreator**](ServiceManager#instancecreator): + +```tb +With Services.ConfigureNew + .Name = "MyService" + .InstanceCreator = New ServiceCreator(Of MyService) ' MyService Implements ITbService +End With +``` + +The package keeps the factory by reference; the dispatcher trampoline calls [**CreateInstance**](#createinstance) once per service start, immediately after the SCM has spawned the service thread. The returned instance is the object whose [**EntryPoint**](ITbService#entrypoint), [**StartupFailed**](ITbService#startupfailed), and [**ChangeState**](ITbService#changestate) methods the package will route to. + +See the package [overview](.) for where **ServiceCreator** fits in the broader lifecycle. + +* TOC +{:toc} + +## Methods + +### CreateInstance +{: .no_toc } + +Returns a fresh `New T` cast as [**ITbService**](ITbService). + +Syntax: *creator*.**CreateInstance** **As** [**ITbService**](ITbService) + +User code rarely calls **CreateInstance** directly; the package's dispatcher trampoline invokes it once per service start. The returned instance is owned by the dispatcher for the lifetime of the service — it is released when the service stops or the dispatcher exits. + +The method has no parameters; if the user's service class needs configuration, it should read it from the [**ServiceManager**](ServiceManager) passed to [**EntryPoint**](ITbService#entrypoint) rather than from constructor arguments. + +## Why a factory rather than a class reference + +`ServiceCreator(Of T)` exists because the SCM dispatch model needs *deferred* instantiation. The configuration phase runs in `Sub Main` before the SCM has decided which services to start; constructing the service class eagerly there would create an unnecessary instance for services the SCM may never launch (or launch only much later). The factory defers the `New T` call until the service actually starts. + +The same indirection lets the dispatcher pair the [**ITbService**](ITbService) instance with the [**ServiceManager**](ServiceManager) one-to-one — the trampoline can pass the service-specific [**ServiceManager**](ServiceManager) into [**EntryPoint**](ITbService#entrypoint) without the service class having to know about the manager at construction time. + +## See Also + +- [WinServicesLib package](.) -- overview, lifecycle, two-thread split +- [ITbService interface](ITbService) -- the contract *T* must implement +- [ServiceManager class](ServiceManager) -- the per-service configuration; **ServiceCreator** instances are assigned to its [**InstanceCreator**](ServiceManager#instancecreator) field diff --git a/docs/Reference/WinServicesLib/ServiceManager.md b/docs/Reference/WinServicesLib/ServiceManager.md new file mode 100644 index 0000000..78dea9e --- /dev/null +++ b/docs/Reference/WinServicesLib/ServiceManager.md @@ -0,0 +1,213 @@ +--- +title: ServiceManager +parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/ServiceManager +has_toc: false +--- + +# ServiceManager class +{: .no_toc } + +The per-service configuration object. One [**ServiceManager**](.) describes one Windows service the EXE knows how to host — its [**Name**](#name), [**Description**](#description), service [**Type**](#type), [**InstallStartMode**](#installstartmode), [**InstanceCreator**](#instancecreator), and the optional fields the SCM cares about — and exposes the methods that act on a single service: [**Install**](#install), [**Uninstall**](#uninstall), and the [**ReportStatus**](#reportstatus) call the service uses to inform the SCM of state transitions while running. + +> [!NOTE] +> Do not construct **ServiceManager** instances directly. Call [**Services.ConfigureNew**](Services#configurenew) instead — it allocates a fresh manager and registers it in the package's internal collection so the dispatcher can find it. + +```tb +With Services.ConfigureNew + .Name = "MyService" + .Description = "An example twinBASIC service" + .Type = tbServiceTypeOwnProcess + .InstallStartMode = tbServiceStartOnDemand + .InstallCmdLine = """" & App.ModulePath & """ -startService" + .InstanceCreator = New ServiceCreator(Of MyService) +End With +``` + +See the package [overview](.) for the broader lifecycle, the [two-thread split](.#two-thread-split), and the elevation rules around installation. + +* TOC +{:toc} + +## Fields + +### LaunchArgs +{: .no_toc } + +The launch-time arguments the SCM forwarded to the service. **String()**. Populated by the package's dispatcher trampoline when the SCM invokes the service-thread entry-point; *not* a configuration field. The service-thread [**ITbService.EntryPoint**](ITbService#entrypoint) reads it to discover the arguments that [**Services.LaunchService**](Services#launchservice) (or the SCM, or `sc.exe`) passed in. + +`LaunchArgs(0)` is the *first user-supplied* argument — the SCM-supplied service name that comes through as `argv[0]` is dropped before the array is populated, so the indexing matches the caller's mental model. + +```tb +Sub EntryPoint(ByVal ServiceManager As ServiceManager) _ + Implements ITbService.EntryPoint + If Join(ServiceManager.LaunchArgs) <> "MySecretPassword" Then + ServiceManager.ReportStatus vbServiceStatusStopped, &H12345678 + Exit Sub + End If + ' ...steady-state work +End Sub +``` + +## Properties + +### AutoInitializeCOM +{: .no_toc } + +Whether the dispatcher trampoline calls `CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)` on the service thread before invoking [**ITbService.EntryPoint**](ITbService#entrypoint). **Boolean**, default **True**. + +Set to **False** if the service needs a different apartment model — for example, a service that creates an MTA worker pool. The service must then call `CoInitializeEx` itself from its [**EntryPoint**](ITbService#entrypoint) before touching COM-aware objects. + +### Description +{: .no_toc } + +The human-readable description listed in `services.msc` and `sc.exe query`. **String**, no default. + +The value is written to the SCM by [**Install**](#install) via `ChangeServiceConfig2W(SERVICE_CONFIG_DESCRIPTION)` and is applied to a fresh service or refreshed on every re-install. Set it before calling [**Install**](#install); changing the field at run-time has no effect until the next install. + +### DependentServices +{: .no_toc } + +A list of service names this service depends on. **Variant()**, no default. + +Pass an `Array("OtherSvc1", "OtherSvc2")`. When the SCM is asked to start the service, it auto-starts the listed dependencies first; if any dependency fails to start, the SCM aborts the start of this service. The package packs the array into the double-null-terminated string that `CreateServiceW` expects. + +```tb +.DependentServices = Array("MSMQ", "LanmanServer") +``` + +### InstallCmdLine +{: .no_toc } + +The command line the SCM will use when launching the service-host EXE. **String**, default `""""""` (the running EXE path, quoted). + +The default suffices only when the EXE always wants to run as a service. The conventional pattern is to **override the default** to add a discriminator argument so the EXE's `Sub Main` can tell which mode it is in: + +```tb +.InstallCmdLine = """" & App.ModulePath & """ -startService" +``` + +The matching `If InStr(Command, "-startService") > 0 Then Services.RunServiceDispatcher` branch in `Sub Main` is what makes the same EXE work both as installer / control-panel UI (when launched normally) and as service host (when launched by the SCM). + +The value is captured into the SCM database at [**Install**](#install) time. Changing it after install requires uninstalling and re-installing the service. + +### InstallStartMode +{: .no_toc } + +The SCM start mode the service is registered with. [**ServiceStartConstants**](Enumerations/ServiceStartConstants), default [**tbServiceStartOnDemand**](Enumerations/ServiceStartConstants#tbServiceStartOnDemand). + +Typical settings: + +- [**tbServiceStartOnDemand**](Enumerations/ServiceStartConstants#tbServiceStartOnDemand) — the service is **not** started automatically; user / installer / [**Services.LaunchService**](Services#launchservice) starts it on demand. +- [**tbServiceStartAuto**](Enumerations/ServiceStartConstants#tbServiceStartAuto) — the SCM starts the service at system boot. +- [**tbServiceStartDisabled**](Enumerations/ServiceStartConstants#tbServiceStartDisabled) — the service cannot be started until its start mode is changed. + +The driver-only modes ([**tbServiceStartBoot**](Enumerations/ServiceStartConstants#tbServiceStartBoot), [**tbServiceStartDriverSystem**](Enumerations/ServiceStartConstants#tbServiceStartDriverSystem)) are not meaningful for user-mode twinBASIC services. + +### InstanceCreator +{: .no_toc } + +The factory the dispatcher uses to create the [**ITbService**](ITbService) instance for this service when the SCM launches it. [**IServiceCreator**](ServiceCreator), no default. + +Set this to `New ServiceCreator(Of MyServiceClass)` where `MyServiceClass` is the user's [**ITbService**](ITbService) implementation: + +```tb +.InstanceCreator = New ServiceCreator(Of MyService) +``` + +[**RunServiceDispatcher**](Services#runservicedispatcher) calls `InstanceCreator.CreateInstance()` once per service start. **InstanceCreator** is read-write — the underlying private interface accepts both **Let** and **Set** assignment, so either syntax works. + +If only [**Install**](#install) / [**Uninstall**](#uninstall) need to run (e.g. inside a stand-alone installer), **InstanceCreator** can be left **Nothing** — the dispatcher only needs it when the SCM actually starts the service. + +### Name +{: .no_toc } + +The service's name in the SCM database, used by `services.msc` and `sc.exe`. **String**, no default. + +The name is what the SCM stores at `HKLM\SYSTEM\CurrentControlSet\Services\`; it is also the name [**Services.LaunchService**](Services#launchservice), [**Services.ControlService**](Services#controlservice), and [**Services.QueryStateOfService**](Services#querystateofservice) take as their *ServiceName* parameter. The same value is used for the SCM's *DisplayName* — the package does not currently expose a distinct display name. + +### SupportsPausing +{: .no_toc } + +Whether the SCM is told that the service accepts [**SERVICE_CONTROL_PAUSE**](Enumerations/ServiceControlCodeConstants#vbServiceControlPause) / [**SERVICE_CONTROL_CONTINUE**](Enumerations/ServiceControlCodeConstants#vbServiceControlContinue) notifications. **Boolean**, default **False**. + +Setting this property immediately resyncs the cached `SERVICE_STATUS` to the SCM via `SetServiceStatus`, so toggling it from inside [**EntryPoint**](ITbService#entrypoint) — once the service is past the `StartPending` phase — takes effect on the next SCM query. Most services that support pausing set the property to **True** at the top of [**EntryPoint**](ITbService#entrypoint) and handle [**vbServiceControlPause**](Enumerations/ServiceControlCodeConstants#vbServiceControlPause) / [**vbServiceControlContinue**](Enumerations/ServiceControlCodeConstants#vbServiceControlContinue) in [**ChangeState**](ITbService#changestate). + +If the service has not yet reached the started state when **SupportsPausing** is set, the resync raises run-time error 5 *"Can't update the service state until the service has started"*. Wait until after the first [**ReportStatus**](#reportstatus)`(vbServiceStatusRunning)` call before toggling the property. + +### Type +{: .no_toc } + +The Win32 service type — controls whether the service runs in its own process, in a shared process, or is a kernel driver. [**ServiceTypeConstants**](Enumerations/ServiceTypeConstants), default [**tbServiceTypeOwnProcess**](Enumerations/ServiceTypeConstants#tbServiceTypeOwnProcess). + +Typical settings: + +- [**tbServiceTypeOwnProcess**](Enumerations/ServiceTypeConstants#tbServiceTypeOwnProcess) — one service per EXE. +- [**tbServiceTypeShareProcess**](Enumerations/ServiceTypeConstants#tbServiceTypeShareProcess) — multiple services hosted in a single EXE; the SCM keeps one process alive that serves all of them. Each [**ServiceManager**](.) still needs its own configuration and [**InstanceCreator**](#instancecreator). + +The driver-only modes ([**tbServiceTypeSystemDriver**](Enumerations/ServiceTypeConstants#tbServiceTypeSystemDriver), [**tbServiceTypeKernelDriver**](Enumerations/ServiceTypeConstants#tbServiceTypeKernelDriver), …) are not meaningful for user-mode twinBASIC services. + +## Methods + +### Install +{: .no_toc } + +Registers this service in the SCM database. + +Syntax: *manager*.**Install** + +Opens the SCM with `SC_MANAGER_CONNECT Or SC_MANAGER_CREATE_SERVICE`, calls `CreateServiceW` with the configured fields. If a service with the same [**Name**](#name) already exists, the method deletes it first (via `OpenServiceW(SERVICE_DELETE)` + `DeleteService`) and retries — so calling [**Install**](#install) on a service that already exists overwrites the existing registration rather than failing. On a successful create the [**Description**](#description) is written via `ChangeServiceConfig2W(SERVICE_CONFIG_DESCRIPTION)`. + +> [!IMPORTANT] +> [**Install**](#install) writes to the SCM database, which requires administrator rights. The usual pattern is to call it once from an elevated installer, not from the application's normal startup path. Running from within the twinBASIC IDE typically fails — the IDE is rarely elevated. + +Raises run-time error 5 with a descriptive message on permission failure (*"Unable to open the Service manager..."*) or unrecoverable create failure (*"CreateServiceW() failed with error code "*). + +### ReportStatus +{: .no_toc } + +Informs the SCM of the service's current state. Called by the service from inside [**ITbService.EntryPoint**](ITbService#entrypoint) (and from [**ITbService.ChangeState**](ITbService#changestate) to acknowledge pending transitions). + +Syntax: *manager*.**ReportStatus** *CurrentState* [, *Win32ExitCode* [, *WaitHint* ] ] + +*CurrentState* +: *required* A [**ServiceStatusConstants**](Enumerations/ServiceStatusConstants) value — typically [**vbServiceStatusRunning**](Enumerations/ServiceStatusConstants#vbServiceStatusRunning), [**vbServiceStatusStopPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStopPending), or [**vbServiceStatusStopped**](Enumerations/ServiceStatusConstants#vbServiceStatusStopped). + +*Win32ExitCode* +: *optional* A **Long** exit code. Default **0** (`NO_ERROR`). When reporting [**vbServiceStatusStopped**](Enumerations/ServiceStatusConstants#vbServiceStatusStopped) after an error, pass either a Win32 error code or, for service-specific codes, the magic value `ERROR_SERVICE_SPECIFIC_ERROR` (1066) along with placing the real code in the service-specific field — but the package's API exposes only the *Win32ExitCode* parameter directly. Most services pass **0** for a clean stop and a small custom code for an error stop. + +*WaitHint* +: *optional* A **Long** giving the SCM an upper-bound milliseconds estimate of how long the current pending transition will take. Default **0**. Only meaningful for pending states ([**vbServiceStatusStartPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStartPending), [**vbServiceStatusStopPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStopPending), [**vbServiceStatusPausePending**](Enumerations/ServiceStatusConstants#vbServiceStatusPausePending), [**vbServiceStatusContinuePending**](Enumerations/ServiceStatusConstants#vbServiceStatusContinuePending)) — the SCM uses it together with the auto-incremented `dwCheckPoint` field to detect a stuck service. + +**ReportStatus** fills the `dwControlsAccepted` field of `SERVICE_STATUS` automatically — *Stop* is always accepted except during [**vbServiceStatusStartPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStartPending), and *Pause* / *Continue* are accepted when [**SupportsPausing**](#supportspausing) is **True**. The `dwCheckPoint` field auto-increments while the service is in a pending state and resets to **0** on [**vbServiceStatusRunning**](Enumerations/ServiceStatusConstants#vbServiceStatusRunning) / [**vbServiceStatusStopped**](Enumerations/ServiceStatusConstants#vbServiceStatusStopped). + +The package's dispatcher trampoline reports [**vbServiceStatusStartPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStartPending) for you immediately before calling [**EntryPoint**](ITbService#entrypoint); the user's [**EntryPoint**](ITbService#entrypoint) is responsible for the subsequent [**vbServiceStatusRunning**](Enumerations/ServiceStatusConstants#vbServiceStatusRunning) and [**vbServiceStatusStopped**](Enumerations/ServiceStatusConstants#vbServiceStatusStopped) transitions. + +### ResyncStatus +{: .no_toc } + +Re-applies the cached `SERVICE_STATUS` to the SCM via `SetServiceStatus`. Called automatically by [**ReportStatus**](#reportstatus) and by the [**SupportsPausing**](#supportspausing) setter; consumer code rarely needs to call this directly. + +Syntax: *manager*.**ResyncStatus** + +Raises run-time error 5 *"Can't update the service state until the service has started"* if called before the service has acquired its SCM status handle (i.e. before the dispatcher trampoline has called `RegisterServiceCtrlHandlerExW`). Use [**ReportStatus**](#reportstatus) from inside [**EntryPoint**](ITbService#entrypoint) instead of **ResyncStatus** directly. + +### Uninstall +{: .no_toc } + +Removes this service from the SCM database. + +Syntax: *manager*.**Uninstall** + +Opens the SCM, opens the service with `SERVICE_DELETE`, calls `DeleteService`. The actual deletion is queued by the SCM and completes once every open handle to the service is closed — `services.msc` may show the service as *"Marked for deletion"* until the host process exits. + +> [!IMPORTANT] +> [**Uninstall**](#uninstall) requires administrator rights. Raises run-time error 5 with a descriptive message if the SCM cannot be opened, the service is not installed, or `DeleteService` fails. + +## See Also + +- [WinServicesLib package](.) -- overview, lifecycle, two-thread split +- [Services class](Services) -- the predeclared coordinator that **ConfigureNew** comes from +- [ITbService interface](ITbService) -- what **InstanceCreator** must produce +- [ServiceCreator(Of T)](ServiceCreator) -- the generic factory typically passed to **InstanceCreator** +- [ServiceStatusConstants enum](Enumerations/ServiceStatusConstants) -- the values **ReportStatus** accepts diff --git a/docs/Reference/WinServicesLib/ServiceState.md b/docs/Reference/WinServicesLib/ServiceState.md new file mode 100644 index 0000000..edefa99 --- /dev/null +++ b/docs/Reference/WinServicesLib/ServiceState.md @@ -0,0 +1,131 @@ +--- +title: ServiceState +parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/ServiceState +has_toc: false +--- + +# ServiceState class +{: .no_toc } + +A read-only snapshot of an installed service's current state as reported by the SCM. Returned by [**Services.QueryStateOfService**](Services#querystateofservice); not directly user-instantiable. + +```tb +Dim state As ServiceState +Set state = Services.QueryStateOfService("MyService") + +Debug.Print state.CurrentStateText, "PID " & state.ProcessId +``` + +The snapshot is taken **once at construction time** and never refreshed. To monitor a service over time, call [**Services.QueryStateOfService**](Services#querystateofservice) again at each sampling interval — typically from a low-frequency Timer. + +The constructor opens the SCM with `SC_MANAGER_CONNECT`, opens the service with `SERVICE_QUERY_STATUS`, calls `QueryServiceStatusEx(SC_STATUS_PROCESS_INFO, ...)`, and copies the result into a private buffer. The three failure modes — SCM open failed, service not installed, status query failed — all raise run-time error 5 with a descriptive message. Wrap the call in `On Error Resume Next` if your UI needs to distinguish "service exists and is running" from "service is not installed": + +```tb +Private Function GetStateText(ByVal serviceName As String) As String + On Error Resume Next + Dim state As ServiceState + Set state = Services.QueryStateOfService(serviceName) + If Err.Number = 0 Then + GetStateText = state.CurrentStateText + Else + GetStateText = "not installed" + End If +End Function +``` + +* TOC +{:toc} + +## Properties + +### CheckPoint +{: .no_toc } + +The SCM-reported `dwCheckPoint` value. **Long**. + +Services in a *Pending* state ([**StartPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStartPending), [**StopPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStopPending), [**PausePending**](Enumerations/ServiceStatusConstants#vbServiceStatusPausePending), [**ContinuePending**](Enumerations/ServiceStatusConstants#vbServiceStatusContinuePending)) report a periodically-incrementing **CheckPoint** so the SCM can tell a slow-but-progressing transition from a hung service. The package's [**ServiceManager.ReportStatus**](ServiceManager#reportstatus) auto-increments the field while the service is in a pending state and resets it to **0** on [**Running**](Enumerations/ServiceStatusConstants#vbServiceStatusRunning) or [**Stopped**](Enumerations/ServiceStatusConstants#vbServiceStatusStopped). + +### ControlsAccepted +{: .no_toc } + +A bitmask of `SERVICE_ACCEPT_*` flags indicating which control codes the service has told the SCM it accepts. **Long**. + +> [!NOTE] +> Although the underlying SCM field is a flag bitmask, the property is typed plain **Long** in this release rather than as a typed enum. The bit values follow the Win32 documented constants — `SERVICE_ACCEPT_STOP` (1), `SERVICE_ACCEPT_PAUSE_CONTINUE` (2), `SERVICE_ACCEPT_SHUTDOWN` (4), `SERVICE_ACCEPT_PARAMCHANGE` (8), `SERVICE_ACCEPT_NETBINDCHANGE` (16), `SERVICE_ACCEPT_HARDWAREPROFILECHANGE` (32), `SERVICE_ACCEPT_POWEREVENT` (64), `SERVICE_ACCEPT_SESSIONCHANGE` (128), `SERVICE_ACCEPT_PRESHUTDOWN` (256), and so on. + +### CurrentState +{: .no_toc } + +The SCM-reported `dwCurrentState` value. **Long**. + +> [!NOTE] +> The property is typed plain **Long** in this release rather than as [**ServiceStatusConstants**](Enumerations/ServiceStatusConstants). The numeric values *do* match the enum (e.g. `4` is [**vbServiceStatusRunning**](Enumerations/ServiceStatusConstants#vbServiceStatusRunning)), so a cast such as `CType(state.CurrentState, ServiceStatusConstants)` recovers typed access if needed. For display purposes [**CurrentStateText**](#currentstatetext) is usually more convenient. + +### CurrentStateText +{: .no_toc } + +A human-readable rendering of [**CurrentState**](#currentstate). **String**. + +The mapping: + +| State value | Text | +|-------------|------| +| [**vbServiceStatusContinuePending**](Enumerations/ServiceStatusConstants#vbServiceStatusContinuePending) | `CONTINUING` | +| [**vbServiceStatusPausePending**](Enumerations/ServiceStatusConstants#vbServiceStatusPausePending) | `PAUSING` | +| [**vbServiceStatusPaused**](Enumerations/ServiceStatusConstants#vbServiceStatusPaused) | `PAUSED` | +| [**vbServiceStatusRunning**](Enumerations/ServiceStatusConstants#vbServiceStatusRunning) | `RUNNING` | +| [**vbServiceStatusStartPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStartPending) | `STARTING` | +| [**vbServiceStatusStopPending**](Enumerations/ServiceStatusConstants#vbServiceStatusStopPending) | `STOPPING` | +| [**vbServiceStatusStopped**](Enumerations/ServiceStatusConstants#vbServiceStatusStopped) | `STOPPED` | + +Any unrecognised state value is rendered as `UNKNOWN STATE ()`. + +### ExitCode +{: .no_toc } + +The SCM-reported `dwWin32ExitCode` value. **Long**. + +For a normally-stopped service this is **0** (`NO_ERROR`); for a service that stopped due to an error, this is either a Win32 error code or the sentinel `ERROR_SERVICE_SPECIFIC_ERROR` (1066) — in which case the real code is in [**ServiceSpecificExitCode**](#servicespecificexitcode). + +### Flags +{: .no_toc } + +The SCM-reported `dwServiceFlags` value. **Long**. + +Only one bit is currently documented — `SERVICE_RUNS_IN_SYSTEM_PROCESS` (1), set when the service is hosted inside the system process (`services.exe`). + +### ProcessId +{: .no_toc } + +The OS process ID hosting the service, or **0** if the service is not running. **Long**. + +Useful for cross-referencing against Task Manager or `tasklist /svc` output, and as a quick "is the service alive?" check that avoids string-comparing [**CurrentStateText**](#currentstatetext). + +### ServiceSpecificExitCode +{: .no_toc } + +The SCM-reported `dwServiceSpecificExitCode` value. **Long**. + +Meaningful only when [**ExitCode**](#exitcode) equals `ERROR_SERVICE_SPECIFIC_ERROR` (1066); otherwise the field is **0** and should be ignored. Services that report custom error codes through [**ServiceManager.ReportStatus**](ServiceManager#reportstatus) populate this field through the package's machinery. + +### Type +{: .no_toc } + +The SCM-reported service type. [**ServiceTypeConstants**](Enumerations/ServiceTypeConstants). + +The value the SCM has on file for the service, typically [**tbServiceTypeOwnProcess**](Enumerations/ServiceTypeConstants#tbServiceTypeOwnProcess) or [**tbServiceTypeShareProcess**](Enumerations/ServiceTypeConstants#tbServiceTypeShareProcess) for twinBASIC services. + +### WaitHint +{: .no_toc } + +The SCM-reported `dwWaitHint` value in milliseconds. **Long**. + +Only meaningful while the service is in a *Pending* state — it is the upper-bound estimate the service has told the SCM the pending transition will take. The SCM uses [**CheckPoint**](#checkpoint) and **WaitHint** together to decide whether a pending service is making progress. + +## See Also + +- [WinServicesLib package](.) -- overview, lifecycle +- [Services.QueryStateOfService method](Services#querystateofservice) -- the only way to obtain a **ServiceState** instance +- [ServiceStatusConstants enum](Enumerations/ServiceStatusConstants) -- the values **CurrentState** can take +- [ServiceTypeConstants enum](Enumerations/ServiceTypeConstants) -- the values **Type** can take diff --git a/docs/Reference/WinServicesLib/Services.md b/docs/Reference/WinServicesLib/Services.md new file mode 100644 index 0000000..4df9feb --- /dev/null +++ b/docs/Reference/WinServicesLib/Services.md @@ -0,0 +1,209 @@ +--- +title: Services +parent: WinServicesLib Package +permalink: /tB/Packages/WinServicesLib/Services +has_toc: false +--- + +# Services class +{: .no_toc } + +The predeclared singleton coordinator for the **WinServicesLib** package. Every interaction with the package starts with **Services**: the class is tagged `[PredeclaredId]`, so a project-wide instance named `Services` exists at start-up and the consumer calls `Services.X` directly without `New`. The instance also doubles as an enumerable collection of the [**ServiceManager**](ServiceManager) instances that have been configured (`For Each manager In Services`). + +```tb +' Configure two services at start-up: +With Services.ConfigureNew + .Name = "MyServiceA" + .InstanceCreator = New ServiceCreator(Of MyServiceA) +End With +With Services.ConfigureNew + .Name = "MyServiceB" + .InstanceCreator = New ServiceCreator(Of MyServiceB) +End With + +' Run the dispatcher if launched as a service host: +If InStr(Command, "-startService") > 0 Then Services.RunServiceDispatcher +``` + +See the package [overview](.) for the broader lifecycle, the [**two-thread split**](.#two-thread-split), and the elevation rules around installation. + +* TOC +{:toc} + +## Methods + +### ConfigureNew +{: .no_toc } + +Allocates a fresh [**ServiceManager**](ServiceManager), adds it to the internal collection, and returns it for the caller to populate. Typically used during `Sub Main` to declare every service the EXE knows how to host. + +Syntax: **Services.ConfigureNew** **As** [**ServiceManager**](ServiceManager) + +```tb +With Services.ConfigureNew + .Name = "MyService" + .Description = "An example twinBASIC service" + .Type = tbServiceTypeOwnProcess + .InstallStartMode = tbServiceStartOnDemand + .InstallCmdLine = """" & App.ModulePath & """ -startService" + .InstanceCreator = New ServiceCreator(Of MyService) +End With +``` + +Configuration is purely in-memory; **ConfigureNew** does not touch the SCM. The configured services are available via the [**For Each**](#_newenum) enumerator, [**GetConfiguredService**](#getconfiguredservice), and the bulk [**Install**](#installall) / [**Uninstall**](#uninstallall) / [**RunServiceDispatcher**](#runservicedispatcher) helpers. + +### ControlService +{: .no_toc } + +Sends an SCM control code to a running service. + +Syntax: **Services.ControlService** *ServiceName*, *ControlCode* + +*ServiceName* +: *required* A **String** naming an installed service. + +*ControlCode* +: *required* A [**ServiceControlCodeConstants**](Enumerations/ServiceControlCodeConstants) value — typically [**vbServiceControlStop**](Enumerations/ServiceControlCodeConstants#vbServiceControlStop), [**vbServiceControlPause**](Enumerations/ServiceControlCodeConstants#vbServiceControlPause), [**vbServiceControlContinue**](Enumerations/ServiceControlCodeConstants#vbServiceControlContinue), or [**vbServiceControlInterrogate**](Enumerations/ServiceControlCodeConstants#vbServiceControlInterrogate). User-defined codes in the range 128–255 are also accepted. + +The method opens the SCM, requests the minimum required permission for the chosen control code (`SERVICE_STOP`, `SERVICE_PAUSE_CONTINUE`, `SERVICE_INTERROGATE`, or `SERVICE_USER_DEFINED_CONTROL`), opens the service, calls `ControlServiceExW`, and closes the handles. For [**vbServiceControlStop**](Enumerations/ServiceControlCodeConstants#vbServiceControlStop) the reason code is filled with `SERVICE_STOP_REASON_FLAG_PLANNED | SERVICE_STOP_REASON_MAJOR_NONE | SERVICE_STOP_REASON_MINOR_NONE` ("planned stop, no specific reason"). Customising the reason code is not currently exposed. + +Raises run-time error 5 with a descriptive message if the SCM cannot be opened (typically a permissions issue), the service is not installed, or `ControlServiceExW` fails. + +### GetConfiguredService +{: .no_toc } + +Looks up a previously-configured [**ServiceManager**](ServiceManager) by its [**Name**](ServiceManager#name). + +Syntax: **Services.GetConfiguredService**( *Name* ) **As** [**ServiceManager**](ServiceManager) + +*Name* +: *required* A **String** matching the [**Name**](ServiceManager#name) of one of the [**ServiceManager**](ServiceManager) instances created with [**ConfigureNew**](#configurenew). + +Raises run-time error 5 *"service not found"* if no configured service carries that name. Typical use is in the interactive / install branch of `Sub Main`, where a UI button needs to act on a single configured service: + +```tb +Private Sub btnInstallA_Click() + If App.IsInIDE() Then Err.Raise 5, , "Run the compiled EXE as administrator." + Services.GetConfiguredService("MyServiceA").Install +End Sub +``` + +Despite the `Property Get` syntax, the lookup is parameterised by name — it reads as a property in source code, but behaves like a function. + +### InstallAll +{: .no_toc } + +Iterates every [**ServiceManager**](ServiceManager) created through [**ConfigureNew**](#configurenew) and calls its [**Install**](ServiceManager#install) method. Convenience for the typical case where the EXE registers every service it hosts in one shot. + +Syntax: **Services.InstallAll** + +> [!IMPORTANT] +> **InstallAll** writes registry entries under `HKEY_LOCAL_MACHINE` and requires administrator rights. The usual pattern is to call it once from an elevated installer. + +Per-service errors raised inside [**ServiceManager.Install**](ServiceManager#install) propagate out of **InstallAll** and abort the bulk operation — there is no per-service `On Error Resume Next` wrapping. Services already installed before the failure remain installed. + +### LaunchService +{: .no_toc } + +Starts an installed service by name and optionally passes launch arguments through to its [**ServiceManager.LaunchArgs**](ServiceManager#launchargs) field. + +Syntax: **Services.LaunchService** *ServiceName* [, *LaunchArgs* ... ] + +*ServiceName* +: *required* A **String** naming an installed service. + +*LaunchArgs* +: *optional* A `ParamArray` of values forwarded to the service through `StartServiceW`. Each value is converted to a **String**; the service-side [**ITbService.EntryPoint**](ITbService#entrypoint) reads them through [**ServiceManager.LaunchArgs**](ServiceManager#launchargs). + +The method opens the SCM with `SC_MANAGER_CONNECT`, opens the service with `SERVICE_START`, and calls `StartServiceW`. Raises run-time error 5 if the SCM cannot be opened, the service is not installed, the caller lacks the *Start* permission, or `StartServiceW` fails (typically because the service is already running). + +The launch-args mechanism is commonly used to gate startup on a shared secret: + +```tb +' UI side — starting the service with a password argument: +Services.LaunchService "MyService", "MySecretPassword" + +' Service side — checking the argument inside EntryPoint: +Sub EntryPoint(ByVal ServiceManager As ServiceManager) _ + Implements ITbService.EntryPoint + If Join(ServiceManager.LaunchArgs) <> "MySecretPassword" Then + ServiceManager.ReportStatus vbServiceStatusStopped, &H12345678 + Exit Sub + End If + ' ...steady-state work +End Sub +``` + +This prevents accidental starts from the Services control-panel applet (which calls `StartServiceW` with no extra arguments). + +### QueryStateOfService +{: .no_toc } + +Returns a fresh [**ServiceState**](ServiceState) snapshot of an installed service. + +Syntax: **Services.QueryStateOfService**( *ServiceName* ) **As** [**ServiceState**](ServiceState) + +*ServiceName* +: *required* A **String** naming an installed service. + +Raises run-time error 5 if the service is not installed or the SCM cannot be opened. The returned [**ServiceState**](ServiceState) is a single-shot snapshot; to monitor a service's state over time, call **QueryStateOfService** again at each sampling interval. + +```tb +Private Sub timerRefresh_Timer() + On Error Resume Next + Dim state As ServiceState + Set state = Services.QueryStateOfService("MyService") + If Err.Number = 0 Then + lblStatus.Caption = state.CurrentStateText _ + & " (PID " & state.ProcessId & ")" + Else + lblStatus.Caption = "not installed" + End If +End Sub +``` + +### RunServiceDispatcher +{: .no_toc } + +Hands control of the main thread over to the SCM and runs the service dispatcher loop. **Blocks** until the SCM signals shutdown. + +Syntax: **Services.RunServiceDispatcher** + +Internally builds a `SERVICE_TABLE_ENTRYW` array from every configured [**ServiceManager**](ServiceManager) and calls `StartServiceCtrlDispatcherW`. The SCM spawns a fresh thread for each service the user (or the *Start* configuration) wants to start, and invokes the package's dispatcher trampoline on that thread; the trampoline reports `StartPending`, optionally initialises COM in STA mode (controlled by [**ServiceManager.AutoInitializeCOM**](ServiceManager#autoinitializecom)), then calls the user's [**ITbService.EntryPoint**](ITbService#entrypoint). + +Raises run-time error 5 *"Unable to start the service dispatcher"* if `StartServiceCtrlDispatcherW` returns zero. The usual cause is that the EXE was launched normally rather than by the SCM — the dispatcher only works when the process is a service host. The conventional `If InStr(Command, "-startService") > 0 Then` gate in `Sub Main` avoids this error. + +### UninstallAll +{: .no_toc } + +Iterates every [**ServiceManager**](ServiceManager) created through [**ConfigureNew**](#configurenew) and calls its [**Uninstall**](ServiceManager#uninstall) method. + +Syntax: **Services.UninstallAll** + +> [!IMPORTANT] +> **UninstallAll** writes registry entries under `HKEY_LOCAL_MACHINE` and requires administrator rights. Per-service errors abort the bulk operation; services already uninstalled before the failure remain uninstalled. + +## Enumerator + +### _NewEnum +{: .no_toc } + +Provides `For Each` support across every [**ServiceManager**](ServiceManager) the project has configured. + +Syntax: **For Each** *manager* **In Services** + +```tb +Dim manager As ServiceManager +For Each manager In Services + Debug.Print manager.Name, manager.Description +Next +``` + +The enumeration order is insertion order — services appear in the order they were created with [**ConfigureNew**](#configurenew). + +## See Also + +- [WinServicesLib package](.) -- overview, lifecycle, two-thread split, integration with WinEventLogLib / WinNamedPipesLib +- [ServiceManager class](ServiceManager) -- the per-service configuration object **ConfigureNew** returns +- [ServiceState class](ServiceState) -- the state snapshot **QueryStateOfService** returns +- [ServiceControlCodeConstants enum](Enumerations/ServiceControlCodeConstants) -- the codes accepted by **ControlService** diff --git a/docs/Reference/WinServicesLib/index.md b/docs/Reference/WinServicesLib/index.md new file mode 100644 index 0000000..67d5874 --- /dev/null +++ b/docs/Reference/WinServicesLib/index.md @@ -0,0 +1,133 @@ +--- +title: WinServicesLib Package +parent: Packages +grand_parent: Reference Section +nav_order: 10 +permalink: /tB/Packages/WinServicesLib/ +has_toc: false +--- + +# WinServicesLib Package +{: .no_toc } + +The **WinServicesLib** built-in package wraps the Windows Service Control Manager so a twinBASIC EXE can run as one or more Windows services. The same EXE typically does double duty as the install / control-panel tool when launched normally and as the service host when launched by the SCM; both modes coexist in a single `Sub Main`. The package handles the SCM handshake, the service-thread dispatch, the control-code routing, and the install / uninstall registry plumbing. + +The package is a built-in package shipped with twinBASIC. Add it through Project → References (**Ctrl-T**) → Available Packages. + +* TOC +{:toc} + +## What a Windows service is + +A *Windows service* is a long-running background process supervised by the **Service Control Manager (SCM)**. Services can start before any user logs in, run under dedicated accounts (`LocalSystem`, `LocalService`, `NetworkService`, or any explicit user), and respond to lifecycle commands — *Start*, *Stop*, *Pause*, *Continue* — issued from the Services control-panel applet (`services.msc`), the `sc.exe` command-line tool, or programmatic equivalents. + +A service-hosting EXE communicates with the SCM through a small set of Win32 entry points: `StartServiceCtrlDispatcherW` to hand the process over to the SCM, `RegisterServiceCtrlHandlerExW` to hook a control-code callback, `SetServiceStatus` to report state transitions, and `CreateServiceW` / `DeleteService` to register / unregister the service in the system database. **WinServicesLib** wraps all of these — the consumer writes one class per service, declares it through the package's coordinator, and the package handles every Win32 detail. + +## Lifecycle + +A service-hosting EXE goes through four phases: + +1. **Configure** — at startup, declare every service the EXE knows how to host by calling [**Services.ConfigureNew**](Services#configurenew) and filling the returned [**ServiceManager**](ServiceManager). Configuration is purely in-memory and does not touch the SCM; it builds the map the dispatcher will use *if* the EXE is launched as a service host. +2. **Install** (one-time, elevated) — register the configured services in the system database via [**ServiceManager.Install**](ServiceManager#install) or [**Services.InstallAll**](Services#installall). This writes registry entries under `HKLM\SYSTEM\CurrentControlSet\Services\` pointing at the EXE and requires administrator rights. Usually run from an installer. +3. **Run as a service** (when the SCM launches the EXE) — the EXE's `Sub Main` detects it was launched as a service host (typically by inspecting `Command` for a known argument like `"-startService"`) and calls [**Services.RunServiceDispatcher**](Services#runservicedispatcher). This blocks the main thread inside `StartServiceCtrlDispatcherW` until the SCM signals shutdown. The SCM spawns a separate service thread per service and calls into the package's dispatcher trampoline; the trampoline reports `StartPending`, then invokes the user's [**ITbService.EntryPoint**](ITbService#entrypoint) on the service thread. +4. **Run normally** (when a user launches the EXE) — the EXE's `Sub Main` does *not* see the service-host argument and proceeds to whatever UI / CLI logic the EXE provides for installation, status display, or interactive testing. The same configured [**ServiceManager**](ServiceManager) instances are still reachable through [**Services.GetConfiguredService**](Services#getconfiguredservice) and the [**For Each**](Services#_newenum) enumerator, which is what enables a single-EXE install-and-host design. + +The canonical `Sub Main` skeleton: + +```tb +Module Startup + Public Sub Main() + With Services.ConfigureNew + .Name = "MyService" + .Description = "An example twinBASIC service" + .Type = tbServiceTypeOwnProcess + .InstallStartMode = tbServiceStartOnDemand + .InstallCmdLine = """" & App.ModulePath & """ -startService" + .InstanceCreator = New ServiceCreator(Of MyService) + End With + + If InStr(Command, "-startService") > 0 Then + Services.RunServiceDispatcher ' blocks until the SCM signals shutdown + Else + MainForm.Show ' control-panel / install UI + End If + End Sub +End Module +``` + +The `-startService` discriminator is the conventional way for the EXE to know which mode it is in. The `InstallCmdLine` field embeds this argument so the SCM passes it back when launching the service; the user-launched path sees no such argument and falls through to the UI branch. + +## The two-thread split +{: #two-thread-split } + +When the SCM launches the EXE as a service host, twinBASIC's runtime ends up driving **two threads** for each service: + +- The **service thread** — the SCM-spawned thread that runs the user's [**ITbService.EntryPoint**](ITbService#entrypoint). This is where the service does its actual work. The thread is created by `StartServiceCtrlDispatcherW`'s machinery; it is *not* the main thread of the EXE. +- The **dispatcher thread** — the EXE's main thread, which is what the SCM invokes when it has a control code to deliver (*Stop*, *Pause*, *Continue*, …). The package routes the control through `RegisterServiceCtrlHandlerExW` to a trampoline that calls the user's [**ITbService.ChangeState**](ITbService#changestate). + +The two methods therefore run *concurrently*: while [**EntryPoint**](ITbService#entrypoint) is doing the service's work on the service thread, [**ChangeState**](ITbService#changestate) is sitting idle on the dispatcher thread, and the SCM wakes it on demand to deliver a control code. The two methods must coordinate through shared `Public` flags on the service class — `IsStopping`, `IsPaused`, and friends — because the package cannot tell the service thread to stop except through the user's own code path. + +```tb +Class MyService + Implements ITbService + + Public IsStopping As Boolean + + Sub EntryPoint(ByVal ServiceManager As ServiceManager) _ + Implements ITbService.EntryPoint + ServiceManager.ReportStatus vbServiceStatusRunning + Do Until IsStopping + ' …do work, then yield with WaitForSingleObject / Sleep / etc. + Loop + ServiceManager.ReportStatus vbServiceStatusStopped + End Sub + + Sub ChangeState(ByVal ServiceManager As ServiceManager, _ + ByVal dwControl As ServiceControlCodeConstants, _ + ByVal dwEventType As Long, _ + ByVal lpEventData As LongPtr) _ + Implements ITbService.ChangeState + Select Case dwControl + Case vbServiceControlStop, vbServiceControlShutdown + ServiceManager.ReportStatus vbServiceStatusStopPending + IsStopping = True ' wakes EntryPoint's loop on the other thread + End Select + End Sub + + Sub StartupFailed(ByVal ServiceManager As ServiceManager) _ + Implements ITbService.StartupFailed + ' …optional failure-reporting hook + End Sub +End Class +``` + +The shared-flag pattern is the documented coordination mechanism — there is no built-in cancellation primitive. For services that host an inherently message-loop-driven object (a [**NamedPipeServer**](../WinNamedPipesLib/NamedPipeServer), a window-message handler, …) the loop-driven object's own *Stop*-signal method is usually called from [**ChangeState**](ITbService#changestate); see [the WinNamedPipesLib service-host idiom](../WinNamedPipesLib/#service-host-idiom) for a worked example. + +## Integration with the sister "winlibs" packages + +`WinServicesLib` is most often used together with [**WinEventLogLib**](../WinEventLogLib/) and [**WinNamedPipesLib**](../WinNamedPipesLib/) — Windows services typically need a place to write diagnostic events (the Windows Event Log) and a way to communicate with non-service processes (named pipes). The three packages compose cleanly: + +- **Logging** — every service class can mix in the [**EventLog**](../WinEventLogLib/EventLog) surface through the [composition-delegation idiom](../WinEventLogLib/#composition-delegation-idiom) (`Implements EventLog(Of EVENTS, CATEGORIES) Via EventLog = New EventLog(...)`), so [**LogSuccess**](../WinEventLogLib/EventLog#logsuccess) / [**LogFailure**](../WinEventLogLib/EventLog#logfailure) read as plain method calls inside `EntryPoint` and `ChangeState`. The events fire under the service account (typically `LocalSystem`), which the Event Viewer renders against the message-table resource the EXE carries. +- **IPC** — a [**NamedPipeServer**](../WinNamedPipesLib/NamedPipeServer) hosted inside the service uses [**ManualMessageLoopEnter**](../WinNamedPipesLib/NamedPipeServer#manualmessageloopenter) as the [**EntryPoint**](ITbService#entrypoint)'s blocking primitive, and [**ManualMessageLoopLeave**](../WinNamedPipesLib/NamedPipeServer#manualmessageloopleave) from [**ChangeState**](ITbService#changestate) becomes the *Stop*-signal mechanism. See [Hosting inside a Windows service](../WinNamedPipesLib/#service-host-idiom) for the complete pattern, including pause / continue and the dispatcher-thread / service-thread interaction. + +## Installation and elevation + +[**Install**](ServiceManager#install) and [**Uninstall**](ServiceManager#uninstall) (and their bulk-helpers [**Services.InstallAll**](Services#installall) / [**Services.UninstallAll**](Services#uninstallall)) call `CreateServiceW` / `DeleteService`, which require an SCM handle opened with `SC_MANAGER_CREATE_SERVICE`. Both succeed only when the calling process runs with **administrator rights**. The typical project structure: + +- A standalone installer EXE (or installer mode inside the same EXE, gated by a `-install` command-line argument) runs elevated and calls [**Install**](ServiceManager#install) / [**Uninstall**](ServiceManager#uninstall) plus a one-time call to [**EventLog.Register**](../WinEventLogLib/EventLog#register). +- The service-host EXE itself does not need elevation at run-time (the SCM launches it under whatever account the service is configured for). +- The control-panel / interactive UI does not need elevation either — it can use [**Services.LaunchService**](Services#launchservice) and [**Services.ControlService**](Services#controlservice) freely, as long as the user has the standard *Start* / *Stop* permissions on the relevant service (the default ACL grants this to **LocalSystem**, **Administrators**, and the running interactive user for *interactive* services). + +Calling [**Install**](ServiceManager#install) while running inside the twinBASIC IDE will fail with an SCM-access error — the IDE is rarely elevated. Either run the compiled EXE as administrator, or wrap the call in an `If App.IsInIDE() Then Err.Raise 5, , "Run the compiled EXE as administrator."` guard. + +## Classes and interface + +- [Services](Services) -- the predeclared singleton coordinator: [**ConfigureNew**](Services#configurenew), [**RunServiceDispatcher**](Services#runservicedispatcher), the bulk install / uninstall helpers, plus the runtime control methods ([**LaunchService**](Services#launchservice), [**ControlService**](Services#controlservice), [**QueryStateOfService**](Services#querystateofservice)) +- [ServiceManager](ServiceManager) -- one per configured service; carries the fields the SCM cares about (name, description, type, start-mode, command-line, dependencies, ...) plus the [**ReportStatus**](ServiceManager#reportstatus) call the service uses to inform the SCM of state transitions +- [ServiceCreator](ServiceCreator) -- the generic [**ServiceCreator**](ServiceCreator)`(Of T)` factory the dispatcher uses to instantiate each service class on demand; *T* must implement [**ITbService**](ITbService) +- [ServiceState](ServiceState) -- a read-only state snapshot returned by [**Services.QueryStateOfService**](Services#querystateofservice), giving the SCM-reported state and process ID of an installed service +- [ITbService](ITbService) -- the interface every service class implements: [**EntryPoint**](ITbService#entrypoint), [**StartupFailed**](ITbService#startupfailed), [**ChangeState**](ITbService#changestate) + +## Enumerations + +- [Enumerations](Enumerations/) -- four user-facing enumerations: [**ServiceTypeConstants**](Enumerations/ServiceTypeConstants), [**ServiceStartConstants**](Enumerations/ServiceStartConstants), [**ServiceControlCodeConstants**](Enumerations/ServiceControlCodeConstants), [**ServiceStatusConstants**](Enumerations/ServiceStatusConstants)