winwatch 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 +35 -0
- data/LICENSE.txt +21 -0
- data/README.md +269 -0
- data/ext/winwatch/extconf.rb +19 -0
- data/ext/winwatch/winwatch.c +883 -0
- data/lib/winwatch/version.rb +5 -0
- data/lib/winwatch.rb +688 -0
- metadata +125 -0
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* winwatch — a ReadDirectoryChangesW directory watcher for Ruby.
|
|
3
|
+
*
|
|
4
|
+
* PURE C (no C++), so rb_raise/longjmp is the normal, safe error mechanism —
|
|
5
|
+
* the same discipline as winipc/winloop, and the opposite of the lithos /EHsc
|
|
6
|
+
* hazard. The directory handle, its OVERLAPPED, manual-reset event and the
|
|
7
|
+
* completion buffer live in a TypedData wrapper whose free hook tears them down,
|
|
8
|
+
* so a forgotten #close never leaks a HANDLE or a buffer. The one blocking call
|
|
9
|
+
* (WaitForSingleObject on the overlapped event) releases the GVL with a real
|
|
10
|
+
* per-op cancel unblock function (CancelIoEx on THIS op), so other Ruby threads
|
|
11
|
+
* run and Thread#kill / Ctrl-C / Timeout break an INFINITE wait.
|
|
12
|
+
*
|
|
13
|
+
* The watcher thread (the no-GVL wait) touches ONLY plain C state — never the
|
|
14
|
+
* Ruby C-API. Everything that allocates Ruby objects (the batch String, the
|
|
15
|
+
* parsed Event array, path joins) happens with the GVL held, after the wait has
|
|
16
|
+
* returned. The OVERLAPPED/buffer are only ever freed when no op is in flight
|
|
17
|
+
* (a reaped completion, or a cancel followed by a settling GetOverlappedResult).
|
|
18
|
+
*
|
|
19
|
+
* The kernel mirror buffer is sized at the FIRST ReadDirectoryChangesW and kept
|
|
20
|
+
* for the handle's lifetime; it accumulates changes between our calls, so the
|
|
21
|
+
* pull design is lossless across take() gaps (overflow becomes :rescan, never a
|
|
22
|
+
* silent drop). See plans/research/rdcw.md and plans/winwatch-spec.md.
|
|
23
|
+
*
|
|
24
|
+
* Links kernel32 only (linked by default): CreateFileW / ReadDirectoryChangesW /
|
|
25
|
+
* CancelIoEx / WaitForSingleObject / GetOverlappedResult / GetLongPathNameW.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
#include <ruby.h>
|
|
29
|
+
#include <ruby/thread.h>
|
|
30
|
+
#include <ruby/encoding.h>
|
|
31
|
+
|
|
32
|
+
#define WIN32_LEAN_AND_MEAN
|
|
33
|
+
#include <windows.h>
|
|
34
|
+
|
|
35
|
+
#include <stdlib.h>
|
|
36
|
+
#include <string.h>
|
|
37
|
+
|
|
38
|
+
/* ------------------------------------------------------------------ globals */
|
|
39
|
+
|
|
40
|
+
static VALUE mWinwatch;
|
|
41
|
+
static VALUE cWatcher;
|
|
42
|
+
static VALUE eError, eOSError, eNotFound, eAccessDenied, eUnsupported,
|
|
43
|
+
eNotADirectory, eClosed;
|
|
44
|
+
|
|
45
|
+
/* FILE_ACTION_* values (winnt.h) — re-stated for clarity (rdcw §3). */
|
|
46
|
+
#ifndef FILE_ACTION_ADDED
|
|
47
|
+
# define FILE_ACTION_ADDED 0x00000001
|
|
48
|
+
# define FILE_ACTION_REMOVED 0x00000002
|
|
49
|
+
# define FILE_ACTION_MODIFIED 0x00000003
|
|
50
|
+
# define FILE_ACTION_RENAMED_OLD_NAME 0x00000004
|
|
51
|
+
# define FILE_ACTION_RENAMED_NEW_NAME 0x00000005
|
|
52
|
+
#endif
|
|
53
|
+
|
|
54
|
+
/* ------------------------------------------------------------ small helpers */
|
|
55
|
+
|
|
56
|
+
/* Build + raise a winwatch error carrying @code (the Win32 error). The code
|
|
57
|
+
* must be captured by the caller immediately after the failing syscall, and any
|
|
58
|
+
* OS allocation released BEFORE this is called (so an OOM longjmp from
|
|
59
|
+
* rb_sprintf / rb_exc_new_str cannot leak it). Verbatim winipc raise_code. */
|
|
60
|
+
static void
|
|
61
|
+
raise_code(VALUE klass, const char *api, DWORD code)
|
|
62
|
+
{
|
|
63
|
+
VALUE exc, msg;
|
|
64
|
+
WCHAR *buf = NULL;
|
|
65
|
+
char detail[512];
|
|
66
|
+
DWORD n;
|
|
67
|
+
|
|
68
|
+
detail[0] = 0;
|
|
69
|
+
n = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
|
70
|
+
FORMAT_MESSAGE_IGNORE_INSERTS, NULL, code,
|
|
71
|
+
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&buf, 0, NULL);
|
|
72
|
+
if (n && buf) {
|
|
73
|
+
while (n && (buf[n - 1] == L'\r' || buf[n - 1] == L'\n' || buf[n - 1] == L'.'))
|
|
74
|
+
buf[--n] = 0;
|
|
75
|
+
WideCharToMultiByte(CP_UTF8, 0, buf, -1, detail, (int)sizeof(detail), NULL, NULL);
|
|
76
|
+
detail[sizeof(detail) - 1] = 0;
|
|
77
|
+
}
|
|
78
|
+
if (buf) LocalFree(buf);
|
|
79
|
+
|
|
80
|
+
if (detail[0])
|
|
81
|
+
msg = rb_sprintf("%s: %s (error %lu)", api, detail, (unsigned long)code);
|
|
82
|
+
else
|
|
83
|
+
msg = rb_sprintf("%s failed (error %lu)", api, (unsigned long)code);
|
|
84
|
+
exc = rb_exc_new_str(klass, msg);
|
|
85
|
+
rb_iv_set(exc, "@code", ULONG2NUM(code));
|
|
86
|
+
rb_exc_raise(exc);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Map an open-time / first-arm Win32 error to the right winwatch subclass and
|
|
90
|
+
* raise (§3.7 / §5.0 open-time mapping). */
|
|
91
|
+
static void
|
|
92
|
+
raise_gle(const char *api, DWORD code)
|
|
93
|
+
{
|
|
94
|
+
VALUE klass = eOSError;
|
|
95
|
+
switch (code) {
|
|
96
|
+
case ERROR_FILE_NOT_FOUND: /* 2 */
|
|
97
|
+
case ERROR_PATH_NOT_FOUND: /* 3 */ klass = eNotFound; break;
|
|
98
|
+
case ERROR_ACCESS_DENIED: /* 5 */ klass = eAccessDenied; break;
|
|
99
|
+
case ERROR_INVALID_FUNCTION:/* 1 */ klass = eUnsupported; break;
|
|
100
|
+
default: break;
|
|
101
|
+
}
|
|
102
|
+
raise_code(klass, api, code);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* =====================================================================
|
|
106
|
+
* Watcher — Winwatch::Watcher
|
|
107
|
+
* ===================================================================== */
|
|
108
|
+
|
|
109
|
+
typedef struct {
|
|
110
|
+
HANDLE dir; /* CreateFileW directory handle; INVALID_HANDLE_VALUE sentinel */
|
|
111
|
+
HANDLE ev; /* manual-reset event = ov.hEvent (standalone; NULL in :winloop) */
|
|
112
|
+
OVERLAPPED ov; /* the single in-flight RDCW (standalone mode) */
|
|
113
|
+
unsigned char *buf; /* malloc'd completion buffer, buf_len bytes (standalone mode) */
|
|
114
|
+
DWORD buf_len;
|
|
115
|
+
DWORD filter; /* dwNotifyFilter, precomputed in Ruby */
|
|
116
|
+
BOOL recursive;
|
|
117
|
+
int external; /* 1 = :winloop mode (no ev/buf/ov of our own) */
|
|
118
|
+
int armed; /* an RDCW we issued on &ov is in flight (standalone) */
|
|
119
|
+
int in_take; /* threads inside _take's wait path; touched only with the GVL */
|
|
120
|
+
/* held (inc before release, dec after reacquire), so close */
|
|
121
|
+
/* reads it race-free — deferred teardown (§3.5 rule 3) */
|
|
122
|
+
int closed;
|
|
123
|
+
/* interrupt-race stash (§5.12): a completed batch drained during an interrupt */
|
|
124
|
+
unsigned char *stash; DWORD stash_len; DWORD stash_code;
|
|
125
|
+
} watch_t;
|
|
126
|
+
|
|
127
|
+
/* Free buf/stash and zero the pointers (idempotent). */
|
|
128
|
+
static void
|
|
129
|
+
watch_free_bufs(watch_t *w)
|
|
130
|
+
{
|
|
131
|
+
if (w->buf) { free(w->buf); w->buf = NULL; }
|
|
132
|
+
if (w->stash) { free(w->stash); w->stash = NULL; }
|
|
133
|
+
w->stash_len = 0;
|
|
134
|
+
w->stash_code = 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* The teardown tail: settle a cancelled op, close handles, free buffers.
|
|
138
|
+
* Runs from close (when no taker is mid-wait), from the last leaver of _take,
|
|
139
|
+
* and from the GC free hook. Idempotent and tolerant of never-initialized
|
|
140
|
+
* sentinels. NO Ruby C-API here (it also runs in the free hook). */
|
|
141
|
+
static void
|
|
142
|
+
watch_teardown(watch_t *w)
|
|
143
|
+
{
|
|
144
|
+
/* Settle a cancelled/in-flight op before freeing its OVERLAPPED + buffer:
|
|
145
|
+
* completions for cancelled ops still arrive and freeing early is
|
|
146
|
+
* use-after-free (rdcw §13). Prompt by construction: a cancel was already
|
|
147
|
+
* issued (close/free path) or the op completed normally. Standalone only —
|
|
148
|
+
* in :winloop mode winloop owns the OVERLAPPED/buffer (wlx §3.4 rule 1). */
|
|
149
|
+
if (!w->external && w->armed && w->dir != INVALID_HANDLE_VALUE) {
|
|
150
|
+
DWORD n = 0;
|
|
151
|
+
GetOverlappedResult(w->dir, &w->ov, &n, TRUE);
|
|
152
|
+
w->armed = 0;
|
|
153
|
+
}
|
|
154
|
+
if (w->ev) { CloseHandle(w->ev); w->ev = NULL; }
|
|
155
|
+
if (w->dir != INVALID_HANDLE_VALUE) { CloseHandle(w->dir); w->dir = INVALID_HANDLE_VALUE; }
|
|
156
|
+
watch_free_bufs(w);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static void
|
|
160
|
+
watch_dfree(void *p)
|
|
161
|
+
{
|
|
162
|
+
watch_t *w = (watch_t *)p;
|
|
163
|
+
/* GC implies no thread is inside _take. Run the identical teardown the API
|
|
164
|
+
* uses (cancel already issued by close, or cancel now if still armed). */
|
|
165
|
+
if (!w->external && w->armed && w->dir != INVALID_HANDLE_VALUE)
|
|
166
|
+
CancelIoEx(w->dir, &w->ov);
|
|
167
|
+
watch_teardown(w);
|
|
168
|
+
xfree(w);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static size_t
|
|
172
|
+
watch_dsize(const void *p)
|
|
173
|
+
{
|
|
174
|
+
const watch_t *w = (const watch_t *)p;
|
|
175
|
+
return sizeof(watch_t) + (size_t)w->buf_len + (size_t)w->stash_len;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static const rb_data_type_t watch_type = {
|
|
179
|
+
"Winwatch::Watcher",
|
|
180
|
+
{ 0, watch_dfree, watch_dsize, },
|
|
181
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
static VALUE
|
|
185
|
+
watch_alloc(VALUE klass)
|
|
186
|
+
{
|
|
187
|
+
watch_t *w;
|
|
188
|
+
VALUE obj = TypedData_Make_Struct(klass, watch_t, &watch_type, w);
|
|
189
|
+
w->dir = INVALID_HANDLE_VALUE;
|
|
190
|
+
w->ev = NULL;
|
|
191
|
+
memset(&w->ov, 0, sizeof(w->ov));
|
|
192
|
+
w->buf = NULL;
|
|
193
|
+
w->buf_len = 0;
|
|
194
|
+
w->filter = 0;
|
|
195
|
+
w->recursive = FALSE;
|
|
196
|
+
w->external = 0;
|
|
197
|
+
w->armed = 0;
|
|
198
|
+
w->in_take = 0;
|
|
199
|
+
w->closed = 0;
|
|
200
|
+
w->stash = NULL;
|
|
201
|
+
w->stash_len = 0;
|
|
202
|
+
w->stash_code = 0;
|
|
203
|
+
return obj;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Raw getter (used by closed?/close — must work on closed objects). */
|
|
207
|
+
static watch_t *
|
|
208
|
+
watch_get(VALUE self)
|
|
209
|
+
{
|
|
210
|
+
watch_t *w;
|
|
211
|
+
TypedData_Get_Struct(self, watch_t, &watch_type, w);
|
|
212
|
+
return w;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Live getter — raises Winwatch::Closed when closed. */
|
|
216
|
+
static watch_t *
|
|
217
|
+
watch_live(VALUE self)
|
|
218
|
+
{
|
|
219
|
+
watch_t *w = watch_get(self);
|
|
220
|
+
if (w->closed || w->dir == INVALID_HANDLE_VALUE)
|
|
221
|
+
rb_raise(eClosed, "winwatch: watcher is closed");
|
|
222
|
+
return w;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* --- UTF-8 path -> extended-length wide path ready for CreateFileW ---------
|
|
226
|
+
*
|
|
227
|
+
* Flips '/'->'\\' and prepends \\?\ (or \\?\UNC\ for \\server\share), so long
|
|
228
|
+
* roots survive (rdcw §2). Returns a freshly malloc'd NUL-terminated WCHAR*;
|
|
229
|
+
* caller frees with free(). Raises on invalid UTF-8 (no OS state held yet). */
|
|
230
|
+
static WCHAR *
|
|
231
|
+
to_ext_wide(VALUE str)
|
|
232
|
+
{
|
|
233
|
+
int len, n, i;
|
|
234
|
+
WCHAR *raw, *out;
|
|
235
|
+
size_t prefix, total;
|
|
236
|
+
const WCHAR *pfx;
|
|
237
|
+
int is_unc;
|
|
238
|
+
|
|
239
|
+
StringValue(str);
|
|
240
|
+
len = (int)RSTRING_LEN(str);
|
|
241
|
+
if (len == 0) rb_raise(rb_eArgError, "winwatch: path must not be empty");
|
|
242
|
+
|
|
243
|
+
n = MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, NULL, 0);
|
|
244
|
+
if (n <= 0) rb_raise(eError, "winwatch: invalid UTF-8 in path");
|
|
245
|
+
raw = (WCHAR *)malloc(sizeof(WCHAR) * ((size_t)n + 1));
|
|
246
|
+
if (!raw) rb_raise(rb_eNoMemError, "winwatch: out of memory");
|
|
247
|
+
MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, raw, n);
|
|
248
|
+
raw[n] = 0;
|
|
249
|
+
|
|
250
|
+
/* normalize separators to backslash */
|
|
251
|
+
for (i = 0; i < n; i++) if (raw[i] == L'/') raw[i] = L'\\';
|
|
252
|
+
|
|
253
|
+
/* UNC if it starts with two backslashes (\\server\share). */
|
|
254
|
+
is_unc = (n >= 2 && raw[0] == L'\\' && raw[1] == L'\\');
|
|
255
|
+
if (is_unc) { pfx = L"\\\\?\\UNC\\"; prefix = 8; }
|
|
256
|
+
else { pfx = L"\\\\?\\"; prefix = 4; }
|
|
257
|
+
|
|
258
|
+
/* For UNC we skip the leading "\\" of the raw path (the prefix supplies it). */
|
|
259
|
+
{
|
|
260
|
+
int skip = is_unc ? 2 : 0;
|
|
261
|
+
size_t body = (size_t)(n - skip);
|
|
262
|
+
total = prefix + body + 1;
|
|
263
|
+
out = (WCHAR *)malloc(sizeof(WCHAR) * total);
|
|
264
|
+
if (!out) { free(raw); rb_raise(rb_eNoMemError, "winwatch: out of memory"); }
|
|
265
|
+
memcpy(out, pfx, prefix * sizeof(WCHAR));
|
|
266
|
+
memcpy(out + prefix, raw + skip, body * sizeof(WCHAR));
|
|
267
|
+
out[prefix + body] = 0;
|
|
268
|
+
}
|
|
269
|
+
free(raw);
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Issue one ReadDirectoryChangesW on &w->ov. GVL held; the call only queues.
|
|
274
|
+
* Returns 0 on success (op queued), else GetLastError, captured immediately. */
|
|
275
|
+
static DWORD
|
|
276
|
+
watch_arm(watch_t *w)
|
|
277
|
+
{
|
|
278
|
+
BOOL ok;
|
|
279
|
+
DWORD gle;
|
|
280
|
+
|
|
281
|
+
ResetEvent(w->ev);
|
|
282
|
+
memset(&w->ov, 0, sizeof(w->ov));
|
|
283
|
+
w->ov.hEvent = w->ev;
|
|
284
|
+
ok = ReadDirectoryChangesW(w->dir, w->buf, w->buf_len, w->recursive,
|
|
285
|
+
w->filter, NULL, &w->ov, NULL);
|
|
286
|
+
if (ok) { w->armed = 1; return 0; }
|
|
287
|
+
gle = GetLastError();
|
|
288
|
+
/* FALSE + ERROR_IO_PENDING still queues the op (defensive; not expected for
|
|
289
|
+
* RDCW, which returns TRUE on a successful queue). */
|
|
290
|
+
if (gle == ERROR_IO_PENDING) { w->armed = 1; return 0; }
|
|
291
|
+
w->armed = 0;
|
|
292
|
+
return gle;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
* Watcher._open(abs_path_utf8, filter_dword, recursive_bool, buffer_size, external_bool)
|
|
297
|
+
* -> Watcher.
|
|
298
|
+
* external_bool=true (:winloop mode) skips event/buffer creation and the first
|
|
299
|
+
* arm (the pump issues against winloop-owned memory). Captures GetLastError
|
|
300
|
+
* immediately, frees the wide path, then raises (the handle is stored into the
|
|
301
|
+
* struct before anything that can raise, so the free hook owns it).
|
|
302
|
+
*/
|
|
303
|
+
static VALUE
|
|
304
|
+
watch_open(VALUE klass, VALUE vpath, VALUE vfilter, VALUE vrec, VALUE vbuf, VALUE vext)
|
|
305
|
+
{
|
|
306
|
+
VALUE obj = watch_alloc(cWatcher);
|
|
307
|
+
watch_t *w = watch_get(obj);
|
|
308
|
+
WCHAR *wpath;
|
|
309
|
+
DWORD bufsz = NUM2ULONG(vbuf);
|
|
310
|
+
DWORD gle;
|
|
311
|
+
BY_HANDLE_FILE_INFORMATION fi;
|
|
312
|
+
|
|
313
|
+
/* Range-assert again in C (§5.18). Round up to a DWORD multiple of 4. */
|
|
314
|
+
if (bufsz < 4096 || bufsz > 65536)
|
|
315
|
+
rb_raise(rb_eArgError, "winwatch: buffer_size out of range");
|
|
316
|
+
bufsz = (bufsz + 3u) & ~3u;
|
|
317
|
+
|
|
318
|
+
w->filter = NUM2ULONG(vfilter);
|
|
319
|
+
w->recursive = RTEST(vrec) ? TRUE : FALSE;
|
|
320
|
+
w->external = RTEST(vext) ? 1 : 0;
|
|
321
|
+
w->buf_len = bufsz;
|
|
322
|
+
|
|
323
|
+
wpath = to_ext_wide(vpath); /* malloc'd; freed below before any raise */
|
|
324
|
+
|
|
325
|
+
/* Share-all (READ|WRITE|DELETE) is load-bearing (§5.6): omitting
|
|
326
|
+
* FILE_SHARE_DELETE blocks delete/rename of the root for other processes. */
|
|
327
|
+
w->dir = CreateFileW(wpath, FILE_LIST_DIRECTORY,
|
|
328
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
329
|
+
NULL, OPEN_EXISTING,
|
|
330
|
+
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
|
|
331
|
+
gle = GetLastError();
|
|
332
|
+
free(wpath);
|
|
333
|
+
if (w->dir == INVALID_HANDLE_VALUE)
|
|
334
|
+
raise_gle("CreateFile", gle); /* 2/3 NotFound, 5 AccessDenied, else OSError */
|
|
335
|
+
|
|
336
|
+
/* It must be a directory (misuse, not OS). */
|
|
337
|
+
if (!GetFileInformationByHandle(w->dir, &fi)) {
|
|
338
|
+
gle = GetLastError();
|
|
339
|
+
raise_gle("GetFileInformationByHandle", gle);
|
|
340
|
+
}
|
|
341
|
+
if (!(fi.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
|
342
|
+
rb_raise(eNotADirectory, "winwatch: not a directory");
|
|
343
|
+
|
|
344
|
+
if (!w->external) {
|
|
345
|
+
w->ev = CreateEventW(NULL, TRUE, FALSE, NULL); /* manual-reset */
|
|
346
|
+
if (!w->ev) raise_gle("CreateEvent", GetLastError());
|
|
347
|
+
w->buf = (unsigned char *)malloc(w->buf_len);
|
|
348
|
+
if (!w->buf) rb_raise(rb_eNoMemError, "winwatch: out of memory");
|
|
349
|
+
|
|
350
|
+
/* First arm in the constructor: coverage starts before watch() returns
|
|
351
|
+
* (rdcw §4/§14 re-arm discipline). A sync failure maps per §5.0. */
|
|
352
|
+
gle = watch_arm(w);
|
|
353
|
+
if (gle != 0)
|
|
354
|
+
raise_gle("ReadDirectoryChangesW", gle); /* 1 Unsupported, 2/3 NotFound, 5 AccessDenied */
|
|
355
|
+
}
|
|
356
|
+
return obj;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* Watcher#_arm -> Integer (0 = queued, else GetLastError). standalone only. */
|
|
360
|
+
static VALUE
|
|
361
|
+
watch_m_arm(VALUE self)
|
|
362
|
+
{
|
|
363
|
+
watch_t *w = watch_live(self);
|
|
364
|
+
return ULONG2NUM(watch_arm(w));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* --- GVL-released overlapped wait, cancelable via CancelIoEx (winipc) ------ */
|
|
368
|
+
|
|
369
|
+
typedef struct {
|
|
370
|
+
watch_t *w;
|
|
371
|
+
DWORD ms;
|
|
372
|
+
DWORD wait;
|
|
373
|
+
DWORD gle;
|
|
374
|
+
} take_wait_t;
|
|
375
|
+
|
|
376
|
+
static void *
|
|
377
|
+
take_wait_fn(void *p)
|
|
378
|
+
{
|
|
379
|
+
take_wait_t *t = (take_wait_t *)p;
|
|
380
|
+
t->wait = WaitForSingleObject(t->w->ev, t->ms);
|
|
381
|
+
if (t->wait == WAIT_FAILED) t->gle = GetLastError(); /* capture before GVL */
|
|
382
|
+
return NULL;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
static void
|
|
386
|
+
take_wait_ubf(void *p)
|
|
387
|
+
{
|
|
388
|
+
take_wait_t *t = (take_wait_t *)p;
|
|
389
|
+
/* Cancel THIS specific op (winipc per-op cancel) so Thread#kill / Ctrl-C /
|
|
390
|
+
* Timeout break an INFINITE wait. No-op when closed: close already
|
|
391
|
+
* cancelled and may have torn down; nothing left to cancel and the dir
|
|
392
|
+
* handle must not be touched (it could be closed/recycled). */
|
|
393
|
+
if (!t->w->closed && t->w->dir != INVALID_HANDLE_VALUE)
|
|
394
|
+
CancelIoEx(t->w->dir, &t->w->ov);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/*
|
|
398
|
+
* Watcher#_take(ms) -> nil | [data(String|nil), code(Int), rearm_code(Int)].
|
|
399
|
+
* standalone only.
|
|
400
|
+
* - nil : timeout (no completion within ms)
|
|
401
|
+
* - [data, code, rc] : a completion (code = Win32 code, rc = re-arm result)
|
|
402
|
+
* data is a binary String (the raw batch) when bytes>0, else nil.
|
|
403
|
+
* - returns the symbol :closed via a distinct shape: we return Qnil only on
|
|
404
|
+
* timeout, and raise nothing here for close — the Ruby layer detects close
|
|
405
|
+
* by re-checking closed? after _take returns nil... To keep it unambiguous
|
|
406
|
+
* we instead signal close by returning the Ruby symbol :closed.
|
|
407
|
+
*/
|
|
408
|
+
static VALUE
|
|
409
|
+
watch_take(VALUE self, VALUE vms)
|
|
410
|
+
{
|
|
411
|
+
watch_t *w = watch_live(self);
|
|
412
|
+
/* Widen the conversion to 64-bit so a large-but-legitimate finite timeout
|
|
413
|
+
* (ms > LONG_MAX on LLP64 where C long is 32-bit) does NOT raise RangeError
|
|
414
|
+
* — the public API documents timeout: as non-negative Numeric seconds that
|
|
415
|
+
* waits, and RangeError is not in the Raises list (§2.5). Negative => INFINITE
|
|
416
|
+
* (the nil/forever spelling). Any value past the WaitForSingleObject finite
|
|
417
|
+
* domain (0..0xFFFFFFFE; 0xFFFFFFFF is the INFINITE sentinel) clamps to the
|
|
418
|
+
* max finite wait 0xFFFFFFFE (~49.7 days) rather than accidentally meaning
|
|
419
|
+
* INFINITE. */
|
|
420
|
+
long long ms_in = NUM2LL(vms);
|
|
421
|
+
DWORD ms;
|
|
422
|
+
take_wait_t t;
|
|
423
|
+
DWORD bytes = 0, code = 0, rearm = 0;
|
|
424
|
+
VALUE data = Qnil;
|
|
425
|
+
BOOL got;
|
|
426
|
+
|
|
427
|
+
if (ms_in < 0)
|
|
428
|
+
ms = INFINITE;
|
|
429
|
+
else if (ms_in > 0xFFFFFFFELL)
|
|
430
|
+
ms = 0xFFFFFFFEu; /* clamp to the max finite WaitForSingleObject wait */
|
|
431
|
+
else
|
|
432
|
+
ms = (DWORD)ms_in;
|
|
433
|
+
|
|
434
|
+
/* Serve a stashed batch first (interrupt-race losslessness, §5.12). */
|
|
435
|
+
if (w->stash) {
|
|
436
|
+
VALUE out;
|
|
437
|
+
if (w->stash_len > 0) {
|
|
438
|
+
data = rb_str_new((const char *)w->stash, (long)w->stash_len);
|
|
439
|
+
rb_enc_associate(data, rb_ascii8bit_encoding());
|
|
440
|
+
}
|
|
441
|
+
code = w->stash_code;
|
|
442
|
+
free(w->stash);
|
|
443
|
+
w->stash = NULL;
|
|
444
|
+
w->stash_len = 0;
|
|
445
|
+
w->stash_code = 0;
|
|
446
|
+
out = rb_ary_new_capa(3);
|
|
447
|
+
rb_ary_push(out, data);
|
|
448
|
+
rb_ary_push(out, ULONG2NUM(code));
|
|
449
|
+
rb_ary_push(out, ULONG2NUM(0)); /* re-arm already done when we stashed */
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* If no op is armed (e.g. a prior re-arm failed and Ruby asked us to wait),
|
|
454
|
+
* try to re-arm right here and report the result as the rearm code, so a
|
|
455
|
+
* successful re-arm restores the watch and a real failure is classified
|
|
456
|
+
* (never an unarmed op that returns [nil,995,0] forever — the busy-livelock
|
|
457
|
+
* fixed here). On a successful re-arm we still report a synthetic abort with
|
|
458
|
+
* rearm==0 (no event, op re-armed, keep waiting); on a failed re-arm we
|
|
459
|
+
* report the synthetic abort WITH the live rearm code so classify can turn a
|
|
460
|
+
* permanent failure into a terminal :gone (or :rescan for 1022). */
|
|
461
|
+
if (!w->armed) {
|
|
462
|
+
DWORD rc = watch_arm(w);
|
|
463
|
+
VALUE out = rb_ary_new_capa(3);
|
|
464
|
+
rb_ary_push(out, Qnil);
|
|
465
|
+
rb_ary_push(out, ULONG2NUM(ERROR_OPERATION_ABORTED));
|
|
466
|
+
rb_ary_push(out, ULONG2NUM(rc));
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
t.w = w; t.ms = ms; t.wait = WAIT_FAILED; t.gle = 0;
|
|
471
|
+
|
|
472
|
+
w->in_take++; /* under the GVL, before release (§3.5 rule 3) */
|
|
473
|
+
rb_thread_call_without_gvl(take_wait_fn, &t, take_wait_ubf, &t);
|
|
474
|
+
w->in_take--; /* under the GVL, after reacquire */
|
|
475
|
+
|
|
476
|
+
/* Closed-first check on every GVL reacquisition (§3.4). A concurrent close
|
|
477
|
+
* ran: touch no dir/ov/buf. If we are the last leaver, perform the deferred
|
|
478
|
+
* teardown tail (§3.5 rule 3). The Ruby layer raises Winwatch::Closed. */
|
|
479
|
+
if (w->closed) {
|
|
480
|
+
if (w->in_take == 0)
|
|
481
|
+
watch_teardown(w);
|
|
482
|
+
return ID2SYM(rb_intern("closed"));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (t.wait == WAIT_TIMEOUT) {
|
|
486
|
+
/* No completion within ms. But an interrupt (Thread#kill / Timeout)
|
|
487
|
+
* may be pending — its ubf fired CancelIoEx, which can complete the op
|
|
488
|
+
* with abort (or a real batch that lost the race). Settle the op into
|
|
489
|
+
* the stash, re-arm, then deliver the interrupt. If no interrupt is
|
|
490
|
+
* pending, the (abort) result is discarded and we return nil. */
|
|
491
|
+
if (!w->armed) return Qnil; /* defensive */
|
|
492
|
+
/* Probe without blocking: did the op complete (cancel or data)? */
|
|
493
|
+
got = GetOverlappedResult(w->dir, &w->ov, &bytes, FALSE);
|
|
494
|
+
if (!got && GetLastError() == ERROR_IO_INCOMPLETE)
|
|
495
|
+
return Qnil; /* genuinely still pending — a real timeout */
|
|
496
|
+
/* fall through to settle+stash below */
|
|
497
|
+
} else if (t.wait != WAIT_OBJECT_0) {
|
|
498
|
+
/* WAIT_FAILED (rare): drain so the op is settled, then report. */
|
|
499
|
+
DWORD n = 0;
|
|
500
|
+
CancelIoEx(w->dir, &w->ov);
|
|
501
|
+
GetOverlappedResult(w->dir, &w->ov, &n, TRUE);
|
|
502
|
+
w->armed = 0;
|
|
503
|
+
rearm = watch_arm(w);
|
|
504
|
+
{
|
|
505
|
+
VALUE out = rb_ary_new_capa(3);
|
|
506
|
+
rb_ary_push(out, Qnil);
|
|
507
|
+
rb_ary_push(out, ULONG2NUM(t.gle ? t.gle : ERROR_OPERATION_ABORTED));
|
|
508
|
+
rb_ary_push(out, ULONG2NUM(rearm));
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
/* Signaled: reap the completion (bWait=FALSE, already signaled). */
|
|
513
|
+
got = GetOverlappedResult(w->dir, &w->ov, &bytes, FALSE);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* Settle the reaped op into local code/data. */
|
|
517
|
+
w->armed = 0;
|
|
518
|
+
if (got) {
|
|
519
|
+
code = 0;
|
|
520
|
+
if (bytes > 0) {
|
|
521
|
+
data = rb_str_new((const char *)w->buf, (long)bytes);
|
|
522
|
+
rb_enc_associate(data, rb_ascii8bit_encoding());
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
code = GetLastError(); /* e.g. 995 (aborted by our cancel), 5, 1022, ... */
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/* Re-arm BEFORE returning to Ruby for parsing (rdcw §14), minimizing the
|
|
529
|
+
* no-op-outstanding window. */
|
|
530
|
+
rearm = watch_arm(w);
|
|
531
|
+
|
|
532
|
+
/* Interrupt-stash discipline (§5.12): copy the settled batch into the
|
|
533
|
+
* struct, then deliver any pending interrupt. If Thread#kill / Timeout is
|
|
534
|
+
* pending, rb_thread_check_ints() raises here — the batch is safely stashed
|
|
535
|
+
* and the NEXT take returns it (lossless). If nothing is pending we pull the
|
|
536
|
+
* stash back out and return it normally. The raise crosses no live kernel
|
|
537
|
+
* ownership: the op is already re-armed (or recorded un-armed). */
|
|
538
|
+
if (data != Qnil || code != 0) {
|
|
539
|
+
long dlen = (data == Qnil) ? 0 : RSTRING_LEN(data);
|
|
540
|
+
/* No prior stash here (we returned early at the top if one existed). */
|
|
541
|
+
if (dlen > 0) {
|
|
542
|
+
w->stash = (unsigned char *)malloc((size_t)dlen);
|
|
543
|
+
if (w->stash) {
|
|
544
|
+
memcpy(w->stash, RSTRING_PTR(data), (size_t)dlen);
|
|
545
|
+
w->stash_len = (DWORD)dlen;
|
|
546
|
+
} else {
|
|
547
|
+
w->stash_len = 0;
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
w->stash = NULL;
|
|
551
|
+
w->stash_len = 0;
|
|
552
|
+
}
|
|
553
|
+
w->stash_code = code;
|
|
554
|
+
|
|
555
|
+
rb_thread_check_ints(); /* may raise (Thread#kill / Timeout) — batch stashed */
|
|
556
|
+
|
|
557
|
+
/* No interrupt: hand the stash back to this caller. */
|
|
558
|
+
{
|
|
559
|
+
VALUE out;
|
|
560
|
+
VALUE sdata = Qnil;
|
|
561
|
+
if (w->stash_len > 0) {
|
|
562
|
+
sdata = rb_str_new((const char *)w->stash, (long)w->stash_len);
|
|
563
|
+
rb_enc_associate(sdata, rb_ascii8bit_encoding());
|
|
564
|
+
}
|
|
565
|
+
code = w->stash_code;
|
|
566
|
+
if (w->stash) { free(w->stash); w->stash = NULL; }
|
|
567
|
+
w->stash_len = 0;
|
|
568
|
+
w->stash_code = 0;
|
|
569
|
+
out = rb_ary_new_capa(3);
|
|
570
|
+
rb_ary_push(out, sdata);
|
|
571
|
+
rb_ary_push(out, ULONG2NUM(code));
|
|
572
|
+
rb_ary_push(out, ULONG2NUM(rearm));
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* data==nil AND code==0: a zero-byte successful completion = kernel buffer
|
|
578
|
+
* overflow (the success spelling, §5.1). The op is re-armed; deliver any
|
|
579
|
+
* pending interrupt, then return [nil, 0, rearm] so Ruby classifies it as
|
|
580
|
+
* :rescan. (Overflow already means "re-enumerate the tree", so the small
|
|
581
|
+
* chance of an interrupt dropping this one rescan signal is harmless — the
|
|
582
|
+
* re-armed op re-signals if still overflowing.) */
|
|
583
|
+
rb_thread_check_ints();
|
|
584
|
+
{
|
|
585
|
+
VALUE out = rb_ary_new_capa(3);
|
|
586
|
+
rb_ary_push(out, Qnil);
|
|
587
|
+
rb_ary_push(out, ULONG2NUM(code));
|
|
588
|
+
rb_ary_push(out, ULONG2NUM(rearm));
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/* Watcher#_issue(ov_addr, buf_addr, len) -> Integer (0 = queued, else GLE).
|
|
594
|
+
* :winloop only; calls RDCW with winloop's OVERLAPPED/buffer addresses. */
|
|
595
|
+
static VALUE
|
|
596
|
+
watch_issue(VALUE self, VALUE vov, VALUE vbuf, VALUE vlen)
|
|
597
|
+
{
|
|
598
|
+
watch_t *w = watch_get(self);
|
|
599
|
+
OVERLAPPED *ov = (OVERLAPPED *)(uintptr_t)NUM2ULL(vov);
|
|
600
|
+
void *buf = (void *)(uintptr_t)NUM2ULL(vbuf);
|
|
601
|
+
DWORD len = NUM2ULONG(vlen);
|
|
602
|
+
BOOL ok;
|
|
603
|
+
DWORD gle;
|
|
604
|
+
|
|
605
|
+
if (w->dir == INVALID_HANDLE_VALUE)
|
|
606
|
+
return ULONG2NUM(ERROR_OPERATION_ABORTED);
|
|
607
|
+
|
|
608
|
+
ok = ReadDirectoryChangesW(w->dir, buf, len, w->recursive, w->filter,
|
|
609
|
+
NULL, ov, NULL);
|
|
610
|
+
if (ok) return ULONG2NUM(0);
|
|
611
|
+
gle = GetLastError();
|
|
612
|
+
if (gle == ERROR_IO_PENDING) return ULONG2NUM(0);
|
|
613
|
+
return ULONG2NUM(gle);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/* Watcher#_cancel — CancelIoEx(dir, NULL); safe from any thread; idempotent. */
|
|
617
|
+
static VALUE
|
|
618
|
+
watch_cancel(VALUE self)
|
|
619
|
+
{
|
|
620
|
+
watch_t *w = watch_get(self);
|
|
621
|
+
if (w->dir != INVALID_HANDLE_VALUE)
|
|
622
|
+
CancelIoEx(w->dir, NULL);
|
|
623
|
+
return Qnil;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/* Watcher#_mark_closed — set the closed flag (under the issue lock in Ruby for
|
|
627
|
+
* :winloop; standalone close uses _close which sets it directly). Returns the
|
|
628
|
+
* in_take count so the Ruby/close path could observe it if needed. */
|
|
629
|
+
static VALUE
|
|
630
|
+
watch_mark_closed(VALUE self)
|
|
631
|
+
{
|
|
632
|
+
watch_t *w = watch_get(self);
|
|
633
|
+
w->closed = 1;
|
|
634
|
+
return INT2NUM(w->in_take);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* Watcher#_close_handle — settle + CloseHandle(s); idempotent; both modes.
|
|
638
|
+
* Only performs the teardown tail if no thread is inside _take (the in_take
|
|
639
|
+
* counter, §3.5 rule 3); otherwise the last leaver does it. */
|
|
640
|
+
static VALUE
|
|
641
|
+
watch_close_handle(VALUE self)
|
|
642
|
+
{
|
|
643
|
+
watch_t *w = watch_get(self);
|
|
644
|
+
if (w->in_take == 0)
|
|
645
|
+
watch_teardown(w);
|
|
646
|
+
return Qnil;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/*
|
|
650
|
+
* Watcher#_close_standalone — the full standalone close sequence (§3.5 rule 3):
|
|
651
|
+
* mark closed -> CancelIoEx(dir, &ov) -> deferred teardown tail iff in_take==0.
|
|
652
|
+
* The cancel wakes any blocked _take (which re-checks closed first and returns
|
|
653
|
+
* the :closed indication; the last leaver performs the tail).
|
|
654
|
+
*/
|
|
655
|
+
static VALUE
|
|
656
|
+
watch_close_standalone(VALUE self)
|
|
657
|
+
{
|
|
658
|
+
watch_t *w = watch_get(self);
|
|
659
|
+
if (w->closed) return Qnil; /* idempotent */
|
|
660
|
+
w->closed = 1;
|
|
661
|
+
if (w->dir != INVALID_HANDLE_VALUE && w->armed)
|
|
662
|
+
CancelIoEx(w->dir, &w->ov);
|
|
663
|
+
if (w->in_take == 0)
|
|
664
|
+
watch_teardown(w);
|
|
665
|
+
/* else: the last leaver of _take runs the tail on its way out. */
|
|
666
|
+
return Qnil;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/* Watcher#closed? — raw getter; works on closed objects. */
|
|
670
|
+
static VALUE
|
|
671
|
+
watch_closed_p(VALUE self)
|
|
672
|
+
{
|
|
673
|
+
watch_t *w = watch_get(self);
|
|
674
|
+
return (w->closed || w->dir == INVALID_HANDLE_VALUE) ? Qtrue : Qfalse;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/* Watcher#in_take — diagnostic (used by the deferred-teardown stress test). */
|
|
678
|
+
static VALUE
|
|
679
|
+
watch_in_take(VALUE self)
|
|
680
|
+
{
|
|
681
|
+
return INT2NUM(watch_get(self)->in_take);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/* Watcher#external? — :winloop mode flag (set at _open). */
|
|
685
|
+
static VALUE
|
|
686
|
+
watch_external_p(VALUE self)
|
|
687
|
+
{
|
|
688
|
+
return watch_get(self)->external ? Qtrue : Qfalse;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/* Watcher#_handle_value -> Integer (the directory HANDLE, for sched.op_associate
|
|
692
|
+
* in :winloop mode). winwatch's struct stays the single owner of the handle. */
|
|
693
|
+
static VALUE
|
|
694
|
+
watch_handle_value(VALUE self)
|
|
695
|
+
{
|
|
696
|
+
watch_t *w = watch_get(self);
|
|
697
|
+
return ULL2NUM((unsigned long long)(uintptr_t)w->dir);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/* =====================================================================
|
|
701
|
+
* Record parsing — Winwatch._parse
|
|
702
|
+
* ===================================================================== */
|
|
703
|
+
|
|
704
|
+
/*
|
|
705
|
+
* Winwatch._parse(data) -> [[action_int, name_utf8], ...]
|
|
706
|
+
*
|
|
707
|
+
* Walks the FILE_NOTIFY_INFORMATION NextEntryOffset chain defensively (§5.13):
|
|
708
|
+
* every offset must be DWORD-aligned and strictly within the batch, every
|
|
709
|
+
* FileNameLength must be in bounds, names are NOT NUL-terminated (length in
|
|
710
|
+
* bytes in FileNameLength). A malformed chain stops the walk; the already-valid
|
|
711
|
+
* records are returned plus one synthesized [-1, ""] sentinel (the Ruby layer
|
|
712
|
+
* turns it into a :rescan, because we can no longer trust the batch). Unknown
|
|
713
|
+
* Action values are skipped with an rb_warn under $VERBOSE. Zero-length names
|
|
714
|
+
* are skipped. All arithmetic uses 64-bit locals before comparison against the
|
|
715
|
+
* byte count (no DWORD wraparound, §5.18). No OS state is held at all.
|
|
716
|
+
*/
|
|
717
|
+
#define WINWATCH_ACTION_RESCAN (-1) /* synthetic: malformed chain -> :rescan */
|
|
718
|
+
|
|
719
|
+
static VALUE
|
|
720
|
+
winwatch_parse(VALUE mod, VALUE data)
|
|
721
|
+
{
|
|
722
|
+
const unsigned char *base;
|
|
723
|
+
unsigned long long total, off;
|
|
724
|
+
int truncated = 0;
|
|
725
|
+
VALUE out = rb_ary_new();
|
|
726
|
+
|
|
727
|
+
StringValue(data);
|
|
728
|
+
base = (const unsigned char *)RSTRING_PTR(data);
|
|
729
|
+
total = (unsigned long long)RSTRING_LEN(data);
|
|
730
|
+
|
|
731
|
+
off = 0;
|
|
732
|
+
while (off < total) {
|
|
733
|
+
const FILE_NOTIFY_INFORMATION *fni;
|
|
734
|
+
unsigned long long next, name_bytes, name_off, rec_end;
|
|
735
|
+
DWORD action;
|
|
736
|
+
|
|
737
|
+
/* Offset must be DWORD-aligned and leave room for the fixed header
|
|
738
|
+
* (NextEntryOffset + Action + FileNameLength = 12 bytes). */
|
|
739
|
+
if ((off & 3u) != 0) { truncated = 1; break; }
|
|
740
|
+
if (off + 12 > total) { truncated = 1; break; }
|
|
741
|
+
|
|
742
|
+
fni = (const FILE_NOTIFY_INFORMATION *)(base + off);
|
|
743
|
+
action = fni->Action;
|
|
744
|
+
name_bytes = (unsigned long long)fni->FileNameLength;
|
|
745
|
+
next = (unsigned long long)fni->NextEntryOffset;
|
|
746
|
+
|
|
747
|
+
/* The name lives at off + offsetof(FileName) for name_bytes bytes. */
|
|
748
|
+
name_off = off + (unsigned long long)FIELD_OFFSET(FILE_NOTIFY_INFORMATION, FileName);
|
|
749
|
+
rec_end = name_off + name_bytes;
|
|
750
|
+
if (rec_end > total || (name_bytes & 1u) != 0) { truncated = 1; break; }
|
|
751
|
+
|
|
752
|
+
if (name_bytes > 0) {
|
|
753
|
+
const WCHAR *wname = (const WCHAR *)(base + name_off);
|
|
754
|
+
int wlen = (int)(name_bytes / 2);
|
|
755
|
+
int u8n = WideCharToMultiByte(CP_UTF8, 0, wname, wlen, NULL, 0, NULL, NULL);
|
|
756
|
+
if (u8n > 0) {
|
|
757
|
+
switch (action) {
|
|
758
|
+
case FILE_ACTION_ADDED:
|
|
759
|
+
case FILE_ACTION_REMOVED:
|
|
760
|
+
case FILE_ACTION_MODIFIED:
|
|
761
|
+
case FILE_ACTION_RENAMED_OLD_NAME:
|
|
762
|
+
case FILE_ACTION_RENAMED_NEW_NAME: {
|
|
763
|
+
VALUE name = rb_utf8_str_new(NULL, u8n);
|
|
764
|
+
VALUE rec;
|
|
765
|
+
WideCharToMultiByte(CP_UTF8, 0, wname, wlen,
|
|
766
|
+
RSTRING_PTR(name), u8n, NULL, NULL);
|
|
767
|
+
rec = rb_ary_new_capa(2);
|
|
768
|
+
rb_ary_push(rec, UINT2NUM(action));
|
|
769
|
+
rb_ary_push(rec, name);
|
|
770
|
+
rb_ary_push(out, rec);
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
default:
|
|
774
|
+
if (RTEST(ruby_verbose))
|
|
775
|
+
rb_warn("winwatch: unknown FILE_ACTION %lu skipped",
|
|
776
|
+
(unsigned long)action);
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/* u8n<=0: a name that won't convert — skip it, keep walking. */
|
|
781
|
+
}
|
|
782
|
+
/* name_bytes==0: zero-length name, skip. */
|
|
783
|
+
|
|
784
|
+
if (next == 0) break; /* end of chain (clean) */
|
|
785
|
+
if ((next & 3u) != 0) { truncated = 1; break; } /* offset must be aligned */
|
|
786
|
+
if (next < (rec_end - off)) { truncated = 1; break; } /* overlaps this record */
|
|
787
|
+
if (off + next <= off) { truncated = 1; break; } /* no forward progress / wrap */
|
|
788
|
+
off += next;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/* Walk stopped on a malformed chain: synthesize a :rescan sentinel so the
|
|
792
|
+
* Ruby layer can no longer trust the batch to be complete (§5.13). */
|
|
793
|
+
if (truncated) {
|
|
794
|
+
VALUE rec = rb_ary_new_capa(2);
|
|
795
|
+
rb_ary_push(rec, INT2NUM(WINWATCH_ACTION_RESCAN));
|
|
796
|
+
rb_ary_push(rec, rb_utf8_str_new("", 0));
|
|
797
|
+
rb_ary_push(out, rec);
|
|
798
|
+
}
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/* =====================================================================
|
|
803
|
+
* GetLongPathNameW bridge — Winwatch._long_path
|
|
804
|
+
* ===================================================================== */
|
|
805
|
+
|
|
806
|
+
/*
|
|
807
|
+
* Winwatch._long_path(abs_utf8) -> String | nil
|
|
808
|
+
*
|
|
809
|
+
* Best-effort 8.3 -> long-name normalization via GetLongPathNameW (rdcw §9).
|
|
810
|
+
* Returns nil on ANY failure (file gone, FS without short names, conversion
|
|
811
|
+
* error) — never raises. Input is an absolute UTF-8 path (the joined path).
|
|
812
|
+
* GVL held; usually no round-trip (may touch the FS on SMB), per-tilde only.
|
|
813
|
+
*/
|
|
814
|
+
static VALUE
|
|
815
|
+
winwatch_long_path(VALUE mod, VALUE vpath)
|
|
816
|
+
{
|
|
817
|
+
int len, n;
|
|
818
|
+
WCHAR *wpath, *wout;
|
|
819
|
+
DWORD got, cap;
|
|
820
|
+
VALUE out;
|
|
821
|
+
|
|
822
|
+
StringValue(vpath);
|
|
823
|
+
len = (int)RSTRING_LEN(vpath);
|
|
824
|
+
if (len == 0) return Qnil;
|
|
825
|
+
n = MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(vpath), len, NULL, 0);
|
|
826
|
+
if (n <= 0) return Qnil;
|
|
827
|
+
wpath = (WCHAR *)malloc(sizeof(WCHAR) * ((size_t)n + 1));
|
|
828
|
+
if (!wpath) return Qnil;
|
|
829
|
+
MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(vpath), len, wpath, n);
|
|
830
|
+
wpath[n] = 0;
|
|
831
|
+
|
|
832
|
+
got = GetLongPathNameW(wpath, NULL, 0);
|
|
833
|
+
if (got == 0) { free(wpath); return Qnil; }
|
|
834
|
+
cap = got;
|
|
835
|
+
wout = (WCHAR *)malloc(sizeof(WCHAR) * (size_t)cap);
|
|
836
|
+
if (!wout) { free(wpath); return Qnil; }
|
|
837
|
+
got = GetLongPathNameW(wpath, wout, cap);
|
|
838
|
+
free(wpath);
|
|
839
|
+
if (got == 0 || got >= cap) { free(wout); return Qnil; }
|
|
840
|
+
|
|
841
|
+
{
|
|
842
|
+
int u8n = WideCharToMultiByte(CP_UTF8, 0, wout, (int)got, NULL, 0, NULL, NULL);
|
|
843
|
+
if (u8n <= 0) { free(wout); return Qnil; }
|
|
844
|
+
out = rb_utf8_str_new(NULL, u8n);
|
|
845
|
+
WideCharToMultiByte(CP_UTF8, 0, wout, (int)got, RSTRING_PTR(out), u8n, NULL, NULL);
|
|
846
|
+
}
|
|
847
|
+
free(wout);
|
|
848
|
+
return out;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/* ----------------------------------------------------------------- Init --- */
|
|
852
|
+
|
|
853
|
+
void
|
|
854
|
+
Init_winwatch(void)
|
|
855
|
+
{
|
|
856
|
+
mWinwatch = rb_define_module("Winwatch");
|
|
857
|
+
|
|
858
|
+
eError = rb_define_class_under(mWinwatch, "Error", rb_eStandardError);
|
|
859
|
+
eOSError = rb_define_class_under(mWinwatch, "OSError", eError);
|
|
860
|
+
eNotFound = rb_define_class_under(mWinwatch, "NotFound", eOSError);
|
|
861
|
+
eAccessDenied = rb_define_class_under(mWinwatch, "AccessDenied", eOSError);
|
|
862
|
+
eUnsupported = rb_define_class_under(mWinwatch, "Unsupported", eOSError);
|
|
863
|
+
eNotADirectory = rb_define_class_under(mWinwatch, "NotADirectory", eError);
|
|
864
|
+
eClosed = rb_define_class_under(mWinwatch, "Closed", eError);
|
|
865
|
+
|
|
866
|
+
cWatcher = rb_define_class_under(mWinwatch, "Watcher", rb_cObject);
|
|
867
|
+
rb_define_alloc_func(cWatcher, watch_alloc);
|
|
868
|
+
rb_define_singleton_method(cWatcher, "_open", watch_open, 5);
|
|
869
|
+
rb_define_method(cWatcher, "_arm", watch_m_arm, 0);
|
|
870
|
+
rb_define_method(cWatcher, "_take", watch_take, 1);
|
|
871
|
+
rb_define_method(cWatcher, "_issue", watch_issue, 3);
|
|
872
|
+
rb_define_method(cWatcher, "_cancel", watch_cancel, 0);
|
|
873
|
+
rb_define_method(cWatcher, "_mark_closed", watch_mark_closed, 0);
|
|
874
|
+
rb_define_method(cWatcher, "_close_handle", watch_close_handle, 0);
|
|
875
|
+
rb_define_method(cWatcher, "_close_standalone", watch_close_standalone, 0);
|
|
876
|
+
rb_define_method(cWatcher, "closed?", watch_closed_p, 0);
|
|
877
|
+
rb_define_method(cWatcher, "_in_take", watch_in_take, 0);
|
|
878
|
+
rb_define_method(cWatcher, "_external?", watch_external_p, 0);
|
|
879
|
+
rb_define_method(cWatcher, "_handle_value", watch_handle_value, 0);
|
|
880
|
+
|
|
881
|
+
rb_define_singleton_method(mWinwatch, "_parse", winwatch_parse, 1);
|
|
882
|
+
rb_define_singleton_method(mWinwatch, "_long_path", winwatch_long_path, 1);
|
|
883
|
+
}
|