winreg 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 +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/ext/winreg/extconf.rb +25 -0
- data/ext/winreg/winreg.c +909 -0
- data/lib/winreg/version.rb +5 -0
- data/lib/winreg.rb +795 -0
- metadata +124 -0
data/ext/winreg/winreg.c
ADDED
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* winreg — typed Windows registry access for Ruby: exact wire formats,
|
|
3
|
+
* least-privilege opens, WOW64 views as a first-class option, and change
|
|
4
|
+
* notification (RegNotifyChangeKeyValue) that cooperates with a fiber
|
|
5
|
+
* scheduler.
|
|
6
|
+
*
|
|
7
|
+
* PURE C (no C++), so rb_raise/longjmp is the normal, safe error mechanism —
|
|
8
|
+
* the same discipline as winipc/winloop, and the opposite of the lithos /EHsc
|
|
9
|
+
* hazard. Every HKEY/HANDLE lives in a TypedData wrapper whose free hook
|
|
10
|
+
* closes it, so a forgotten #close never leaks a handle. The ONLY blocking
|
|
11
|
+
* call in the gem is Watch#_wait's WaitForMultipleObjects, which releases the
|
|
12
|
+
* GVL via rb_nogvl(..., RB_NOGVL_INTR_FAIL) with an unblock function that
|
|
13
|
+
* sets a private stop event. Registry data/metadata calls are local,
|
|
14
|
+
* microsecond-scale hive operations and run with the GVL held — which also
|
|
15
|
+
* makes ALL key_t/watch_t state mutation GVL-serialized and race-free by
|
|
16
|
+
* construction.
|
|
17
|
+
*
|
|
18
|
+
* Watch close/wait protocol invariants (spec §3.6 / R6 — do not weaken):
|
|
19
|
+
* 1. ALL watch_t state mutation happens under the GVL; the GVL-released
|
|
20
|
+
* region only READS the two handle values.
|
|
21
|
+
* 2. EVERY wake path re-checks `closed` before anything else (WFMO reports
|
|
22
|
+
* the lowest-index signaled handle, so a notify wake / timeout /
|
|
23
|
+
* WAIT_FAILED can mask a cross-thread #close that ran while off-GVL).
|
|
24
|
+
* 3. Interrupts are delivered ONLY at explicit rb_thread_check_ints()
|
|
25
|
+
* points (rb_nogvl + RB_NOGVL_INTR_FAIL, never plain
|
|
26
|
+
* rb_thread_call_without_gvl) — so the rearm always happens before a
|
|
27
|
+
* pending Thread#kill / Timeout fires, and `waiting` can never wedge.
|
|
28
|
+
*
|
|
29
|
+
* include <ruby.h> before the Windows headers; never name a variable IN/OUT.
|
|
30
|
+
* Links advapi32 (registry APIs); kernel32 (events, waits, expansion) is
|
|
31
|
+
* linked by default.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
#include <ruby.h>
|
|
35
|
+
#include <ruby/thread.h>
|
|
36
|
+
#include <ruby/encoding.h>
|
|
37
|
+
#include <limits.h>
|
|
38
|
+
|
|
39
|
+
#define WIN32_LEAN_AND_MEAN
|
|
40
|
+
#include <windows.h>
|
|
41
|
+
|
|
42
|
+
/* ------------------------------------------------------------------ globals */
|
|
43
|
+
|
|
44
|
+
static VALUE mWinreg;
|
|
45
|
+
static VALUE cKey, cWatch;
|
|
46
|
+
static VALUE eError, eOSError, eNotFound, eAccessDenied, eKeyDeleted,
|
|
47
|
+
eTypeMismatch, eMalformedValue, eClosed;
|
|
48
|
+
|
|
49
|
+
/* ------------------------------------------------------------ small helpers */
|
|
50
|
+
|
|
51
|
+
/* UTF-8 (any-encoding, exported) Ruby String -> freshly xmalloc'd
|
|
52
|
+
* NUL-terminated UTF-16. Caller xfrees; never held across a raise.
|
|
53
|
+
* MB_ERR_INVALID_CHARS so invalid UTF-8 raises rather than silently mapping
|
|
54
|
+
* to U+FFFD (names must round-trip exactly). */
|
|
55
|
+
static WCHAR *
|
|
56
|
+
to_wide(VALUE str)
|
|
57
|
+
{
|
|
58
|
+
int len, n;
|
|
59
|
+
WCHAR *w;
|
|
60
|
+
str = rb_str_export_to_enc(StringValue(str), rb_utf8_encoding());
|
|
61
|
+
len = (int)RSTRING_LEN(str);
|
|
62
|
+
if (len == 0) {
|
|
63
|
+
w = (WCHAR *)xmalloc(sizeof(WCHAR));
|
|
64
|
+
w[0] = 0;
|
|
65
|
+
return w;
|
|
66
|
+
}
|
|
67
|
+
n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, RSTRING_PTR(str), len, NULL, 0);
|
|
68
|
+
if (n <= 0) rb_raise(eError, "winreg: invalid UTF-8 in name");
|
|
69
|
+
w = (WCHAR *)xmalloc(sizeof(WCHAR) * ((size_t)n + 1));
|
|
70
|
+
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, RSTRING_PTR(str), len, w, n);
|
|
71
|
+
w[n] = 0;
|
|
72
|
+
return w;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Same conversion, but the UTF-16 lives inside a Ruby String (BINARY), so it
|
|
76
|
+
* is GC-owned and safe to hold across later Ruby allocations / raises (used
|
|
77
|
+
* by the probe-fill read loop and the expansion helper, where rb_str_new /
|
|
78
|
+
* rb_str_resize interleave with uses of the name). Take RSTRING_PTR fresh
|
|
79
|
+
* after every Ruby allocation (GC compaction rule). */
|
|
80
|
+
static VALUE
|
|
81
|
+
to_wide_val(VALUE str)
|
|
82
|
+
{
|
|
83
|
+
int len, n;
|
|
84
|
+
VALUE w;
|
|
85
|
+
str = rb_str_export_to_enc(StringValue(str), rb_utf8_encoding());
|
|
86
|
+
len = (int)RSTRING_LEN(str);
|
|
87
|
+
if (len == 0) {
|
|
88
|
+
w = rb_str_new(NULL, sizeof(WCHAR));
|
|
89
|
+
memset(RSTRING_PTR(w), 0, sizeof(WCHAR));
|
|
90
|
+
return w;
|
|
91
|
+
}
|
|
92
|
+
n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, RSTRING_PTR(str), len, NULL, 0);
|
|
93
|
+
if (n <= 0) rb_raise(eError, "winreg: invalid UTF-8 in name");
|
|
94
|
+
w = rb_str_new(NULL, (long)(((size_t)n + 1) * sizeof(WCHAR)));
|
|
95
|
+
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, RSTRING_PTR(str), len,
|
|
96
|
+
(WCHAR *)RSTRING_PTR(w), n);
|
|
97
|
+
((WCHAR *)RSTRING_PTR(w))[n] = 0;
|
|
98
|
+
return w;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* UTF-16 -> UTF-8 Ruby String, LENIENT: default flags map lone surrogates to
|
|
102
|
+
* U+FFFD by design (lenient-names policy, spec E4/E14) so enumeration never
|
|
103
|
+
* aborts on one hostile name. wlen in WCHARs (excluding any NUL). */
|
|
104
|
+
static VALUE
|
|
105
|
+
wide_to_utf8(const WCHAR *w, int wlen)
|
|
106
|
+
{
|
|
107
|
+
int n;
|
|
108
|
+
VALUE s;
|
|
109
|
+
if (wlen <= 0) return rb_utf8_str_new("", 0);
|
|
110
|
+
n = WideCharToMultiByte(CP_UTF8, 0, w, wlen, NULL, 0, NULL, NULL);
|
|
111
|
+
if (n <= 0) rb_raise(eError, "winreg: cannot convert UTF-16 name to UTF-8");
|
|
112
|
+
s = rb_str_new(NULL, n);
|
|
113
|
+
WideCharToMultiByte(CP_UTF8, 0, w, wlen, RSTRING_PTR(s), n, NULL, NULL);
|
|
114
|
+
rb_enc_associate(s, rb_utf8_encoding());
|
|
115
|
+
return s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Build + raise a winreg error carrying @code. The code must be captured by
|
|
119
|
+
* the caller immediately at the failing call site (registry APIs return it as
|
|
120
|
+
* their LSTATUS result; GetLastError is NOT used for them). */
|
|
121
|
+
static void
|
|
122
|
+
raise_code(VALUE klass, const char *api, DWORD code)
|
|
123
|
+
{
|
|
124
|
+
VALUE exc, msg;
|
|
125
|
+
WCHAR *buf = NULL;
|
|
126
|
+
char detail[512];
|
|
127
|
+
DWORD n;
|
|
128
|
+
|
|
129
|
+
detail[0] = 0;
|
|
130
|
+
n = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
|
131
|
+
FORMAT_MESSAGE_IGNORE_INSERTS, NULL, code,
|
|
132
|
+
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&buf, 0, NULL);
|
|
133
|
+
if (n && buf) {
|
|
134
|
+
/* trim trailing CR/LF/period, then copy to a stack buffer */
|
|
135
|
+
while (n && (buf[n-1] == L'\r' || buf[n-1] == L'\n' || buf[n-1] == L'.')) buf[--n] = 0;
|
|
136
|
+
WideCharToMultiByte(CP_UTF8, 0, buf, -1, detail, (int)sizeof(detail), NULL, NULL);
|
|
137
|
+
detail[sizeof(detail) - 1] = 0;
|
|
138
|
+
}
|
|
139
|
+
/* Release the OS buffer BEFORE any Ruby allocation, so an OOM longjmp from
|
|
140
|
+
* rb_sprintf / rb_exc_new_str cannot leak it. */
|
|
141
|
+
if (buf) LocalFree(buf);
|
|
142
|
+
|
|
143
|
+
if (detail[0])
|
|
144
|
+
msg = rb_sprintf("%s: %s (error %lu)", api, detail, (unsigned long)code);
|
|
145
|
+
else
|
|
146
|
+
msg = rb_sprintf("%s failed (error %lu)", api, (unsigned long)code);
|
|
147
|
+
exc = rb_exc_new_str(klass, msg);
|
|
148
|
+
rb_iv_set(exc, "@code", ULONG2NUM(code));
|
|
149
|
+
rb_exc_raise(exc);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Map a Win32 / LSTATUS code to the winreg subclass and raise. ERROR_MORE_DATA
|
|
153
|
+
* and ERROR_NO_MORE_ITEMS are flow control at every call site, never raised. */
|
|
154
|
+
static void
|
|
155
|
+
raise_lstatus(const char *api, LONG rc)
|
|
156
|
+
{
|
|
157
|
+
VALUE klass = eOSError;
|
|
158
|
+
switch (rc) {
|
|
159
|
+
case ERROR_FILE_NOT_FOUND: klass = eNotFound; break; /* 2 */
|
|
160
|
+
case ERROR_ACCESS_DENIED: klass = eAccessDenied; break; /* 5 */
|
|
161
|
+
case ERROR_KEY_DELETED: klass = eKeyDeleted; break; /* 1018 */
|
|
162
|
+
default: break;
|
|
163
|
+
}
|
|
164
|
+
raise_code(klass, api, (DWORD)rc);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* GetLastError-reporting APIs (events, waits, expansion) use the same map. */
|
|
168
|
+
static void
|
|
169
|
+
raise_gle(const char *api, DWORD code)
|
|
170
|
+
{
|
|
171
|
+
raise_lstatus(api, (LONG)code);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Length of a Ruby String as a DWORD, guarding the 4 GiB ceiling. */
|
|
175
|
+
static DWORD
|
|
176
|
+
dword_len(VALUE str)
|
|
177
|
+
{
|
|
178
|
+
long n;
|
|
179
|
+
StringValue(str);
|
|
180
|
+
n = RSTRING_LEN(str);
|
|
181
|
+
if ((unsigned long long)n > 0xFFFFFFFFull)
|
|
182
|
+
rb_raise(rb_eArgError, "winreg: data too large (> 4 GiB)");
|
|
183
|
+
return (DWORD)n;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Predefined-root Integer (0x80000000..0x80000005) -> HKEY. winreg.h defines
|
|
187
|
+
* these sign-extended through LONG ((HKEY)(ULONG_PTR)((LONG)0x80000001)), so
|
|
188
|
+
* we must replicate the sign extension on 64-bit. */
|
|
189
|
+
static HKEY
|
|
190
|
+
root_hkey(VALUE v)
|
|
191
|
+
{
|
|
192
|
+
return (HKEY)(ULONG_PTR)(LONG_PTR)(LONG)(DWORD)NUM2ULONG(v);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#define WOW64_VIEW_MASK (KEY_WOW64_64KEY | KEY_WOW64_32KEY) /* 0x300 */
|
|
196
|
+
|
|
197
|
+
/* =====================================================================
|
|
198
|
+
* Winreg::Key
|
|
199
|
+
* ===================================================================== */
|
|
200
|
+
|
|
201
|
+
typedef struct {
|
|
202
|
+
HKEY h; /* sentinel: NULL */
|
|
203
|
+
DWORD view_sam; /* 0, KEY_WOW64_64KEY, or KEY_WOW64_32KEY; re-applied to children */
|
|
204
|
+
int created; /* REG_CREATED_NEW_KEY disposition */
|
|
205
|
+
int predefined; /* bare-root Key wrapping a predefined pseudo-handle:
|
|
206
|
+
#close and the free hook skip RegCloseKey */
|
|
207
|
+
int closed;
|
|
208
|
+
} key_t;
|
|
209
|
+
|
|
210
|
+
static void
|
|
211
|
+
key_free(void *p)
|
|
212
|
+
{
|
|
213
|
+
key_t *k = (key_t *)p;
|
|
214
|
+
/* Safety net, never the API. Never RegCloseKey a predefined pseudo-handle. */
|
|
215
|
+
if (k->h && !k->predefined) RegCloseKey(k->h);
|
|
216
|
+
xfree(k);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
static const rb_data_type_t key_type = {
|
|
220
|
+
"Winreg::Key", { 0, key_free, 0, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
static VALUE
|
|
224
|
+
key_alloc(VALUE klass)
|
|
225
|
+
{
|
|
226
|
+
key_t *k;
|
|
227
|
+
VALUE obj = TypedData_Make_Struct(klass, key_t, &key_type, k);
|
|
228
|
+
k->h = NULL;
|
|
229
|
+
k->view_sam = 0;
|
|
230
|
+
k->created = 0;
|
|
231
|
+
k->predefined = 0;
|
|
232
|
+
k->closed = 1; /* closed until _open/_create succeeds: .allocate abuse is inert */
|
|
233
|
+
return obj;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static key_t *
|
|
237
|
+
key_get(VALUE self)
|
|
238
|
+
{
|
|
239
|
+
key_t *k;
|
|
240
|
+
TypedData_Get_Struct(self, key_t, &key_type, k);
|
|
241
|
+
return k;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static key_t *
|
|
245
|
+
key_live(VALUE self)
|
|
246
|
+
{
|
|
247
|
+
key_t *k = key_get(self);
|
|
248
|
+
if (k->closed || !k->h) rb_raise(eClosed, "winreg: key is closed");
|
|
249
|
+
return k;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Key._open(root, subpath_or_nil, sam) — private singleton. A nil subpath is
|
|
253
|
+
* a bare root: per the documented RegOpenKeyExW behavior a NULL lpSubKey on a
|
|
254
|
+
* predefined key returns the SAME process-global pseudo-handle, so no OS call
|
|
255
|
+
* is made; the Key wraps the predefined handle value with `predefined` set
|
|
256
|
+
* and #close / the free hook skip RegCloseKey. */
|
|
257
|
+
static VALUE
|
|
258
|
+
key_s_open(VALUE klass, VALUE vroot, VALUE vsub, VALUE vsam)
|
|
259
|
+
{
|
|
260
|
+
VALUE obj = key_alloc(cKey);
|
|
261
|
+
key_t *k = key_get(obj);
|
|
262
|
+
HKEY root = root_hkey(vroot);
|
|
263
|
+
DWORD sam = (DWORD)NUM2ULONG(vsam);
|
|
264
|
+
HKEY h = NULL;
|
|
265
|
+
LONG rc;
|
|
266
|
+
WCHAR *w;
|
|
267
|
+
|
|
268
|
+
k->view_sam = sam & WOW64_VIEW_MASK;
|
|
269
|
+
if (NIL_P(vsub)) {
|
|
270
|
+
k->h = root;
|
|
271
|
+
k->predefined = 1;
|
|
272
|
+
k->closed = 0;
|
|
273
|
+
return obj;
|
|
274
|
+
}
|
|
275
|
+
w = to_wide(vsub);
|
|
276
|
+
rc = RegOpenKeyExW(root, w, 0, sam, &h);
|
|
277
|
+
xfree(w);
|
|
278
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegOpenKeyExW", rc);
|
|
279
|
+
k->h = h;
|
|
280
|
+
k->closed = 0;
|
|
281
|
+
return obj;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* Key._create(root, subpath, sam) — private singleton. Creates all missing
|
|
285
|
+
* intermediate keys; NULL SECURITY_ATTRIBUTES inherits the parent SD. */
|
|
286
|
+
static VALUE
|
|
287
|
+
key_s_create(VALUE klass, VALUE vroot, VALUE vsub, VALUE vsam)
|
|
288
|
+
{
|
|
289
|
+
VALUE obj = key_alloc(cKey);
|
|
290
|
+
key_t *k = key_get(obj);
|
|
291
|
+
HKEY root = root_hkey(vroot);
|
|
292
|
+
DWORD sam = (DWORD)NUM2ULONG(vsam);
|
|
293
|
+
HKEY h = NULL;
|
|
294
|
+
DWORD disp = 0;
|
|
295
|
+
LONG rc;
|
|
296
|
+
WCHAR *w;
|
|
297
|
+
|
|
298
|
+
k->view_sam = sam & WOW64_VIEW_MASK;
|
|
299
|
+
w = to_wide(vsub);
|
|
300
|
+
rc = RegCreateKeyExW(root, w, 0, NULL, REG_OPTION_NON_VOLATILE, sam, NULL, &h, &disp);
|
|
301
|
+
xfree(w);
|
|
302
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegCreateKeyExW", rc);
|
|
303
|
+
k->h = h;
|
|
304
|
+
k->created = (disp == REG_CREATED_NEW_KEY) ? 1 : 0;
|
|
305
|
+
k->closed = 0;
|
|
306
|
+
return obj;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Key#_open_child(subpath, sam) — handle-relative open. The parent's WOW64
|
|
310
|
+
* view is OR'd in here, so children can never escape it (spec E14). */
|
|
311
|
+
static VALUE
|
|
312
|
+
key_open_child(VALUE self, VALUE vsub, VALUE vsam)
|
|
313
|
+
{
|
|
314
|
+
key_t *k = key_live(self);
|
|
315
|
+
VALUE obj = key_alloc(cKey); /* wrapper first: no handle live across OOM */
|
|
316
|
+
key_t *child = key_get(obj);
|
|
317
|
+
DWORD sam = (DWORD)NUM2ULONG(vsam) | k->view_sam;
|
|
318
|
+
HKEY h = NULL;
|
|
319
|
+
LONG rc;
|
|
320
|
+
WCHAR *w;
|
|
321
|
+
|
|
322
|
+
w = to_wide(vsub);
|
|
323
|
+
rc = RegOpenKeyExW(k->h, w, 0, sam, &h);
|
|
324
|
+
xfree(w);
|
|
325
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegOpenKeyExW", rc);
|
|
326
|
+
child->h = h;
|
|
327
|
+
child->view_sam = k->view_sam;
|
|
328
|
+
child->closed = 0;
|
|
329
|
+
return obj;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Key#_create_child(subpath, sam) — handle-relative create, view inherited. */
|
|
333
|
+
static VALUE
|
|
334
|
+
key_create_child(VALUE self, VALUE vsub, VALUE vsam)
|
|
335
|
+
{
|
|
336
|
+
key_t *k = key_live(self);
|
|
337
|
+
VALUE obj = key_alloc(cKey);
|
|
338
|
+
key_t *child = key_get(obj);
|
|
339
|
+
DWORD sam = (DWORD)NUM2ULONG(vsam) | k->view_sam;
|
|
340
|
+
HKEY h = NULL;
|
|
341
|
+
DWORD disp = 0;
|
|
342
|
+
LONG rc;
|
|
343
|
+
WCHAR *w;
|
|
344
|
+
|
|
345
|
+
w = to_wide(vsub);
|
|
346
|
+
rc = RegCreateKeyExW(k->h, w, 0, NULL, REG_OPTION_NON_VOLATILE, sam, NULL, &h, &disp);
|
|
347
|
+
xfree(w);
|
|
348
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegCreateKeyExW", rc);
|
|
349
|
+
child->h = h;
|
|
350
|
+
child->view_sam = k->view_sam;
|
|
351
|
+
child->created = (disp == REG_CREATED_NEW_KEY) ? 1 : 0;
|
|
352
|
+
child->closed = 0;
|
|
353
|
+
return obj;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* Key#_read_raw(name_or_nil) -> [type_int, bytes (BINARY)].
|
|
357
|
+
* RegQueryValueExW probe + fill, looped on ERROR_MORE_DATA (TOCTOU growth,
|
|
358
|
+
* spec E12) with cb re-initialized each pass. The destination buffer IS a
|
|
359
|
+
* Ruby string; pointers are taken after the last Ruby allocation. The name is
|
|
360
|
+
* held in a Ruby-owned UTF-16 string so nothing leaks across a raise. */
|
|
361
|
+
static VALUE
|
|
362
|
+
key_read_raw(VALUE self, VALUE name)
|
|
363
|
+
{
|
|
364
|
+
key_t *k = key_live(self);
|
|
365
|
+
VALUE wname = Qnil;
|
|
366
|
+
VALUE buf;
|
|
367
|
+
const WCHAR *pw;
|
|
368
|
+
DWORD type = 0, cb = 0;
|
|
369
|
+
LONG rc;
|
|
370
|
+
|
|
371
|
+
if (!NIL_P(name)) wname = to_wide_val(name);
|
|
372
|
+
|
|
373
|
+
/* size probe */
|
|
374
|
+
pw = NIL_P(wname) ? NULL : (const WCHAR *)RSTRING_PTR(wname);
|
|
375
|
+
rc = RegQueryValueExW(k->h, pw, NULL, &type, NULL, &cb);
|
|
376
|
+
if (rc != ERROR_SUCCESS && rc != ERROR_MORE_DATA)
|
|
377
|
+
raise_lstatus("RegQueryValueExW", rc);
|
|
378
|
+
|
|
379
|
+
buf = rb_str_new(NULL, (long)cb);
|
|
380
|
+
for (;;) {
|
|
381
|
+
DWORD cb2 = cb;
|
|
382
|
+
/* re-take both pointers after any Ruby allocation (GC compaction) */
|
|
383
|
+
pw = NIL_P(wname) ? NULL : (const WCHAR *)RSTRING_PTR(wname);
|
|
384
|
+
rc = RegQueryValueExW(k->h, pw, NULL, &type, (LPBYTE)RSTRING_PTR(buf), &cb2);
|
|
385
|
+
if (rc == ERROR_SUCCESS) { cb = cb2; break; }
|
|
386
|
+
if (rc == ERROR_MORE_DATA) { cb = cb2; rb_str_resize(buf, (long)cb); continue; }
|
|
387
|
+
raise_lstatus("RegQueryValueExW", rc);
|
|
388
|
+
}
|
|
389
|
+
rb_str_set_len(buf, (long)cb);
|
|
390
|
+
rb_enc_associate(buf, rb_ascii8bit_encoding());
|
|
391
|
+
RB_GC_GUARD(wname);
|
|
392
|
+
return rb_assoc_new(ULONG2NUM(type), buf);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* Key#_value_p(name_or_nil) -> true/false. Pure size probe; error 2 = false. */
|
|
396
|
+
static VALUE
|
|
397
|
+
key_value_p(VALUE self, VALUE name)
|
|
398
|
+
{
|
|
399
|
+
key_t *k = key_live(self);
|
|
400
|
+
WCHAR *wname = NIL_P(name) ? NULL : to_wide(name);
|
|
401
|
+
DWORD type = 0, cb = 0;
|
|
402
|
+
LONG rc = RegQueryValueExW(k->h, wname, NULL, &type, NULL, &cb);
|
|
403
|
+
if (wname) xfree(wname);
|
|
404
|
+
if (rc == ERROR_SUCCESS || rc == ERROR_MORE_DATA) return Qtrue;
|
|
405
|
+
if (rc == ERROR_FILE_NOT_FOUND) return Qfalse;
|
|
406
|
+
raise_lstatus("RegQueryValueExW", rc);
|
|
407
|
+
UNREACHABLE_RETURN(Qnil);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* Key#_write_raw(name_or_nil, type_tag, bytes) — RegSetValueExW. cb includes
|
|
411
|
+
* any terminators: the Ruby layer's serializers build the exact byte image. */
|
|
412
|
+
static VALUE
|
|
413
|
+
key_write_raw(VALUE self, VALUE name, VALUE vtype, VALUE bytes)
|
|
414
|
+
{
|
|
415
|
+
key_t *k = key_live(self);
|
|
416
|
+
DWORD cb = dword_len(bytes); /* coercion + 4 GiB guard up front */
|
|
417
|
+
DWORD type = (DWORD)NUM2ULONG(vtype);
|
|
418
|
+
WCHAR *wname = NIL_P(name) ? NULL : to_wide(name);
|
|
419
|
+
LONG rc;
|
|
420
|
+
|
|
421
|
+
/* data pointer taken inline, after the last possible Ruby/GC allocation */
|
|
422
|
+
rc = RegSetValueExW(k->h, wname, 0, type,
|
|
423
|
+
cb ? (const BYTE *)RSTRING_PTR(bytes) : NULL, cb);
|
|
424
|
+
if (wname) xfree(wname);
|
|
425
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegSetValueExW", rc);
|
|
426
|
+
return Qnil;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* Key#_delete_value(name_or_nil) — RegDeleteValueW. */
|
|
430
|
+
static VALUE
|
|
431
|
+
key_delete_value(VALUE self, VALUE name)
|
|
432
|
+
{
|
|
433
|
+
key_t *k = key_live(self);
|
|
434
|
+
WCHAR *wname = NIL_P(name) ? NULL : to_wide(name);
|
|
435
|
+
LONG rc = RegDeleteValueW(k->h, wname);
|
|
436
|
+
if (wname) xfree(wname);
|
|
437
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegDeleteValueW", rc);
|
|
438
|
+
return Qnil;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Key#_delete_key(name) — RegDeleteKeyExW, the view-aware form (plain
|
|
442
|
+
* RegDeleteKeyW cannot carry the WOW64 view). Non-recursive only; the
|
|
443
|
+
* recursive walk lives in Ruby over these raise-safe primitives. */
|
|
444
|
+
static VALUE
|
|
445
|
+
key_delete_key(VALUE self, VALUE name)
|
|
446
|
+
{
|
|
447
|
+
key_t *k = key_live(self);
|
|
448
|
+
WCHAR *wname = to_wide(name);
|
|
449
|
+
LONG rc = RegDeleteKeyExW(k->h, wname, k->view_sam, 0);
|
|
450
|
+
xfree(wname);
|
|
451
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegDeleteKeyExW", rc);
|
|
452
|
+
return Qnil;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Key#_enum_value(index) -> [name, type_int, bytes] or nil (end).
|
|
456
|
+
* Name + type + data in ONE RegEnumValueW call — no name-then-query TOCTOU.
|
|
457
|
+
* Name buffer is 16,384 WCHARs (value names go to 16,383 chars — spec E13)
|
|
458
|
+
* via ALLOCV, which is GC-owned and therefore safe across raises. */
|
|
459
|
+
#define VALUE_NAME_CCH 16384
|
|
460
|
+
static VALUE
|
|
461
|
+
key_enum_value(VALUE self, VALUE vidx)
|
|
462
|
+
{
|
|
463
|
+
key_t *k = key_live(self);
|
|
464
|
+
DWORD idx = (DWORD)NUM2ULONG(vidx);
|
|
465
|
+
VALUE vnb;
|
|
466
|
+
WCHAR *namebuf = ALLOCV_N(WCHAR, vnb, VALUE_NAME_CCH);
|
|
467
|
+
VALUE data = rb_str_new(NULL, 256);
|
|
468
|
+
VALUE name, out;
|
|
469
|
+
DWORD cch, cb, type = 0;
|
|
470
|
+
LONG rc;
|
|
471
|
+
|
|
472
|
+
for (;;) {
|
|
473
|
+
cch = VALUE_NAME_CCH; /* chars incl. NUL in, excl. NUL out */
|
|
474
|
+
cb = (DWORD)RSTRING_LEN(data); /* re-initialized each pass (E12) */
|
|
475
|
+
rc = RegEnumValueW(k->h, idx, namebuf, &cch, NULL, &type,
|
|
476
|
+
(LPBYTE)RSTRING_PTR(data), &cb);
|
|
477
|
+
if (rc == ERROR_SUCCESS) break;
|
|
478
|
+
if (rc == ERROR_NO_MORE_ITEMS) { ALLOCV_END(vnb); return Qnil; }
|
|
479
|
+
if (rc == ERROR_MORE_DATA) { rb_str_resize(data, (long)cb); continue; }
|
|
480
|
+
ALLOCV_END(vnb);
|
|
481
|
+
raise_lstatus("RegEnumValueW", rc);
|
|
482
|
+
}
|
|
483
|
+
name = wide_to_utf8(namebuf, (int)cch); /* lenient: U+FFFD, never aborts */
|
|
484
|
+
ALLOCV_END(vnb);
|
|
485
|
+
rb_str_set_len(data, (long)cb);
|
|
486
|
+
rb_enc_associate(data, rb_ascii8bit_encoding());
|
|
487
|
+
out = rb_ary_new_from_args(3, name, ULONG2NUM(type), data);
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/* Key#_enum_key(index) -> name or nil (end). Key names cap at 255 chars. */
|
|
492
|
+
static VALUE
|
|
493
|
+
key_enum_key(VALUE self, VALUE vidx)
|
|
494
|
+
{
|
|
495
|
+
key_t *k = key_live(self);
|
|
496
|
+
DWORD idx = (DWORD)NUM2ULONG(vidx);
|
|
497
|
+
WCHAR namebuf[512];
|
|
498
|
+
DWORD cch = 512;
|
|
499
|
+
LONG rc = RegEnumKeyExW(k->h, idx, namebuf, &cch, NULL, NULL, NULL, NULL);
|
|
500
|
+
if (rc == ERROR_NO_MORE_ITEMS) return Qnil;
|
|
501
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegEnumKeyExW", rc);
|
|
502
|
+
return wide_to_utf8(namebuf, (int)cch);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* Key#_query_info -> [subkeys, max_subkey_chars, values, max_name_chars,
|
|
506
|
+
* max_data_bytes, last_write_filetime_int] */
|
|
507
|
+
static VALUE
|
|
508
|
+
key_query_info(VALUE self)
|
|
509
|
+
{
|
|
510
|
+
key_t *k = key_live(self);
|
|
511
|
+
DWORD subkeys = 0, maxsub = 0, values = 0, maxname = 0, maxdata = 0;
|
|
512
|
+
FILETIME ft;
|
|
513
|
+
ULONGLONG t;
|
|
514
|
+
LONG rc;
|
|
515
|
+
|
|
516
|
+
ft.dwLowDateTime = 0;
|
|
517
|
+
ft.dwHighDateTime = 0;
|
|
518
|
+
rc = RegQueryInfoKeyW(k->h, NULL, NULL, NULL, &subkeys, &maxsub, NULL,
|
|
519
|
+
&values, &maxname, &maxdata, NULL, &ft);
|
|
520
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegQueryInfoKeyW", rc);
|
|
521
|
+
t = ((ULONGLONG)ft.dwHighDateTime << 32) | (ULONGLONG)ft.dwLowDateTime;
|
|
522
|
+
return rb_ary_new_from_args(6, ULONG2NUM(subkeys), ULONG2NUM(maxsub),
|
|
523
|
+
ULONG2NUM(values), ULONG2NUM(maxname),
|
|
524
|
+
ULONG2NUM(maxdata), ULL2NUM(t));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
static VALUE
|
|
528
|
+
key_close(VALUE self)
|
|
529
|
+
{
|
|
530
|
+
key_t *k = key_get(self);
|
|
531
|
+
if (!k->closed) {
|
|
532
|
+
/* bare roots: flag flip only — never RegCloseKey a pseudo-handle */
|
|
533
|
+
if (k->h && !k->predefined) RegCloseKey(k->h);
|
|
534
|
+
k->h = NULL;
|
|
535
|
+
k->closed = 1;
|
|
536
|
+
}
|
|
537
|
+
return Qnil;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
static VALUE key_closed_p(VALUE self) { return key_get(self)->closed ? Qtrue : Qfalse; }
|
|
541
|
+
static VALUE key_created_p(VALUE self) { return key_get(self)->created ? Qtrue : Qfalse; }
|
|
542
|
+
|
|
543
|
+
/* =====================================================================
|
|
544
|
+
* Winreg::Watch — RegNotifyChangeKeyValue
|
|
545
|
+
* ===================================================================== */
|
|
546
|
+
|
|
547
|
+
typedef struct {
|
|
548
|
+
HKEY hkey; /* private KEY_NOTIFY|view handle, owned */
|
|
549
|
+
HANDLE notify_event; /* auto-reset, unnamed (research §6.6: verified) */
|
|
550
|
+
HANDLE stop_event; /* manual-reset, unnamed: ubf + #close wakeup */
|
|
551
|
+
DWORD filter; /* includes REG_NOTIFY_THREAD_AGNOSTIC, frozen at construction */
|
|
552
|
+
BOOL subtree;
|
|
553
|
+
int predefined; /* hkey is a predefined pseudo-handle (bare-root watch) */
|
|
554
|
+
int waiting; /* a _wait is in flight (single-waiter guard) */
|
|
555
|
+
int finished; /* :deleted delivered; handles already torn down */
|
|
556
|
+
int closed;
|
|
557
|
+
} watch_t;
|
|
558
|
+
|
|
559
|
+
static void
|
|
560
|
+
watch_teardown(watch_t *w)
|
|
561
|
+
{
|
|
562
|
+
if (w->hkey) {
|
|
563
|
+
if (!w->predefined) RegCloseKey(w->hkey);
|
|
564
|
+
w->hkey = NULL;
|
|
565
|
+
}
|
|
566
|
+
if (w->notify_event) { CloseHandle(w->notify_event); w->notify_event = NULL; }
|
|
567
|
+
if (w->stop_event) { CloseHandle(w->stop_event); w->stop_event = NULL; }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
static void
|
|
571
|
+
watch_free(void *p)
|
|
572
|
+
{
|
|
573
|
+
watch_t *w = (watch_t *)p;
|
|
574
|
+
/* Safety net. A waiter keeps the object reachable (receiver on its
|
|
575
|
+
* stack), so this can never run concurrently with a wait. */
|
|
576
|
+
watch_teardown(w);
|
|
577
|
+
xfree(w);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
static const rb_data_type_t watch_type = {
|
|
581
|
+
"Winreg::Watch", { 0, watch_free, 0, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
static VALUE
|
|
585
|
+
watch_alloc(VALUE klass)
|
|
586
|
+
{
|
|
587
|
+
watch_t *w;
|
|
588
|
+
VALUE obj = TypedData_Make_Struct(klass, watch_t, &watch_type, w);
|
|
589
|
+
w->hkey = NULL;
|
|
590
|
+
w->notify_event = NULL;
|
|
591
|
+
w->stop_event = NULL;
|
|
592
|
+
w->filter = 0;
|
|
593
|
+
w->subtree = FALSE;
|
|
594
|
+
w->predefined = 0;
|
|
595
|
+
w->waiting = 0;
|
|
596
|
+
w->finished = 0;
|
|
597
|
+
w->closed = 1; /* closed until _new succeeds */
|
|
598
|
+
return obj;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
static watch_t *
|
|
602
|
+
watch_get(VALUE self)
|
|
603
|
+
{
|
|
604
|
+
watch_t *w;
|
|
605
|
+
TypedData_Get_Struct(self, watch_t, &watch_type, w);
|
|
606
|
+
return w;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/* REG_NOTIFY_THREAD_AGNOSTIC (Win8+, gem targets Win10+): unconditional —
|
|
610
|
+
* registrations must survive ephemeral Ruby threads (run_blocking workers). */
|
|
611
|
+
#ifndef REG_NOTIFY_THREAD_AGNOSTIC
|
|
612
|
+
#define REG_NOTIFY_THREAD_AGNOSTIC 0x10000000L
|
|
613
|
+
#endif
|
|
614
|
+
|
|
615
|
+
/* Watch._new(root, subpath_or_nil, view_sam, subtree, filter) — private
|
|
616
|
+
* singleton. Opens its OWN handle (KEY_NOTIFY | view) by path, creates the
|
|
617
|
+
* two private events, and arms the first registration so changes between
|
|
618
|
+
* construction and the first #wait are caught. Any failure: capture code,
|
|
619
|
+
* close what was opened, raise. */
|
|
620
|
+
static VALUE
|
|
621
|
+
watch_s_new(VALUE klass, VALUE vroot, VALUE vsub, VALUE vview, VALUE vsubtree, VALUE vfilter)
|
|
622
|
+
{
|
|
623
|
+
VALUE obj = watch_alloc(cWatch);
|
|
624
|
+
watch_t *w = watch_get(obj);
|
|
625
|
+
HKEY root = root_hkey(vroot);
|
|
626
|
+
DWORD view = (DWORD)NUM2ULONG(vview);
|
|
627
|
+
DWORD filter = (DWORD)NUM2ULONG(vfilter) | REG_NOTIFY_THREAD_AGNOSTIC;
|
|
628
|
+
BOOL subtree = RTEST(vsubtree) ? TRUE : FALSE;
|
|
629
|
+
HKEY h = NULL;
|
|
630
|
+
LONG rc;
|
|
631
|
+
DWORD gle;
|
|
632
|
+
|
|
633
|
+
if (NIL_P(vsub)) {
|
|
634
|
+
/* bare root: the predefined pseudo-handle itself carries KEY_NOTIFY */
|
|
635
|
+
h = root;
|
|
636
|
+
w->predefined = 1;
|
|
637
|
+
} else {
|
|
638
|
+
WCHAR *ws = to_wide(vsub);
|
|
639
|
+
rc = RegOpenKeyExW(root, ws, 0, KEY_NOTIFY | view, &h);
|
|
640
|
+
xfree(ws);
|
|
641
|
+
if (rc != ERROR_SUCCESS) raise_lstatus("RegOpenKeyExW", rc);
|
|
642
|
+
}
|
|
643
|
+
w->hkey = h; /* owned from here: free hook / teardown closes it */
|
|
644
|
+
|
|
645
|
+
w->notify_event = CreateEventW(NULL, FALSE, FALSE, NULL); /* auto-reset */
|
|
646
|
+
if (!w->notify_event) {
|
|
647
|
+
gle = GetLastError();
|
|
648
|
+
watch_teardown(w);
|
|
649
|
+
raise_gle("CreateEvent", gle);
|
|
650
|
+
}
|
|
651
|
+
w->stop_event = CreateEventW(NULL, TRUE, FALSE, NULL); /* manual-reset */
|
|
652
|
+
if (!w->stop_event) {
|
|
653
|
+
gle = GetLastError();
|
|
654
|
+
watch_teardown(w);
|
|
655
|
+
raise_gle("CreateEvent", gle);
|
|
656
|
+
}
|
|
657
|
+
rc = RegNotifyChangeKeyValue(w->hkey, subtree, filter, w->notify_event, TRUE);
|
|
658
|
+
if (rc != ERROR_SUCCESS) {
|
|
659
|
+
watch_teardown(w);
|
|
660
|
+
raise_lstatus("RegNotifyChangeKeyValue", rc);
|
|
661
|
+
}
|
|
662
|
+
w->filter = filter;
|
|
663
|
+
w->subtree = subtree;
|
|
664
|
+
w->closed = 0;
|
|
665
|
+
return obj;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/* --- the GVL-released wait ------------------------------------------------ */
|
|
669
|
+
|
|
670
|
+
typedef struct {
|
|
671
|
+
HANDLE handles[2]; /* {notify, stop} */
|
|
672
|
+
DWORD ms;
|
|
673
|
+
DWORD result;
|
|
674
|
+
DWORD gle;
|
|
675
|
+
} wfmo_arg_t;
|
|
676
|
+
|
|
677
|
+
static void *
|
|
678
|
+
wfmo_fn(void *p)
|
|
679
|
+
{
|
|
680
|
+
wfmo_arg_t *a = (wfmo_arg_t *)p;
|
|
681
|
+
a->result = WaitForMultipleObjects(2, a->handles, FALSE, a->ms);
|
|
682
|
+
if (a->result == WAIT_FAILED) a->gle = GetLastError(); /* capture before GVL */
|
|
683
|
+
return NULL;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
static void
|
|
687
|
+
wfmo_ubf(void *p)
|
|
688
|
+
{
|
|
689
|
+
wfmo_arg_t *a = (wfmo_arg_t *)p;
|
|
690
|
+
SetEvent(a->handles[1]); /* stop event: cancel-capable without polling */
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* Watch#_wait(ms) -> :changed / :deleted / nil(timeout). Implements spec §3.6
|
|
694
|
+
* exactly — see the banner invariants. ms < 0 = INFINITE. */
|
|
695
|
+
static VALUE
|
|
696
|
+
watch_wait(VALUE self, VALUE vms)
|
|
697
|
+
{
|
|
698
|
+
watch_t *w = watch_get(self);
|
|
699
|
+
long ms_in = NUM2LONG(vms);
|
|
700
|
+
int infinite = (ms_in < 0);
|
|
701
|
+
ULONGLONG deadline = 0;
|
|
702
|
+
|
|
703
|
+
/* step 2: entry checks (GVL held) */
|
|
704
|
+
if (w->closed) rb_raise(eClosed, "winreg: watch is closed");
|
|
705
|
+
if (w->waiting) rb_raise(eError, "winreg: Watch#wait is single-waiter");
|
|
706
|
+
w->waiting = 1;
|
|
707
|
+
/* consume a stale ubf signal from a previous interrupted wait — safe: a
|
|
708
|
+
* concurrent close cannot run while we hold the GVL, and once released,
|
|
709
|
+
* close SETS the event after setting `closed`, which step 4a re-checks. */
|
|
710
|
+
ResetEvent(w->stop_event);
|
|
711
|
+
/* the absolute deadline is computed ONCE; re-waits measure remaining time
|
|
712
|
+
* against it, so spurious wakes cannot silently extend the timeout */
|
|
713
|
+
if (!infinite) deadline = GetTickCount64() + (ULONGLONG)ms_in;
|
|
714
|
+
|
|
715
|
+
for (;;) {
|
|
716
|
+
wfmo_arg_t a;
|
|
717
|
+
if (infinite) {
|
|
718
|
+
a.ms = INFINITE;
|
|
719
|
+
} else {
|
|
720
|
+
ULONGLONG now = GetTickCount64();
|
|
721
|
+
a.ms = (now >= deadline) ? 0 : (DWORD)(deadline - now);
|
|
722
|
+
}
|
|
723
|
+
a.handles[0] = w->notify_event;
|
|
724
|
+
a.handles[1] = w->stop_event;
|
|
725
|
+
/* If a pending interrupt makes rb_nogvl skip the call entirely
|
|
726
|
+
* (RB_NOGVL_INTR_FAIL fails fast), this sentinel routes us to the
|
|
727
|
+
* stop path (4c), whose rb_thread_check_ints delivers it cleanly. */
|
|
728
|
+
a.result = WAIT_OBJECT_0 + 1;
|
|
729
|
+
a.gle = 0;
|
|
730
|
+
|
|
731
|
+
/* step 3: the only GVL-released region in the gem. RB_NOGVL_INTR_FAIL
|
|
732
|
+
* is load-bearing: nothing raises on return; pending interrupts fire
|
|
733
|
+
* only at the explicit rb_thread_check_ints() points below, where the
|
|
734
|
+
* watch state is consistent (rearm-before-interrupt-delivery). */
|
|
735
|
+
rb_nogvl(wfmo_fn, &a, wfmo_ubf, &a, RB_NOGVL_INTR_FAIL);
|
|
736
|
+
|
|
737
|
+
/* step 4a: unconditional closed check, FIRST — before branching on
|
|
738
|
+
* the wait result. WFMO reports the lowest-index signaled handle, so
|
|
739
|
+
* any wake can mask a cross-thread #close that ran while off-GVL. */
|
|
740
|
+
if (w->closed) {
|
|
741
|
+
watch_teardown(w); /* the deferred teardown #close left to us */
|
|
742
|
+
w->waiting = 0;
|
|
743
|
+
rb_raise(eClosed, "winreg: watch is closed");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (a.result == WAIT_OBJECT_0) {
|
|
747
|
+
/* 4b: notify — rearm BEFORE delivering (same frozen params: the
|
|
748
|
+
* same-handle param trap is structurally impossible). */
|
|
749
|
+
LONG rc = RegNotifyChangeKeyValue(w->hkey, w->subtree, w->filter,
|
|
750
|
+
w->notify_event, TRUE);
|
|
751
|
+
if (rc == ERROR_SUCCESS) {
|
|
752
|
+
w->waiting = 0;
|
|
753
|
+
/* a pending kill/timeout raises HERE, with the registration
|
|
754
|
+
* already rearmed and the same Watch reusable — only the
|
|
755
|
+
* coalesced :changed is dropped (stateless delivery). */
|
|
756
|
+
rb_thread_check_ints();
|
|
757
|
+
return ID2SYM(rb_intern("changed"));
|
|
758
|
+
}
|
|
759
|
+
if (rc == ERROR_KEY_DELETED) {
|
|
760
|
+
w->finished = 1;
|
|
761
|
+
w->closed = 1;
|
|
762
|
+
watch_teardown(w);
|
|
763
|
+
w->waiting = 0;
|
|
764
|
+
return ID2SYM(rb_intern("deleted"));
|
|
765
|
+
}
|
|
766
|
+
w->closed = 1;
|
|
767
|
+
watch_teardown(w);
|
|
768
|
+
w->waiting = 0;
|
|
769
|
+
raise_lstatus("RegNotifyChangeKeyValue", rc);
|
|
770
|
+
}
|
|
771
|
+
else if (a.result == WAIT_OBJECT_0 + 1) {
|
|
772
|
+
/* 4c: stop event, not closed (per 4a): the ubf fired for a
|
|
773
|
+
* pending interrupt (or rb_nogvl failed fast on one). */
|
|
774
|
+
w->waiting = 0;
|
|
775
|
+
rb_thread_check_ints(); /* delivers Thread#kill / Timeout; the
|
|
776
|
+
* registration stays armed, nothing leaks */
|
|
777
|
+
/* spurious return: re-validate the step-2 entry conditions */
|
|
778
|
+
if (w->closed) rb_raise(eClosed, "winreg: watch is closed");
|
|
779
|
+
if (w->waiting) rb_raise(eError, "winreg: Watch#wait is single-waiter");
|
|
780
|
+
if (!infinite && GetTickCount64() >= deadline) return Qnil;
|
|
781
|
+
w->waiting = 1;
|
|
782
|
+
ResetEvent(w->stop_event);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
else if (a.result == WAIT_TIMEOUT) {
|
|
786
|
+
/* 4d: registration remains armed */
|
|
787
|
+
w->waiting = 0;
|
|
788
|
+
return Qnil;
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
/* 4e: WAIT_FAILED (code captured inside the no-GVL fn) */
|
|
792
|
+
DWORD g = a.gle;
|
|
793
|
+
w->waiting = 0;
|
|
794
|
+
raise_gle("WaitForMultipleObjects", g);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/* Watch#close — step 5. Idempotent; unblocks a concurrent #wait (which then
|
|
800
|
+
* raises Closed in the waiting thread). Handles MUST NOT be closed while
|
|
801
|
+
* another thread waits on them: if `waiting`, the teardown is deferred to the
|
|
802
|
+
* waiter's next wake (step 4a, which every wake path executes). `waiting` is
|
|
803
|
+
* only ever cleared with the GVL held, so waiting == 0 guarantees no thread
|
|
804
|
+
* is blocked on the handles. */
|
|
805
|
+
static VALUE
|
|
806
|
+
watch_close(VALUE self)
|
|
807
|
+
{
|
|
808
|
+
watch_t *w = watch_get(self);
|
|
809
|
+
if (w->closed) return Qnil;
|
|
810
|
+
w->closed = 1;
|
|
811
|
+
if (w->waiting) {
|
|
812
|
+
SetEvent(w->stop_event);
|
|
813
|
+
return Qnil;
|
|
814
|
+
}
|
|
815
|
+
watch_teardown(w);
|
|
816
|
+
return Qnil;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
static VALUE watch_closed_p(VALUE self) { return watch_get(self)->closed ? Qtrue : Qfalse; }
|
|
820
|
+
|
|
821
|
+
/* =====================================================================
|
|
822
|
+
* Winreg._expand — ExpandEnvironmentStringsW
|
|
823
|
+
* ===================================================================== */
|
|
824
|
+
|
|
825
|
+
static VALUE
|
|
826
|
+
wr_expand(VALUE mod, VALUE str)
|
|
827
|
+
{
|
|
828
|
+
VALUE wsrc = to_wide_val(str); /* Ruby-owned: raise-safe across allocs */
|
|
829
|
+
VALUE wout, out;
|
|
830
|
+
DWORD n, r;
|
|
831
|
+
|
|
832
|
+
/* size probe: returned size is in WCHARs and includes the NUL */
|
|
833
|
+
n = ExpandEnvironmentStringsW((const WCHAR *)RSTRING_PTR(wsrc), NULL, 0);
|
|
834
|
+
if (n == 0) raise_gle("ExpandEnvironmentStrings", GetLastError());
|
|
835
|
+
wout = rb_str_new(NULL, (long)n * (long)sizeof(WCHAR));
|
|
836
|
+
for (;;) {
|
|
837
|
+
/* re-take both pointers after any Ruby allocation */
|
|
838
|
+
r = ExpandEnvironmentStringsW((const WCHAR *)RSTRING_PTR(wsrc),
|
|
839
|
+
(WCHAR *)RSTRING_PTR(wout), n);
|
|
840
|
+
if (r == 0) raise_gle("ExpandEnvironmentStrings", GetLastError());
|
|
841
|
+
if (r <= n) { n = r; break; }
|
|
842
|
+
n = r; /* grew between calls (env mutated): resize and retry */
|
|
843
|
+
rb_str_resize(wout, (long)n * (long)sizeof(WCHAR));
|
|
844
|
+
}
|
|
845
|
+
/* n includes the terminating NUL; exclude it from the result */
|
|
846
|
+
out = wide_to_utf8((const WCHAR *)RSTRING_PTR(wout), (int)(n - 1));
|
|
847
|
+
RB_GC_GUARD(wsrc);
|
|
848
|
+
RB_GC_GUARD(wout);
|
|
849
|
+
return out;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/* ----------------------------------------------------------------- Init --- */
|
|
853
|
+
|
|
854
|
+
void
|
|
855
|
+
Init_winreg(void)
|
|
856
|
+
{
|
|
857
|
+
mWinreg = rb_define_module("Winreg");
|
|
858
|
+
|
|
859
|
+
eError = rb_define_class_under(mWinreg, "Error", rb_eStandardError);
|
|
860
|
+
eOSError = rb_define_class_under(mWinreg, "OSError", eError);
|
|
861
|
+
eNotFound = rb_define_class_under(mWinreg, "NotFound", eOSError);
|
|
862
|
+
eAccessDenied = rb_define_class_under(mWinreg, "AccessDenied", eOSError);
|
|
863
|
+
eKeyDeleted = rb_define_class_under(mWinreg, "KeyDeleted", eOSError);
|
|
864
|
+
eTypeMismatch = rb_define_class_under(mWinreg, "TypeMismatch", eError);
|
|
865
|
+
eMalformedValue = rb_define_class_under(mWinreg, "MalformedValue", eError);
|
|
866
|
+
eClosed = rb_define_class_under(mWinreg, "Closed", eError);
|
|
867
|
+
|
|
868
|
+
/* ---- predefined root handle values (verified, winreg.h) ----
|
|
869
|
+
* Public-but-undocumented plumbing for the Ruby layer's path parser.
|
|
870
|
+
* HKEY_PERFORMANCE_DATA (0x80000004) is deliberately not defined. */
|
|
871
|
+
rb_define_const(mWinreg, "HKEY_CLASSES_ROOT", ULONG2NUM(0x80000000UL));
|
|
872
|
+
rb_define_const(mWinreg, "HKEY_CURRENT_USER", ULONG2NUM(0x80000001UL));
|
|
873
|
+
rb_define_const(mWinreg, "HKEY_LOCAL_MACHINE", ULONG2NUM(0x80000002UL));
|
|
874
|
+
rb_define_const(mWinreg, "HKEY_USERS", ULONG2NUM(0x80000003UL));
|
|
875
|
+
rb_define_const(mWinreg, "HKEY_CURRENT_CONFIG", ULONG2NUM(0x80000005UL));
|
|
876
|
+
|
|
877
|
+
/* Key: no public .new — instances come only from Winreg.open/create and
|
|
878
|
+
* Key#open/create. The allocator exists so a GC'd never-initialized Key
|
|
879
|
+
* is inert (closed sentinel). */
|
|
880
|
+
cKey = rb_define_class_under(mWinreg, "Key", rb_cObject);
|
|
881
|
+
rb_define_alloc_func(cKey, key_alloc);
|
|
882
|
+
rb_undef_method(rb_singleton_class(cKey), "new");
|
|
883
|
+
rb_define_singleton_method(cKey, "_open", key_s_open, 3);
|
|
884
|
+
rb_define_singleton_method(cKey, "_create", key_s_create, 3);
|
|
885
|
+
rb_define_private_method(cKey, "_open_child", key_open_child, 2);
|
|
886
|
+
rb_define_private_method(cKey, "_create_child", key_create_child, 2);
|
|
887
|
+
rb_define_private_method(cKey, "_read_raw", key_read_raw, 1);
|
|
888
|
+
rb_define_private_method(cKey, "_value_p", key_value_p, 1);
|
|
889
|
+
rb_define_private_method(cKey, "_write_raw", key_write_raw, 3);
|
|
890
|
+
rb_define_private_method(cKey, "_delete_value", key_delete_value, 1);
|
|
891
|
+
rb_define_private_method(cKey, "_delete_key", key_delete_key, 1);
|
|
892
|
+
rb_define_private_method(cKey, "_enum_value", key_enum_value, 1);
|
|
893
|
+
rb_define_private_method(cKey, "_enum_key", key_enum_key, 1);
|
|
894
|
+
rb_define_private_method(cKey, "_query_info", key_query_info, 0);
|
|
895
|
+
rb_define_method(cKey, "close", key_close, 0);
|
|
896
|
+
rb_define_method(cKey, "closed?", key_closed_p, 0);
|
|
897
|
+
rb_define_method(cKey, "created?", key_created_p, 0);
|
|
898
|
+
|
|
899
|
+
/* Watch: constructed only via Key#watch. */
|
|
900
|
+
cWatch = rb_define_class_under(mWinreg, "Watch", rb_cObject);
|
|
901
|
+
rb_define_alloc_func(cWatch, watch_alloc);
|
|
902
|
+
rb_undef_method(rb_singleton_class(cWatch), "new");
|
|
903
|
+
rb_define_singleton_method(cWatch, "_new", watch_s_new, 5);
|
|
904
|
+
rb_define_private_method(cWatch, "_wait", watch_wait, 1);
|
|
905
|
+
rb_define_method(cWatch, "close", watch_close, 0);
|
|
906
|
+
rb_define_method(cWatch, "closed?", watch_closed_p, 0);
|
|
907
|
+
|
|
908
|
+
rb_define_module_function(mWinreg, "_expand", wr_expand, 1);
|
|
909
|
+
}
|