Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 372 additions & 5 deletions WIP.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/Reference/Packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
6 changes: 4 additions & 2 deletions docs/Reference/WinEventLogLib/EventLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -112,7 +114,7 @@ Creates `HKLM\SYSTEM\CurrentControlSet\Services\EventLog\<LogPath>` (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 (\<LogName\>)"*. Typical causes are insufficient privileges and a *LogPath* that points at a non-existent parent log.

Expand Down
79 changes: 78 additions & 1 deletion docs/Reference/WinEventLogLib/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Class> Via <field> = <expression>` 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 [**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.

## 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

Expand Down
5 changes: 3 additions & 2 deletions docs/Reference/WinNamedPipesLib/NamedPipeClientConnection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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**.
Expand All @@ -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
3 changes: 3 additions & 0 deletions docs/Reference/WinNamedPipesLib/NamedPipeClientManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand All @@ -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
10 changes: 8 additions & 2 deletions docs/Reference/WinNamedPipesLib/NamedPipeServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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**.
Expand All @@ -154,13 +154,17 @@ 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 }

Posts a `WM_USER_QUITTING` message to the hidden marshalling window, causing the [**ManualMessageLoopEnter**](#manualmessageloopenter) loop on the other thread to break out cleanly. Safe to call from any thread.

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 }

Expand Down Expand Up @@ -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
Loading
Loading