winlog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE.txt +28 -0
- data/README.md +243 -0
- data/ext/winlog/TraceLoggingDynamic.h +3482 -0
- data/ext/winlog/extconf.rb +30 -0
- data/ext/winlog/winlog.cpp +788 -0
- data/lib/winlog/version.rb +5 -0
- data/lib/winlog.rb +216 -0
- metadata +125 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* winlog — structured, registration-free ETW TraceLogging emit for Ruby.
|
|
3
|
+
*
|
|
4
|
+
* The suite's FIRST C++ extension, by necessity: a runtime-dynamic
|
|
5
|
+
* log(level, event, **fields) API cannot use TraceLoggingProvider.h (which
|
|
6
|
+
* needs compile-time-constant provider/event/field names). Microsoft's
|
|
7
|
+
* supported answer for runtime-dynamic events is TraceLoggingDynamic.h, which
|
|
8
|
+
* is C++-only (templates + std::vector). It is vendored verbatim into
|
|
9
|
+
* ext/winlog/TraceLoggingDynamic.h, MIT-licensed (c) Microsoft Corporation,
|
|
10
|
+
* from microsoft/tracelogging (etw/cpp/traceloggingdynamic). The header is a
|
|
11
|
+
* point-in-time copy (see plans/research/tracelogging.md §1/§12 for provenance:
|
|
12
|
+
* the same file built and ran in the research spike on this machine).
|
|
13
|
+
*
|
|
14
|
+
* ----------------------------------------------------------------------------
|
|
15
|
+
* C++ / rb_raise HYGIENE (the lithos discipline, adapted — see §3.6 of the
|
|
16
|
+
* spec). Invariants, in order of precedence:
|
|
17
|
+
* 1. No C++ exception ever crosses into Ruby frames. Every region that runs
|
|
18
|
+
* tld / std::vector code is wrapped try { ... } catch (...) and converted
|
|
19
|
+
* to rb_raise from a frame that holds no live C++ object.
|
|
20
|
+
* 2. No rb_raise longjmp ever crosses a frame holding a live C++ object. The
|
|
21
|
+
* tld::Event is heap-allocated (new), owned by the _write frame as a raw
|
|
22
|
+
* pointer, and delete'd on EVERY exit path before any raise. No
|
|
23
|
+
* stack-scoped C++ objects with destructors live in any frame a longjmp
|
|
24
|
+
* can cross.
|
|
25
|
+
* 3. All argument coercion / validation happens up front, before the
|
|
26
|
+
* tld::Event exists.
|
|
27
|
+
*
|
|
28
|
+
* GVL STRATEGY — every call HOLDS the GVL (documented deviation, spec §3.5).
|
|
29
|
+
* Nothing in winlog blocks on the kernel:
|
|
30
|
+
* | call | GVL | why |
|
|
31
|
+
* | EventRegister + EventSetInformation | hold | µs-scale; MUST serialize |
|
|
32
|
+
* | (via tld::Provider ctor) | | vs unregister; GVL=lock |
|
|
33
|
+
* | EventWriteTransfer (tld::Event::Write) | hold | non-blocking buffer copy |
|
|
34
|
+
* | EventUnregister (close / GC free) | hold | same as register |
|
|
35
|
+
* | EventActivityIdControl(CREATE_ID) | hold | trivial |
|
|
36
|
+
* Consequence: no ubf, no OVERLAPPED, no cancellation logic anywhere.
|
|
37
|
+
*
|
|
38
|
+
* INCLUDE-ORDER TRAP (mswin, research §2): <ruby.h> MUST precede <windows.h>.
|
|
39
|
+
* ruby/win32.h pulls <winsock2.h>; a bare <windows.h> first pulls <winsock.h>
|
|
40
|
+
* -> C2375 redefinition storm. Never name an identifier IN/OUT. mkmf already
|
|
41
|
+
* injects -D_WIN32_WINNT=_WIN32_WINNT_WIN8 (verified) — do not redefine it.
|
|
42
|
+
*
|
|
43
|
+
* E15 (DLL-unload crash hazard) / E16 (fork): not reachable on MRI. MRI never
|
|
44
|
+
* FreeLibrary's an extension .so before process exit, winlog has no DllMain,
|
|
45
|
+
* and every other path unregisters first (explicit close, GC free hook). fork
|
|
46
|
+
* does not exist on mswin. Nothing to implement (spec §5 E15/E16).
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
#include <ruby.h>
|
|
50
|
+
#include <ruby/encoding.h>
|
|
51
|
+
|
|
52
|
+
#define WIN32_LEAN_AND_MEAN
|
|
53
|
+
#include <windows.h>
|
|
54
|
+
#include <evntprov.h>
|
|
55
|
+
|
|
56
|
+
#include <vector>
|
|
57
|
+
#include "TraceLoggingDynamic.h"
|
|
58
|
+
|
|
59
|
+
#include <limits.h>
|
|
60
|
+
#include <string.h>
|
|
61
|
+
|
|
62
|
+
typedef tld::Event<std::vector<BYTE> > DynEvent;
|
|
63
|
+
|
|
64
|
+
/* ------------------------------------------------------------------ globals */
|
|
65
|
+
|
|
66
|
+
static VALUE mWinlog;
|
|
67
|
+
static VALUE cProvider;
|
|
68
|
+
static VALUE eError; /* Winlog::Error < StandardError */
|
|
69
|
+
static VALUE eClosed; /* Winlog::Closed < Winlog::Error */
|
|
70
|
+
|
|
71
|
+
/* Reserved keyword bits 48..63 (Microsoft-defined). Mirrors the Ruby constant
|
|
72
|
+
* Winlog::KEYWORD_RESERVED_MASK (spec §2.1 / E3). */
|
|
73
|
+
#define WINLOG_KEYWORD_RESERVED_MASK 0xFFFF000000000000ULL
|
|
74
|
+
|
|
75
|
+
/* ---------------------------------------------------------- native struct */
|
|
76
|
+
|
|
77
|
+
typedef struct {
|
|
78
|
+
tld::Provider *prov; /* heap; owns REGHANDLE + provider metadata blob */
|
|
79
|
+
int closed;
|
|
80
|
+
} provider_t;
|
|
81
|
+
|
|
82
|
+
static void
|
|
83
|
+
provider_free(void *p)
|
|
84
|
+
{
|
|
85
|
+
provider_t *pt = (provider_t *)p;
|
|
86
|
+
/* The tld::Provider destructor calls EventUnregister and HeapFree's the
|
|
87
|
+
* metadata blob. Finalizer is a safety net, never the API (spec §3.4):
|
|
88
|
+
* explicit idempotent close + Winlog.open's block form exist. */
|
|
89
|
+
if (pt->prov) {
|
|
90
|
+
delete pt->prov;
|
|
91
|
+
pt->prov = NULL;
|
|
92
|
+
}
|
|
93
|
+
xfree(pt);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static size_t
|
|
97
|
+
provider_memsize(const void *p)
|
|
98
|
+
{
|
|
99
|
+
const provider_t *pt = (const provider_t *)p;
|
|
100
|
+
/* Safe even after a failed registration: InitFail frees the real blob and
|
|
101
|
+
* points m_pbMetadata at the static 3-byte NullMetadata, so
|
|
102
|
+
* GetMetadataSize() returns 3 (not a dangling read). */
|
|
103
|
+
return sizeof(provider_t) +
|
|
104
|
+
(pt->prov ? (size_t)pt->prov->GetMetadataSize() : 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static const rb_data_type_t provider_type = {
|
|
108
|
+
"Winlog::Provider",
|
|
109
|
+
{ 0, provider_free, provider_memsize, }, /* no dmark: no VALUEs in struct */
|
|
110
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
static VALUE
|
|
114
|
+
provider_alloc(VALUE klass)
|
|
115
|
+
{
|
|
116
|
+
provider_t *pt;
|
|
117
|
+
VALUE obj = TypedData_Make_Struct(klass, provider_t, &provider_type, pt);
|
|
118
|
+
pt->prov = NULL; /* a GC'd never-initialized object is safe to free */
|
|
119
|
+
pt->closed = 0;
|
|
120
|
+
return obj;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Raw getter (close/closed?/inspect must work on closed objects). */
|
|
124
|
+
static provider_t *
|
|
125
|
+
provider_get(VALUE self)
|
|
126
|
+
{
|
|
127
|
+
provider_t *pt;
|
|
128
|
+
TypedData_Get_Struct(self, provider_t, &provider_type, pt);
|
|
129
|
+
return pt;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Live getter: raises Winlog::Closed on a closed or never-registered object
|
|
133
|
+
* (E19/E20). An .allocate'd instance has prov == NULL -> Closed, never a crash. */
|
|
134
|
+
static provider_t *
|
|
135
|
+
provider_live(VALUE self)
|
|
136
|
+
{
|
|
137
|
+
provider_t *pt = provider_get(self);
|
|
138
|
+
if (pt->prov == NULL || pt->closed)
|
|
139
|
+
rb_raise(eClosed, "winlog: provider is closed");
|
|
140
|
+
return pt;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ------------------------------------------------------------- GUID format */
|
|
144
|
+
|
|
145
|
+
/* GUID -> 36-char lowercase hyphenated UTF-8 String. */
|
|
146
|
+
static VALUE
|
|
147
|
+
guid_to_rb(GUID const &g)
|
|
148
|
+
{
|
|
149
|
+
char buf[40];
|
|
150
|
+
snprintf(buf, sizeof buf,
|
|
151
|
+
"%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
|
152
|
+
(unsigned long)g.Data1, g.Data2, g.Data3,
|
|
153
|
+
g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3],
|
|
154
|
+
g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7]);
|
|
155
|
+
return rb_utf8_str_new_cstr(buf);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Parse a strict 36-char lowercase-or-uppercase hyphenated GUID String into a
|
|
159
|
+
* GUID. Returns 1 on success, 0 on any deviation from the canonical form
|
|
160
|
+
* (E29: braced {...}, hyphenless, wrong length all rejected). Case-insensitive
|
|
161
|
+
* per the spec ("activity: 36-char hyphenated GUID String (case-insensitive)").
|
|
162
|
+
* Pure C parsing — no Ruby allocation, safe to call before any C++ object. */
|
|
163
|
+
static int
|
|
164
|
+
parse_guid(VALUE str, GUID *out)
|
|
165
|
+
{
|
|
166
|
+
const char *s;
|
|
167
|
+
long len;
|
|
168
|
+
int i;
|
|
169
|
+
unsigned int b[16]; /* the 16 data bytes, in display order */
|
|
170
|
+
static const int hyphen_at[4] = { 8, 13, 18, 23 };
|
|
171
|
+
int bi = 0;
|
|
172
|
+
|
|
173
|
+
Check_Type(str, T_STRING);
|
|
174
|
+
s = RSTRING_PTR(str);
|
|
175
|
+
len = RSTRING_LEN(str);
|
|
176
|
+
if (len != 36)
|
|
177
|
+
return 0;
|
|
178
|
+
|
|
179
|
+
for (i = 0; i < 36; i++) {
|
|
180
|
+
char c = s[i];
|
|
181
|
+
if (i == hyphen_at[0] || i == hyphen_at[1] ||
|
|
182
|
+
i == hyphen_at[2] || i == hyphen_at[3]) {
|
|
183
|
+
if (c != '-') return 0;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!((c >= '0' && c <= '9') ||
|
|
187
|
+
(c >= 'a' && c <= 'f') ||
|
|
188
|
+
(c >= 'A' && c <= 'F')))
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Re-scan, packing hex pairs into 16 bytes (display byte order). */
|
|
193
|
+
{
|
|
194
|
+
int pos = 0;
|
|
195
|
+
while (pos < 36 && bi < 16) {
|
|
196
|
+
if (s[pos] == '-') { pos++; continue; }
|
|
197
|
+
{
|
|
198
|
+
char pair[3];
|
|
199
|
+
pair[0] = s[pos];
|
|
200
|
+
pair[1] = s[pos + 1];
|
|
201
|
+
pair[2] = 0;
|
|
202
|
+
b[bi++] = (unsigned int)strtoul(pair, NULL, 16);
|
|
203
|
+
pos += 2;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (bi != 16)
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Display order is big-endian for Data1/2/3; assemble accordingly. */
|
|
211
|
+
out->Data1 = ((DWORD)b[0] << 24) | ((DWORD)b[1] << 16) |
|
|
212
|
+
((DWORD)b[2] << 8) | (DWORD)b[3];
|
|
213
|
+
out->Data2 = (WORD)(((unsigned)b[4] << 8) | (unsigned)b[5]);
|
|
214
|
+
out->Data3 = (WORD)(((unsigned)b[6] << 8) | (unsigned)b[7]);
|
|
215
|
+
for (i = 0; i < 8; i++)
|
|
216
|
+
out->Data4[i] = (BYTE)b[8 + i];
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* ------------------------------------------------- field name / event name */
|
|
221
|
+
|
|
222
|
+
/* Transcode a String to UTF-8 with String#encode semantics (the strict path):
|
|
223
|
+
* - already UTF-8: returned as-is (the caller checks coderange/NUL).
|
|
224
|
+
* - other encoding: rb_str_encode, which RAISES Ruby's own
|
|
225
|
+
* Encoding::UndefinedConversionError on a byte with no UTF-8 mapping (E22) —
|
|
226
|
+
* unlike rb_str_export_to_enc, which silently substitutes. This matches the
|
|
227
|
+
* spec's "bytes with no UTF-8 mapping raise Encoding::UndefinedConversionError".
|
|
228
|
+
* Raises from a clean frame (no C++ object live). */
|
|
229
|
+
static VALUE
|
|
230
|
+
transcode_utf8(VALUE str)
|
|
231
|
+
{
|
|
232
|
+
rb_encoding *u8 = rb_utf8_encoding();
|
|
233
|
+
if (rb_enc_get(str) == u8)
|
|
234
|
+
return str;
|
|
235
|
+
return rb_str_encode(str, rb_enc_from_encoding(u8), 0, Qnil);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Coerce a Symbol/String name to a UTF-8 String, validating: non-empty, no
|
|
239
|
+
* embedded NUL (ETW requirement, E6), no broken UTF-8 coderange (E22). Raises
|
|
240
|
+
* ArgumentError/TypeError from a clean frame (no C++ object live). +what+ is
|
|
241
|
+
* "event name" or a field name for the message. Returns the (possibly new)
|
|
242
|
+
* UTF-8 String VALUE; the caller must keep it referenced while using its ptr. */
|
|
243
|
+
static VALUE
|
|
244
|
+
coerce_name_utf8(VALUE v, const char *what)
|
|
245
|
+
{
|
|
246
|
+
VALUE str;
|
|
247
|
+
|
|
248
|
+
if (SYMBOL_P(v)) {
|
|
249
|
+
str = rb_sym2str(v); /* already UTF-8 */
|
|
250
|
+
} else if (RB_TYPE_P(v, T_STRING)) {
|
|
251
|
+
str = v;
|
|
252
|
+
} else {
|
|
253
|
+
rb_raise(rb_eTypeError, "winlog: %s must be a String or Symbol, got %" PRIsVALUE,
|
|
254
|
+
what, rb_obj_class(v));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
str = transcode_utf8(str);
|
|
258
|
+
|
|
259
|
+
if (RSTRING_LEN(str) == 0)
|
|
260
|
+
rb_raise(rb_eArgError, "winlog: %s must not be empty", what);
|
|
261
|
+
if (rb_enc_str_coderange(str) == ENC_CODERANGE_BROKEN)
|
|
262
|
+
rb_raise(rb_eArgError, "winlog: %s is not valid UTF-8", what);
|
|
263
|
+
if (memchr(RSTRING_PTR(str), '\0', RSTRING_LEN(str)) != NULL)
|
|
264
|
+
rb_raise(rb_eArgError, "winlog: %s must not contain a NUL byte", what);
|
|
265
|
+
|
|
266
|
+
return str;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* =================================================================
|
|
270
|
+
* Field building (the hot path). add_field_i runs under rb_hash_foreach,
|
|
271
|
+
* itself under rb_protect, with the tld::Event owned on the heap OUTSIDE this
|
|
272
|
+
* frame. add_field_i MAY rb_raise directly (TypeError/ArgumentError/RangeError
|
|
273
|
+
* with the field name): the longjmp unwinds through rb_hash_foreach
|
|
274
|
+
* (longjmp-safe) and is caught by rb_protect, crossing NO frame that holds a
|
|
275
|
+
* live C++ object. Every tld:: call here is individually try/catch(...)-wrapped
|
|
276
|
+
* so std::bad_alloc never unwinds through rb_hash_foreach's bookkeeping.
|
|
277
|
+
* =================================================================*/
|
|
278
|
+
|
|
279
|
+
struct build_ctx {
|
|
280
|
+
DynEvent *ev;
|
|
281
|
+
int oom; /* set if any tld:: call threw (std::bad_alloc etc.) */
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/* GC-compaction rule (spec §3.6): do every Ruby-side op (Symbol->String,
|
|
285
|
+
* transcode) FIRST, then take RSTRING_PTR and immediately copy into the event.
|
|
286
|
+
* The tld Add* calls are pure C++ (no Ruby allocation between pointer fetch and
|
|
287
|
+
* copy), GVL held throughout, so no concurrent GC moves the buffer.
|
|
288
|
+
*
|
|
289
|
+
* COMPACTION PITFALL (do not reintroduce): RSTRING_PTR(fname_str) must be taken
|
|
290
|
+
* only AFTER each branch's LAST Ruby allocation, never once at the top. In the
|
|
291
|
+
* text branch transcode_utf8(val) calls rb_str_encode, a Ruby allocation that
|
|
292
|
+
* can trigger GC compaction and relocate fname_str's character buffer; a fname
|
|
293
|
+
* pointer captured before it would be stale when AddString does strlen+memcpy.
|
|
294
|
+
* So fname is fetched immediately before the AddField/AddString pair in every
|
|
295
|
+
* branch, with no Ruby allocation in between. RB_GC_GUARD pins fname_str against
|
|
296
|
+
* collection; the late fetch handles movement. */
|
|
297
|
+
static int
|
|
298
|
+
add_field_i(VALUE key, VALUE val, VALUE arg)
|
|
299
|
+
{
|
|
300
|
+
struct build_ctx *ctx = (struct build_ctx *)arg;
|
|
301
|
+
DynEvent *ev = ctx->ev;
|
|
302
|
+
VALUE fname_str = coerce_name_utf8(key, "field name");
|
|
303
|
+
const char *fname; /* fetched late, per-branch — see header note */
|
|
304
|
+
|
|
305
|
+
switch (TYPE(val)) {
|
|
306
|
+
case T_STRING: {
|
|
307
|
+
if (rb_enc_get_index(val) == rb_ascii8bit_encindex()) {
|
|
308
|
+
/* BINARY -> TypeBinary (UINT16-counted). E7: > 65535 bytes. */
|
|
309
|
+
long n = RSTRING_LEN(val);
|
|
310
|
+
if (n > 0xFFFF) {
|
|
311
|
+
fname = RSTRING_PTR(fname_str);
|
|
312
|
+
rb_raise(rb_eArgError,
|
|
313
|
+
"winlog: binary field %s is %ld bytes (max 65535)",
|
|
314
|
+
fname, n);
|
|
315
|
+
}
|
|
316
|
+
/* No Ruby allocation past this point in this branch. */
|
|
317
|
+
fname = RSTRING_PTR(fname_str);
|
|
318
|
+
try {
|
|
319
|
+
ev->AddField(fname, tld::TypeBinary);
|
|
320
|
+
ev->AddBinary(RSTRING_PTR(val), (UINT16)n);
|
|
321
|
+
} catch (...) { ctx->oom = 1; return ST_STOP; }
|
|
322
|
+
} else {
|
|
323
|
+
/* text -> TypeUtf8String (NUL-terminated). Transcode + validate
|
|
324
|
+
* (E6 embedded NUL would silently truncate; E22 broken UTF-8;
|
|
325
|
+
* unmappable bytes raise Encoding::UndefinedConversionError).
|
|
326
|
+
* transcode_utf8 may allocate (rb_str_encode) and move fname_str,
|
|
327
|
+
* so fname is fetched only AFTER it. */
|
|
328
|
+
VALUE utf8 = transcode_utf8(val);
|
|
329
|
+
if (rb_enc_str_coderange(utf8) == ENC_CODERANGE_BROKEN) {
|
|
330
|
+
fname = RSTRING_PTR(fname_str);
|
|
331
|
+
rb_raise(rb_eArgError,
|
|
332
|
+
"winlog: text field %s is not valid UTF-8", fname);
|
|
333
|
+
}
|
|
334
|
+
if (memchr(RSTRING_PTR(utf8), '\0', RSTRING_LEN(utf8)) != NULL) {
|
|
335
|
+
fname = RSTRING_PTR(fname_str);
|
|
336
|
+
rb_raise(rb_eArgError,
|
|
337
|
+
"winlog: text field %s contains a NUL byte "
|
|
338
|
+
"(use a binary string for raw bytes)", fname);
|
|
339
|
+
}
|
|
340
|
+
{
|
|
341
|
+
/* fname and cstr fetched back-to-back, no allocation between
|
|
342
|
+
* here and the Add* copy. */
|
|
343
|
+
fname = RSTRING_PTR(fname_str);
|
|
344
|
+
const char *cstr = RSTRING_PTR(utf8);
|
|
345
|
+
try {
|
|
346
|
+
ev->AddField(fname, tld::TypeUtf8String);
|
|
347
|
+
ev->AddString(cstr);
|
|
348
|
+
} catch (...) { ctx->oom = 1; return ST_STOP; }
|
|
349
|
+
RB_GC_GUARD(utf8);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case T_FIXNUM:
|
|
355
|
+
case T_BIGNUM: {
|
|
356
|
+
INT64 v = (INT64)NUM2LL(val); /* RangeError outside INT64 (E9) */
|
|
357
|
+
fname = RSTRING_PTR(fname_str);
|
|
358
|
+
try {
|
|
359
|
+
ev->AddField(fname, tld::TypeInt64);
|
|
360
|
+
ev->AddValue(v);
|
|
361
|
+
} catch (...) { ctx->oom = 1; return ST_STOP; }
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case T_FLOAT: {
|
|
365
|
+
double v = NUM2DBL(val);
|
|
366
|
+
fname = RSTRING_PTR(fname_str);
|
|
367
|
+
try {
|
|
368
|
+
ev->AddField(fname, tld::TypeDouble);
|
|
369
|
+
ev->AddValue(v);
|
|
370
|
+
} catch (...) { ctx->oom = 1; return ST_STOP; }
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case T_TRUE:
|
|
374
|
+
case T_FALSE: {
|
|
375
|
+
INT32 v = (val == Qtrue) ? 1 : 0;
|
|
376
|
+
fname = RSTRING_PTR(fname_str);
|
|
377
|
+
try {
|
|
378
|
+
ev->AddField(fname, tld::TypeBool32);
|
|
379
|
+
ev->AddValue(v);
|
|
380
|
+
} catch (...) { ctx->oom = 1; return ST_STOP; }
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
default:
|
|
384
|
+
fname = RSTRING_PTR(fname_str);
|
|
385
|
+
rb_raise(rb_eTypeError,
|
|
386
|
+
"winlog: unsupported value type for field %s "
|
|
387
|
+
"(String, Integer, Float, true, or false; "
|
|
388
|
+
"nil and others are rejected)", fname);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
RB_GC_GUARD(fname_str);
|
|
392
|
+
return ST_CONTINUE;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
struct foreach_args {
|
|
396
|
+
VALUE fields;
|
|
397
|
+
struct build_ctx *ctx;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
static VALUE
|
|
401
|
+
build_fields_protected(VALUE a)
|
|
402
|
+
{
|
|
403
|
+
struct foreach_args *fa = (struct foreach_args *)a;
|
|
404
|
+
rb_hash_foreach(fa->fields, add_field_i, (VALUE)fa->ctx);
|
|
405
|
+
return Qnil;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* Core build + write. UNGATED. Returns the HRESULT as an Integer. Validation of
|
|
409
|
+
* the event name and control args happens BEFORE the heap event exists; field
|
|
410
|
+
* values are validated inside add_field_i (only reached here, i.e. only when
|
|
411
|
+
* the caller decided to build). The tld::Event is heap-owned by THIS frame and
|
|
412
|
+
* delete'd on every exit path before any raise (hygiene invariant 2). */
|
|
413
|
+
static VALUE
|
|
414
|
+
do_write(provider_t *pt, VALUE event, VALUE fields,
|
|
415
|
+
ULONGLONG keyword, UCHAR level, UCHAR opcode,
|
|
416
|
+
GUID *pActivity, GUID *pRelated)
|
|
417
|
+
{
|
|
418
|
+
VALUE ev_name = coerce_name_utf8(event, "event name");
|
|
419
|
+
const char *name = RSTRING_PTR(ev_name);
|
|
420
|
+
DynEvent *ev = NULL;
|
|
421
|
+
HRESULT hr;
|
|
422
|
+
int state = 0;
|
|
423
|
+
struct build_ctx ctx;
|
|
424
|
+
struct foreach_args fa;
|
|
425
|
+
|
|
426
|
+
/* Construct the heap event. The ctor builds metadata via std::vector and
|
|
427
|
+
* can throw std::bad_alloc; nothing is leaked on throw (ctor cleanup). */
|
|
428
|
+
try {
|
|
429
|
+
ev = new DynEvent(name, level, keyword,
|
|
430
|
+
0 /*tags*/, opcode, 0 /*task*/,
|
|
431
|
+
pActivity, pRelated);
|
|
432
|
+
} catch (...) {
|
|
433
|
+
ev = NULL;
|
|
434
|
+
}
|
|
435
|
+
RB_GC_GUARD(ev_name); /* keep name alive across the ctor */
|
|
436
|
+
if (ev == NULL)
|
|
437
|
+
rb_raise(rb_eNoMemError, "winlog: out of memory building event");
|
|
438
|
+
|
|
439
|
+
ctx.ev = ev;
|
|
440
|
+
ctx.oom = 0;
|
|
441
|
+
fa.fields = fields;
|
|
442
|
+
fa.ctx = &ctx;
|
|
443
|
+
|
|
444
|
+
if (!NIL_P(fields))
|
|
445
|
+
rb_protect(build_fields_protected, (VALUE)&fa, &state);
|
|
446
|
+
|
|
447
|
+
if (state) { delete ev; rb_jump_tag(state); } /* re-raise clean */
|
|
448
|
+
if (ctx.oom) {
|
|
449
|
+
delete ev;
|
|
450
|
+
rb_raise(rb_eNoMemError, "winlog: out of memory building event");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
hr = ev->Write(*pt->prov);
|
|
455
|
+
} catch (...) {
|
|
456
|
+
delete ev;
|
|
457
|
+
rb_raise(rb_eNoMemError, "winlog: out of memory writing event");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
delete ev;
|
|
461
|
+
return INT2NUM((int)hr);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* --------------------------------------------------- control-arg helpers */
|
|
465
|
+
|
|
466
|
+
/* level: Symbol already mapped to Integer by the Ruby layer; here we accept an
|
|
467
|
+
* Integer 1..255 (E5: 0 rejected). Raises ArgumentError from a clean frame. */
|
|
468
|
+
static UCHAR
|
|
469
|
+
level_to_uchar(VALUE vlevel)
|
|
470
|
+
{
|
|
471
|
+
long lv = NUM2LONG(vlevel);
|
|
472
|
+
if (lv < 1 || lv > 255)
|
|
473
|
+
rb_raise(rb_eArgError,
|
|
474
|
+
"winlog: level must be 1..255 (got %ld); 0 is not a valid "
|
|
475
|
+
"ETW level", lv);
|
|
476
|
+
return (UCHAR)lv;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* keyword: Integer 0..0x0000_FFFF_FFFF_FFFF. Reserved bits 48..63 -> ArgError
|
|
480
|
+
* (E3). Negative -> ArgError. Non-Integer is rejected by NUM2ULL/Check before. */
|
|
481
|
+
static ULONGLONG
|
|
482
|
+
keyword_to_ull(VALUE vkw)
|
|
483
|
+
{
|
|
484
|
+
/* Reject negatives explicitly: NUM2ULL would wrap them. */
|
|
485
|
+
if (RB_TYPE_P(vkw, T_FIXNUM) || RB_TYPE_P(vkw, T_BIGNUM)) {
|
|
486
|
+
if (rb_funcall(vkw, rb_intern("<"), 1, INT2FIX(0)) == Qtrue)
|
|
487
|
+
rb_raise(rb_eArgError, "winlog: keyword must not be negative");
|
|
488
|
+
} else {
|
|
489
|
+
rb_raise(rb_eTypeError, "winlog: keyword must be an Integer");
|
|
490
|
+
}
|
|
491
|
+
{
|
|
492
|
+
ULONGLONG kw = (ULONGLONG)NUM2ULL(vkw);
|
|
493
|
+
if (kw & WINLOG_KEYWORD_RESERVED_MASK)
|
|
494
|
+
rb_raise(rb_eArgError,
|
|
495
|
+
"winlog: keyword bits 48..63 are reserved by Microsoft "
|
|
496
|
+
"(mask 0x%016llX)", (unsigned long long)WINLOG_KEYWORD_RESERVED_MASK);
|
|
497
|
+
return kw;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* opcode: Integer 0..255 (Ruby layer maps Symbols to Integers first). */
|
|
502
|
+
static UCHAR
|
|
503
|
+
opcode_to_uchar(VALUE vop)
|
|
504
|
+
{
|
|
505
|
+
long op = NUM2LONG(vop);
|
|
506
|
+
if (op < 0 || op > 255)
|
|
507
|
+
rb_raise(rb_eArgError, "winlog: opcode must be 0..255 (got %ld)", op);
|
|
508
|
+
return (UCHAR)op;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/* =================================================================
|
|
512
|
+
* Winlog::Provider — C methods
|
|
513
|
+
* =================================================================*/
|
|
514
|
+
|
|
515
|
+
/* Provider#_register(name) — construct + register the tld::Provider. Called by
|
|
516
|
+
* Ruby initialize AFTER name validation. Double-init guard (lithos pattern):
|
|
517
|
+
* raise Winlog::Error if already registered or closed (T11). Never raises on
|
|
518
|
+
* EventRegister failure: the tld handle is a benign no-op (E11). */
|
|
519
|
+
static VALUE
|
|
520
|
+
provider_register(VALUE self, VALUE name)
|
|
521
|
+
{
|
|
522
|
+
provider_t *pt = provider_get(self);
|
|
523
|
+
const char *cname;
|
|
524
|
+
|
|
525
|
+
if (pt->prov != NULL || pt->closed)
|
|
526
|
+
rb_raise(eError, "winlog: provider already registered");
|
|
527
|
+
|
|
528
|
+
/* name is a validated printable-ASCII String from the Ruby layer; a strict
|
|
529
|
+
* subset of UTF-8, safe for the char* (UTF-8) tld ctor. NUL-terminate via
|
|
530
|
+
* StringValueCStr (raises ArgumentError on an embedded NUL — defense in
|
|
531
|
+
* depth; the Ruby validator already forbids it). */
|
|
532
|
+
cname = StringValueCStr(name);
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
pt->prov = new tld::Provider(cname);
|
|
536
|
+
} catch (...) {
|
|
537
|
+
pt->prov = NULL;
|
|
538
|
+
}
|
|
539
|
+
if (pt->prov == NULL)
|
|
540
|
+
rb_raise(rb_eNoMemError, "winlog: out of memory registering provider");
|
|
541
|
+
|
|
542
|
+
return self;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/* Provider#_write(level, event, fields, keyword, opcode, act_or_nil, rel_or_nil)
|
|
546
|
+
* UNGATED build+write, returns HRESULT Integer. Test/benchmark plumbing only
|
|
547
|
+
* (spec §3.3): EventWriteTransfer returns S_OK with no listener, so the full
|
|
548
|
+
* build path is exercisable without an elevated session. */
|
|
549
|
+
static VALUE
|
|
550
|
+
provider_write(VALUE self, VALUE vlevel, VALUE event, VALUE fields,
|
|
551
|
+
VALUE vkeyword, VALUE vopcode, VALUE vact, VALUE vrel)
|
|
552
|
+
{
|
|
553
|
+
provider_t *pt = provider_live(self);
|
|
554
|
+
UCHAR level = level_to_uchar(vlevel);
|
|
555
|
+
ULONGLONG keyword = keyword_to_ull(vkeyword);
|
|
556
|
+
UCHAR opcode = opcode_to_uchar(vopcode);
|
|
557
|
+
GUID act, rel;
|
|
558
|
+
GUID *pAct = NULL, *pRel = NULL;
|
|
559
|
+
|
|
560
|
+
if (!NIL_P(fields))
|
|
561
|
+
Check_Type(fields, T_HASH);
|
|
562
|
+
|
|
563
|
+
/* related: without activity: is meaningless (E18). The Ruby layer also
|
|
564
|
+
* guards this, but keep the C contract self-standing. */
|
|
565
|
+
if (!NIL_P(vrel) && NIL_P(vact))
|
|
566
|
+
rb_raise(rb_eArgError,
|
|
567
|
+
"winlog: related activity given without an activity id");
|
|
568
|
+
|
|
569
|
+
if (!NIL_P(vact)) {
|
|
570
|
+
if (!parse_guid(vact, &act))
|
|
571
|
+
rb_raise(rb_eArgError,
|
|
572
|
+
"winlog: activity must be a 36-char hyphenated GUID string");
|
|
573
|
+
pAct = &act;
|
|
574
|
+
}
|
|
575
|
+
if (!NIL_P(vrel)) {
|
|
576
|
+
if (!parse_guid(vrel, &rel))
|
|
577
|
+
rb_raise(rb_eArgError,
|
|
578
|
+
"winlog: related must be a 36-char hyphenated GUID string");
|
|
579
|
+
pRel = &rel;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return do_write(pt, event, fields, keyword, level, opcode, pAct, pRel);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/* Provider#_log(level, event, fields) — the full gated public path. Extracts
|
|
586
|
+
* control kwargs from +fields+ (rb_hash_lookup2 with a Qundef sentinel: cheap,
|
|
587
|
+
* no allocation, not iteration), validates them ALWAYS (E1), gates on
|
|
588
|
+
* IsEnabled(level, keyword), and only then builds+writes. The disabled return
|
|
589
|
+
* (Qfalse) happens BEFORE any field value is read (E1/E2 asymmetry). */
|
|
590
|
+
static VALUE
|
|
591
|
+
provider_log(VALUE self, VALUE vlevel, VALUE event, VALUE fields)
|
|
592
|
+
{
|
|
593
|
+
provider_t *pt = provider_live(self);
|
|
594
|
+
UCHAR level = level_to_uchar(vlevel); /* always validated */
|
|
595
|
+
VALUE vkw, vop, vact, vrel;
|
|
596
|
+
ULONGLONG keyword;
|
|
597
|
+
UCHAR opcode;
|
|
598
|
+
GUID act, rel;
|
|
599
|
+
GUID *pAct = NULL, *pRel = NULL;
|
|
600
|
+
VALUE hr;
|
|
601
|
+
|
|
602
|
+
Check_Type(fields, T_HASH);
|
|
603
|
+
|
|
604
|
+
/* Extract reserved control kwargs by exact Symbol key. String keys with the
|
|
605
|
+
* same text are NEVER control args (E21) — they stay as fields. */
|
|
606
|
+
vkw = rb_hash_lookup2(fields, ID2SYM(rb_intern("keyword")), Qundef);
|
|
607
|
+
vop = rb_hash_lookup2(fields, ID2SYM(rb_intern("opcode")), Qundef);
|
|
608
|
+
vact = rb_hash_lookup2(fields, ID2SYM(rb_intern("activity")), Qundef);
|
|
609
|
+
vrel = rb_hash_lookup2(fields, ID2SYM(rb_intern("related")), Qundef);
|
|
610
|
+
|
|
611
|
+
keyword = (vkw == Qundef) ? 0ULL : keyword_to_ull(vkw); /* always validated */
|
|
612
|
+
|
|
613
|
+
if (vop == Qundef) {
|
|
614
|
+
opcode = 0;
|
|
615
|
+
} else {
|
|
616
|
+
opcode = opcode_to_uchar(vop); /* Ruby layer maps Symbols first */
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/* related: without activity: -> ArgumentError, even disabled (E18, E1). */
|
|
620
|
+
if (vrel != Qundef && (vact == Qundef || NIL_P(vact)))
|
|
621
|
+
rb_raise(rb_eArgError,
|
|
622
|
+
"winlog: related activity given without an activity id");
|
|
623
|
+
|
|
624
|
+
if (vact != Qundef && !NIL_P(vact)) {
|
|
625
|
+
if (!parse_guid(vact, &act))
|
|
626
|
+
rb_raise(rb_eArgError,
|
|
627
|
+
"winlog: activity must be a 36-char hyphenated GUID string");
|
|
628
|
+
pAct = &act;
|
|
629
|
+
}
|
|
630
|
+
if (vrel != Qundef && !NIL_P(vrel)) {
|
|
631
|
+
if (!parse_guid(vrel, &rel))
|
|
632
|
+
rb_raise(rb_eArgError,
|
|
633
|
+
"winlog: related must be a 36-char hyphenated GUID string");
|
|
634
|
+
pRel = &rel;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* Validate the event name on EVERY call (E1/E6), enabled or not. This both
|
|
638
|
+
* matches the documented "control args always validated" contract and
|
|
639
|
+
* coerces from a clean frame before any C++ object exists. */
|
|
640
|
+
(void)coerce_name_utf8(event, "event name");
|
|
641
|
+
|
|
642
|
+
/* The gate. No system call; reads in-process enable state (E1, ~0.4 ns). */
|
|
643
|
+
if (!pt->prov->IsEnabled(level, keyword))
|
|
644
|
+
return Qfalse;
|
|
645
|
+
|
|
646
|
+
/* Enabled: build + write. Strip the control kwargs out so they are not
|
|
647
|
+
* emitted as fields. dup so we never mutate the caller's hash. */
|
|
648
|
+
{
|
|
649
|
+
VALUE eff = fields;
|
|
650
|
+
if (vkw != Qundef || vop != Qundef || vact != Qundef || vrel != Qundef) {
|
|
651
|
+
eff = rb_hash_dup(fields);
|
|
652
|
+
if (vkw != Qundef) rb_hash_delete(eff, ID2SYM(rb_intern("keyword")));
|
|
653
|
+
if (vop != Qundef) rb_hash_delete(eff, ID2SYM(rb_intern("opcode")));
|
|
654
|
+
if (vact != Qundef) rb_hash_delete(eff, ID2SYM(rb_intern("activity")));
|
|
655
|
+
if (vrel != Qundef) rb_hash_delete(eff, ID2SYM(rb_intern("related")));
|
|
656
|
+
}
|
|
657
|
+
hr = do_write(pt, event, eff, keyword, level, opcode, pAct, pRel);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/* hr success (S_OK == 0) -> true; any ETW drop -> false (never raise, E8). */
|
|
661
|
+
return (NUM2INT(hr) == 0) ? Qtrue : Qfalse;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/* Provider#_enabled(level_or_nil, keyword) — IsEnabled bridge. level nil means
|
|
665
|
+
* "enabled at any level" (uses IsEnabled() / level 0xFF probe semantics). */
|
|
666
|
+
static VALUE
|
|
667
|
+
provider_enabled(VALUE self, VALUE vlevel, VALUE vkeyword)
|
|
668
|
+
{
|
|
669
|
+
provider_t *pt = provider_live(self);
|
|
670
|
+
ULONGLONG keyword = keyword_to_ull(vkeyword);
|
|
671
|
+
|
|
672
|
+
if (NIL_P(vlevel)) {
|
|
673
|
+
/* "any level": enabled at all, with the keyword filter applied. Level 1
|
|
674
|
+
* (most restrictive that any session enabling implies) — but tld's
|
|
675
|
+
* IsEnabled(level,keyword) checks level < m_levelPlus1, so to mean "any
|
|
676
|
+
* level enabled" we probe with level 1 (a session enabling at any level
|
|
677
|
+
* L >= 1 sets m_levelPlus1 >= 2, so 1 < m_levelPlus1 holds). Combined
|
|
678
|
+
* with the keyword check this is the documented "enabled at any level"
|
|
679
|
+
* gate. */
|
|
680
|
+
UCHAR level = 1;
|
|
681
|
+
return pt->prov->IsEnabled(level, keyword) ? Qtrue : Qfalse;
|
|
682
|
+
} else {
|
|
683
|
+
UCHAR level = level_to_uchar(vlevel);
|
|
684
|
+
return pt->prov->IsEnabled(level, keyword) ? Qtrue : Qfalse;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* Provider#guid -> 36-char lowercase hyphenated String (from tld Provider::Id,
|
|
689
|
+
* authoritative; equals Winlog.guid_for(name)). Set before Init runs, untouched
|
|
690
|
+
* by registration failure (E11). Raises Winlog::Closed after close. */
|
|
691
|
+
static VALUE
|
|
692
|
+
provider_guid(VALUE self)
|
|
693
|
+
{
|
|
694
|
+
provider_t *pt = provider_live(self);
|
|
695
|
+
return guid_to_rb(pt->prov->Id());
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/* Provider#registered? -> true if EventRegister succeeded (E11). */
|
|
699
|
+
static VALUE
|
|
700
|
+
provider_registered_p(VALUE self)
|
|
701
|
+
{
|
|
702
|
+
provider_t *pt = provider_live(self);
|
|
703
|
+
return SUCCEEDED(pt->prov->InitializationResult()) ? Qtrue : Qfalse;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* Provider#registration_result -> raw HRESULT Integer; 0 on success. */
|
|
707
|
+
static VALUE
|
|
708
|
+
provider_registration_result(VALUE self)
|
|
709
|
+
{
|
|
710
|
+
provider_t *pt = provider_live(self);
|
|
711
|
+
return INT2NUM((int)pt->prov->InitializationResult());
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* Provider#close -> nil. Idempotent (E20). Unregister + free the native
|
|
715
|
+
* provider now; safe to call again, on a never-registered object, etc. */
|
|
716
|
+
static VALUE
|
|
717
|
+
provider_close(VALUE self)
|
|
718
|
+
{
|
|
719
|
+
provider_t *pt = provider_get(self);
|
|
720
|
+
if (pt->prov) {
|
|
721
|
+
delete pt->prov; /* tld dtor: EventUnregister + HeapFree metadata */
|
|
722
|
+
pt->prov = NULL;
|
|
723
|
+
}
|
|
724
|
+
pt->closed = 1;
|
|
725
|
+
return Qnil;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/* Provider#closed? -> true/false. */
|
|
729
|
+
static VALUE
|
|
730
|
+
provider_closed_p(VALUE self)
|
|
731
|
+
{
|
|
732
|
+
provider_t *pt = provider_get(self);
|
|
733
|
+
return (pt->prov == NULL || pt->closed) ? Qtrue : Qfalse;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/* =================================================================
|
|
737
|
+
* Module function: Winlog.new_activity_id
|
|
738
|
+
* =================================================================*/
|
|
739
|
+
|
|
740
|
+
/* Winlog.new_activity_id -> 36-char lowercase hyphenated GUID String. Wraps
|
|
741
|
+
* EventActivityIdControl(CREATE_ID), which only GENERATES an id and never reads
|
|
742
|
+
* or writes the calling thread's implicit activity id (E17 fiber-safety).
|
|
743
|
+
* Raises Winlog::Error only if the OS call fails (practically never). */
|
|
744
|
+
static VALUE
|
|
745
|
+
winlog_new_activity_id(VALUE self)
|
|
746
|
+
{
|
|
747
|
+
GUID g;
|
|
748
|
+
ULONG status;
|
|
749
|
+
|
|
750
|
+
memset(&g, 0, sizeof g);
|
|
751
|
+
status = EventActivityIdControl(EVENT_ACTIVITY_CTRL_CREATE_ID, &g);
|
|
752
|
+
if (status != ERROR_SUCCESS)
|
|
753
|
+
rb_raise(eError,
|
|
754
|
+
"winlog: EventActivityIdControl(CREATE_ID) failed (error %lu)",
|
|
755
|
+
(unsigned long)status);
|
|
756
|
+
return guid_to_rb(g);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/* ----------------------------------------------------------------- Init ---- */
|
|
760
|
+
|
|
761
|
+
extern "C" void
|
|
762
|
+
Init_winlog(void)
|
|
763
|
+
{
|
|
764
|
+
mWinlog = rb_define_module("Winlog");
|
|
765
|
+
|
|
766
|
+
eError = rb_define_class_under(mWinlog, "Error", rb_eStandardError);
|
|
767
|
+
eClosed = rb_define_class_under(mWinlog, "Closed", eError);
|
|
768
|
+
|
|
769
|
+
cProvider = rb_define_class_under(mWinlog, "Provider", rb_cObject);
|
|
770
|
+
rb_define_alloc_func(cProvider, provider_alloc);
|
|
771
|
+
|
|
772
|
+
/* Private C bridges (underscore convention; hidden by the Ruby layer's
|
|
773
|
+
* `private`). The validated, keyword-aware public API lives in lib. */
|
|
774
|
+
rb_define_private_method(cProvider, "_register", RUBY_METHOD_FUNC(provider_register), 1);
|
|
775
|
+
rb_define_private_method(cProvider, "_log", RUBY_METHOD_FUNC(provider_log), 3);
|
|
776
|
+
rb_define_private_method(cProvider, "_write", RUBY_METHOD_FUNC(provider_write), 7);
|
|
777
|
+
rb_define_private_method(cProvider, "_enabled", RUBY_METHOD_FUNC(provider_enabled), 2);
|
|
778
|
+
|
|
779
|
+
/* Public read-only / simple methods exposed directly (spec §3.3). */
|
|
780
|
+
rb_define_method(cProvider, "guid", RUBY_METHOD_FUNC(provider_guid), 0);
|
|
781
|
+
rb_define_method(cProvider, "registered?", RUBY_METHOD_FUNC(provider_registered_p), 0);
|
|
782
|
+
rb_define_method(cProvider, "registration_result", RUBY_METHOD_FUNC(provider_registration_result), 0);
|
|
783
|
+
rb_define_method(cProvider, "close", RUBY_METHOD_FUNC(provider_close), 0);
|
|
784
|
+
rb_define_method(cProvider, "closed?", RUBY_METHOD_FUNC(provider_closed_p), 0);
|
|
785
|
+
|
|
786
|
+
rb_define_singleton_method(mWinlog, "new_activity_id",
|
|
787
|
+
RUBY_METHOD_FUNC(winlog_new_activity_id), 0);
|
|
788
|
+
}
|