winproc 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.
@@ -0,0 +1,1922 @@
1
+ /*
2
+ * winproc — Windows process control for Ruby: argv-array spawning with exact
3
+ * quoting + per-handle inheritance, job objects with kill-on-close (crash-safe
4
+ * process trees), ConPTY pseudoconsoles, and UAC elevation helpers.
5
+ *
6
+ * PURE C (no C++), so rb_raise/longjmp is the normal, safe error mechanism —
7
+ * the same discipline as winipc/winloop/phylax, and the opposite of the lithos
8
+ * /EHsc hazard. Every kernel object lives in a TypedData wrapper whose free
9
+ * hook closes it, so a forgotten #close never leaks a HANDLE. Blocking waits
10
+ * release the GVL (rb_thread_call_without_gvl) with an unblock function so other
11
+ * Ruby threads run and Thread#kill / Ctrl-C / Timeout can break them.
12
+ *
13
+ * Include <ruby.h> before the Windows headers; never name a variable IN/OUT.
14
+ *
15
+ * Arch-neutral by construction: gates on _WIN64 (not _M_X64) and uses ULONG_PTR
16
+ * for handle-sized values; no inline asm. Builds unchanged on arm64-mswin.
17
+ *
18
+ * Links kernel32 (process/job/pipe/pseudoconsole, default) + advapi32
19
+ * (token/privilege/SID) + shell32 (ShellExecuteExW) + ole32 (CoInitializeEx).
20
+ */
21
+
22
+ #include <ruby.h>
23
+ #include <ruby/thread.h>
24
+ #include <ruby/encoding.h>
25
+ #include <ruby/io.h>
26
+ #include <limits.h>
27
+
28
+ #define WIN32_LEAN_AND_MEAN
29
+ #include <windows.h>
30
+ #include <shellapi.h> /* ShellExecuteExW, SHELLEXECUTEINFOW */
31
+ #include <objbase.h> /* CoInitializeEx / CoUninitialize */
32
+
33
+ /* The mswin build compiles with _WIN32_WINNT = _WIN32_WINNT_WIN8 (0x0602), so
34
+ * these Windows 10+ ProcThreadAttribute selectors may be gated out of the SDK
35
+ * headers. Define them from their documented values (verified against
36
+ * <processthreadsapi.h>: ProcThreadAttributeValue(num, thread, input, additive))
37
+ * so the source builds regardless of the target-version macro. Behaviour at
38
+ * runtime is unchanged — the JOB_LIST attribute is Win10+, and PSEUDOCONSOLE is
39
+ * only ever used after the runtime CreatePseudoConsole probe (Win10 1809+). */
40
+ #ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST
41
+ # define PROC_THREAD_ATTRIBUTE_JOB_LIST \
42
+ ProcThreadAttributeValue(ProcThreadAttributeJobList, FALSE, TRUE, FALSE)
43
+ #endif
44
+ #ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
45
+ # define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \
46
+ ProcThreadAttributeValue(ProcThreadAttributePseudoConsole, FALSE, TRUE, FALSE)
47
+ #endif
48
+ /* The two enum members above may also be absent; supply their documented
49
+ * ordinals (ProcThreadAttributeJobList = 13, ProcThreadAttributePseudoConsole =
50
+ * 22) when the SDK enum doesn't define them. */
51
+ #ifndef ProcThreadAttributeJobList
52
+ # define ProcThreadAttributeJobList 13
53
+ #endif
54
+ #ifndef ProcThreadAttributePseudoConsole
55
+ # define ProcThreadAttributePseudoConsole 22
56
+ #endif
57
+
58
+ /* ------------------------------------------------------------------ globals */
59
+
60
+ static VALUE mWinproc;
61
+ static VALUE cProcess, cJob, cStream, cPTY;
62
+ static VALUE eError, eOSError, eNotFound, eAccessDenied, eCanceled, eBrokenPipe,
63
+ eElevationRequired, ePrivilegeNotHeld, eUnsupported,
64
+ eModeError, eClosed;
65
+
66
+ /* ConPTY entry points resolved at runtime (absent on Windows < 10 1809). */
67
+ typedef HRESULT (WINAPI *PFN_CreatePseudoConsole)(COORD, HANDLE, HANDLE, DWORD, void **);
68
+ typedef HRESULT (WINAPI *PFN_ResizePseudoConsole)(void *, COORD);
69
+ typedef void (WINAPI *PFN_ClosePseudoConsole)(void *);
70
+ static PFN_CreatePseudoConsole p_CreatePseudoConsole;
71
+ static PFN_ResizePseudoConsole p_ResizePseudoConsole;
72
+ static PFN_ClosePseudoConsole p_ClosePseudoConsole;
73
+
74
+ /* The private completion-port key for a Job's IOCP, and the wake key used by
75
+ * #close / ubf to break an in-flight GetQueuedCompletionStatus. */
76
+ #define JOB_KEY ((ULONG_PTR)0x4A4F4200) /* 'JOB\0' */
77
+ #define WAKE_KEY ((ULONG_PTR)0x57414B45) /* 'WAKE' */
78
+
79
+ /* ------------------------------------------------------------ small helpers */
80
+
81
+ /* UTF-8 Ruby String -> freshly xmalloc'd NUL-terminated UTF-16. Caller xfrees.
82
+ * Used for paths/argv/cwd/verbs — text, never binary payloads. */
83
+ static WCHAR *
84
+ to_wide(VALUE str)
85
+ {
86
+ int len, n;
87
+ WCHAR *w;
88
+ StringValue(str);
89
+ len = (int)RSTRING_LEN(str);
90
+ if (len == 0) {
91
+ w = (WCHAR *)xmalloc(sizeof(WCHAR));
92
+ w[0] = 0;
93
+ return w;
94
+ }
95
+ n = MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, NULL, 0);
96
+ if (n <= 0) rb_raise(eError, "winproc: invalid UTF-8 in string");
97
+ w = (WCHAR *)xmalloc(sizeof(WCHAR) * (n + 1));
98
+ MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, w, n);
99
+ w[n] = 0;
100
+ return w;
101
+ }
102
+
103
+ /* Like to_wide but for a String that may legitimately contain embedded NULs
104
+ * (the env block "K=V\0K=V\0"); converts the whole byte run and appends the
105
+ * final double-NUL terminator. Returns NULL only for an empty input. */
106
+ static WCHAR *
107
+ env_to_wide(VALUE str)
108
+ {
109
+ int len, n;
110
+ WCHAR *w;
111
+ StringValue(str);
112
+ len = (int)RSTRING_LEN(str);
113
+ if (len == 0) return NULL;
114
+ n = MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, NULL, 0);
115
+ if (n <= 0) rb_raise(eError, "winproc: invalid UTF-8 in environment");
116
+ /* +1 for the extra terminating NUL that closes the block. */
117
+ w = (WCHAR *)xmalloc(sizeof(WCHAR) * (n + 1));
118
+ MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, w, n);
119
+ w[n] = 0;
120
+ return w;
121
+ }
122
+
123
+ /* Build + raise a winproc error carrying @code (the Win32 error). The code must
124
+ * be captured by the caller immediately after the failing syscall. */
125
+ static void
126
+ raise_code(VALUE klass, const char *api, DWORD code)
127
+ {
128
+ VALUE exc, msg;
129
+ WCHAR *buf = NULL;
130
+ char detail[512];
131
+ DWORD n;
132
+
133
+ detail[0] = 0;
134
+ n = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
135
+ FORMAT_MESSAGE_IGNORE_INSERTS, NULL, code,
136
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&buf, 0, NULL);
137
+ if (n && buf) {
138
+ while (n && (buf[n-1] == L'\r' || buf[n-1] == L'\n' || buf[n-1] == L'.')) buf[--n] = 0;
139
+ WideCharToMultiByte(CP_UTF8, 0, buf, -1, detail, (int)sizeof(detail), NULL, NULL);
140
+ detail[sizeof(detail) - 1] = 0;
141
+ }
142
+ /* Release the OS buffer BEFORE any Ruby allocation, so an OOM longjmp from
143
+ * rb_sprintf / rb_exc_new_str cannot leak it. */
144
+ if (buf) LocalFree(buf);
145
+
146
+ if (detail[0])
147
+ msg = rb_sprintf("%s: %s (error %lu)", api, detail, (unsigned long)code);
148
+ else
149
+ msg = rb_sprintf("%s failed (error %lu)", api, (unsigned long)code);
150
+ exc = rb_exc_new_str(klass, msg);
151
+ rb_iv_set(exc, "@code", ULONG2NUM(code));
152
+ rb_exc_raise(exc);
153
+ }
154
+
155
+ /* Pick the right subclass for a Win32 error and raise it. */
156
+ static void
157
+ raise_gle(const char *api, DWORD code)
158
+ {
159
+ VALUE klass = eOSError;
160
+ switch (code) {
161
+ case ERROR_FILE_NOT_FOUND: /* 2 */
162
+ case ERROR_PATH_NOT_FOUND: klass = eNotFound; break;/* 3 */
163
+ case ERROR_ACCESS_DENIED: klass = eAccessDenied;break;/* 5 */
164
+ case ERROR_OPERATION_ABORTED: /* 995 */
165
+ case ERROR_CANCELLED: klass = eCanceled; break;/* 1223 */
166
+ case ERROR_BROKEN_PIPE: /* 109 */
167
+ case ERROR_NO_DATA: /* 232 */
168
+ case ERROR_PIPE_NOT_CONNECTED: klass = eBrokenPipe; break;/* 233 */
169
+ case ERROR_ELEVATION_REQUIRED: klass = eElevationRequired; break; /* 740 */
170
+ case ERROR_NOT_ALL_ASSIGNED: klass = ePrivilegeNotHeld; break; /* 1300 */
171
+ case ERROR_NOT_SUPPORTED: /* 50 */
172
+ case ERROR_CALL_NOT_IMPLEMENTED: /* 120 */
173
+ case ERROR_PROC_NOT_FOUND: klass = eUnsupported; break;/* 127 */
174
+ default: break;
175
+ }
176
+ raise_code(klass, api, code);
177
+ }
178
+
179
+ /* =====================================================================
180
+ * Winproc::Process — wraps the child's process HANDLE
181
+ * ===================================================================== */
182
+
183
+ typedef struct {
184
+ HANDLE h; /* process handle; INVALID_HANDLE_VALUE sentinel */
185
+ HANDLE cancel_event; /* manual-reset; ubf + #close wake for #wait */
186
+ DWORD pid;
187
+ DWORD exit_code; /* memoized once reaped */
188
+ int exited; /* exit_code valid */
189
+ volatile LONG waiters;/* #wait calls inside the no-GVL wait (E-30) */
190
+ volatile LONG closing;/* set by #close; woken waiters raise Closed */
191
+ int closed;
192
+ } process_t;
193
+
194
+ static void
195
+ process_free(void *p)
196
+ {
197
+ process_t *pr = (process_t *)p;
198
+ /* GC can never run while a wait is in flight — the waiting thread's stack
199
+ * references the object — so free never races a waiter. Never terminates. */
200
+ if (pr->h && pr->h != INVALID_HANDLE_VALUE) CloseHandle(pr->h);
201
+ if (pr->cancel_event) CloseHandle(pr->cancel_event);
202
+ xfree(pr);
203
+ }
204
+
205
+ static const rb_data_type_t process_type = {
206
+ "Winproc::Process", { 0, process_free, 0, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
207
+ };
208
+
209
+ static VALUE
210
+ process_alloc(VALUE klass)
211
+ {
212
+ process_t *pr;
213
+ VALUE obj = TypedData_Make_Struct(klass, process_t, &process_type, pr);
214
+ pr->h = INVALID_HANDLE_VALUE;
215
+ pr->cancel_event = NULL;
216
+ pr->pid = 0;
217
+ pr->exit_code = 0;
218
+ pr->exited = 0;
219
+ pr->waiters = 0;
220
+ pr->closing = 0;
221
+ pr->closed = 0;
222
+ return obj;
223
+ }
224
+
225
+ static process_t *
226
+ process_get(VALUE self)
227
+ {
228
+ process_t *pr;
229
+ TypedData_Get_Struct(self, process_t, &process_type, pr);
230
+ return pr;
231
+ }
232
+
233
+ static process_t *
234
+ process_live(VALUE self)
235
+ {
236
+ process_t *pr = process_get(self);
237
+ if (pr->closed) rb_raise(eClosed, "winproc: process is closed");
238
+ return pr;
239
+ }
240
+
241
+ /* user-facing Process.new is forbidden; alloc exists only for safe GC. */
242
+ static VALUE
243
+ process_initialize(int argc, VALUE *argv, VALUE self)
244
+ {
245
+ (void)argc; (void)argv; (void)self;
246
+ rb_raise(eError, "winproc: processes are created by Winproc.spawn / "
247
+ "Winproc.pty / Winproc.runas");
248
+ return self;
249
+ }
250
+
251
+ /* --- interruptible, GVL-released wait on {h, cancel_event} ----------------- */
252
+
253
+ typedef struct {
254
+ process_t *pr;
255
+ DWORD ms; /* INFINITE or remaining slice */
256
+ DWORD wait;
257
+ DWORD exit_code;
258
+ DWORD gle;
259
+ int got_exit; /* exit_code valid */
260
+ int do_closed; /* woke with closing set */
261
+ } pwait_t;
262
+
263
+ static void *
264
+ pwait_fn(void *p)
265
+ {
266
+ pwait_t *w = (pwait_t *)p;
267
+ HANDLE hs[2];
268
+ DWORD r;
269
+ hs[0] = w->pr->h;
270
+ hs[1] = w->pr->cancel_event;
271
+ r = WaitForMultipleObjects(2, hs, FALSE, w->ms);
272
+ w->wait = r;
273
+ if (r == WAIT_OBJECT_0) {
274
+ DWORD code = 0;
275
+ if (GetExitCodeProcess(w->pr->h, &code)) {
276
+ w->exit_code = code;
277
+ w->got_exit = 1;
278
+ } else {
279
+ w->gle = GetLastError();
280
+ }
281
+ } else if (r == WAIT_OBJECT_0 + 1) {
282
+ /* cancel_event signaled: closing (E-30) or ubf interrupt */
283
+ if (w->pr->closing) w->do_closed = 1;
284
+ } else if (r == WAIT_FAILED) {
285
+ w->gle = GetLastError();
286
+ }
287
+ return NULL;
288
+ }
289
+
290
+ static void
291
+ pwait_ubf(void *p)
292
+ {
293
+ pwait_t *w = (pwait_t *)p;
294
+ SetEvent(w->pr->cancel_event); /* manual-reset: all concurrent waiters wake */
295
+ }
296
+
297
+ /* The body of Process#_wait, run inside rb_ensure so the waiters count is
298
+ * decremented on EVERY exit — including a Thread#kill / Timeout longjmp out of
299
+ * rb_thread_check_ints (otherwise the count would leak and #close would spin
300
+ * forever / the VM would tear the thread down with the count still held). */
301
+ typedef struct { process_t *pr; long long ms_in; } pwait_args;
302
+
303
+ static VALUE
304
+ process_wait_body(VALUE vargs)
305
+ {
306
+ pwait_args *a = (pwait_args *)vargs;
307
+ process_t *pr = a->pr;
308
+ int infinite = (a->ms_in < 0);
309
+ ULONGLONG deadline = infinite ? 0 : GetTickCount64() + (ULONGLONG)a->ms_in;
310
+
311
+ for (;;) {
312
+ pwait_t w;
313
+ DWORD slice;
314
+ if (infinite) {
315
+ slice = INFINITE;
316
+ } else {
317
+ ULONGLONG now = GetTickCount64();
318
+ ULONGLONG left;
319
+ if (now >= deadline) return Qnil;
320
+ left = deadline - now;
321
+ /* Clamp the per-wait slice below INFINITE (0xFFFFFFFF): the deadline
322
+ * loop re-waits, so a clamped slice on a multi-day timeout is exact,
323
+ * and a slice of exactly INFINITE would wait forever (E-Slice). */
324
+ slice = (left >= (ULONGLONG)INFINITE) ? (INFINITE - 1) : (DWORD)left;
325
+ }
326
+ w.pr = pr; w.ms = slice; w.wait = WAIT_FAILED;
327
+ w.exit_code = 0; w.gle = 0; w.got_exit = 0; w.do_closed = 0;
328
+ rb_thread_call_without_gvl(pwait_fn, &w, pwait_ubf, &w);
329
+
330
+ if (w.got_exit) {
331
+ pr->exit_code = w.exit_code;
332
+ pr->exited = 1;
333
+ return ULONG2NUM(w.exit_code);
334
+ }
335
+ if (w.wait == WAIT_OBJECT_0 && !w.got_exit)
336
+ raise_gle("GetExitCodeProcess", w.gle);
337
+ if (w.do_closed)
338
+ /* E-30: #close woke us. Do NOT ResetEvent — leave the manual-reset
339
+ * event set so sibling waiters also drain. */
340
+ rb_raise(eClosed, "winproc: process is closed");
341
+ if (w.wait == WAIT_FAILED)
342
+ raise_gle("WaitForMultipleObjects", w.gle);
343
+ /* cancel_event without closing => a ubf interrupt. Reset (GVL held),
344
+ * service interrupts (may longjmp), recompute, re-wait. */
345
+ if (w.wait == WAIT_OBJECT_0 + 1) {
346
+ ResetEvent(pr->cancel_event);
347
+ rb_thread_check_ints();
348
+ if (pr->closing || pr->closed)
349
+ rb_raise(eClosed, "winproc: process is closed");
350
+ }
351
+ /* WAIT_TIMEOUT loops; the deadline check at the top returns nil. */
352
+ }
353
+ }
354
+
355
+ static VALUE
356
+ process_wait_ensure(VALUE vpr)
357
+ {
358
+ process_t *pr = (process_t *)vpr;
359
+ InterlockedDecrement(&pr->waiters);
360
+ return Qnil;
361
+ }
362
+
363
+ /* Process#_wait(ms) -> exit code Integer, or nil on timeout (-1 ms = INFINITE).
364
+ * Memoizes the exit code. Raises Closed if #close is in progress. */
365
+ static VALUE
366
+ process_do_wait(VALUE self, VALUE vms)
367
+ {
368
+ process_t *pr = process_get(self);
369
+ pwait_args a;
370
+
371
+ if (pr->exited) return ULONG2NUM(pr->exit_code); /* memoized; works after close */
372
+ if (pr->closed) rb_raise(eClosed, "winproc: process is closed");
373
+
374
+ a.pr = pr;
375
+ a.ms_in = NUM2LL(vms); /* 64-bit: ms can exceed LONG_MAX on LLP64 (E-Slice) */
376
+ InterlockedIncrement(&pr->waiters); /* under the GVL, before the region */
377
+ return rb_ensure(process_wait_body, (VALUE)&a, process_wait_ensure, (VALUE)pr);
378
+ }
379
+
380
+ static VALUE
381
+ process_pid(VALUE self)
382
+ {
383
+ /* never raises; works after close */
384
+ return ULONG2NUM(process_get(self)->pid);
385
+ }
386
+
387
+ static VALUE
388
+ process_alive_p(VALUE self)
389
+ {
390
+ process_t *pr = process_live(self);
391
+ DWORD r;
392
+ if (pr->exited) return Qfalse;
393
+ r = WaitForSingleObject(pr->h, 0);
394
+ if (r == WAIT_OBJECT_0) {
395
+ DWORD code = 0;
396
+ if (GetExitCodeProcess(pr->h, &code)) { pr->exit_code = code; pr->exited = 1; }
397
+ return Qfalse;
398
+ }
399
+ if (r == WAIT_TIMEOUT) return Qtrue;
400
+ raise_gle("WaitForSingleObject", GetLastError());
401
+ return Qfalse; /* unreached */
402
+ }
403
+
404
+ static VALUE
405
+ process_exitstatus(VALUE self)
406
+ {
407
+ process_t *pr = process_get(self);
408
+ DWORD r;
409
+ if (pr->exited) return ULONG2NUM(pr->exit_code);
410
+ if (pr->closed) return Qnil; /* memoized value or nil after close */
411
+ /* Only read GetExitCodeProcess after WaitForSingleObject reports signaled,
412
+ * so 259 STILL_ACTIVE is never exposed as a real status (E-17). */
413
+ r = WaitForSingleObject(pr->h, 0);
414
+ if (r == WAIT_OBJECT_0) {
415
+ DWORD code = 0;
416
+ if (GetExitCodeProcess(pr->h, &code)) {
417
+ pr->exit_code = code; pr->exited = 1;
418
+ return ULONG2NUM(code);
419
+ }
420
+ raise_gle("GetExitCodeProcess", GetLastError());
421
+ }
422
+ if (r == WAIT_TIMEOUT) return Qnil; /* still running */
423
+ raise_gle("WaitForSingleObject", GetLastError());
424
+ return Qnil; /* unreached */
425
+ }
426
+
427
+ static VALUE
428
+ process_kill(int argc, VALUE *argv, VALUE self)
429
+ {
430
+ process_t *pr = process_live(self);
431
+ VALUE vcode;
432
+ UINT code = 1;
433
+ DWORD r;
434
+ rb_scan_args(argc, argv, "01", &vcode);
435
+ if (!NIL_P(vcode)) code = (UINT)NUM2UINT(vcode);
436
+
437
+ if (pr->exited) return self; /* no-op if already exited */
438
+ r = WaitForSingleObject(pr->h, 0);
439
+ if (r == WAIT_OBJECT_0) {
440
+ DWORD c = 0;
441
+ if (GetExitCodeProcess(pr->h, &c)) { pr->exit_code = c; pr->exited = 1; }
442
+ return self; /* already gone */
443
+ }
444
+ if (!TerminateProcess(pr->h, code)) {
445
+ DWORD gle = GetLastError();
446
+ /* lost the race with natural exit: treat "already gone" as success */
447
+ if (gle == ERROR_ACCESS_DENIED) {
448
+ if (WaitForSingleObject(pr->h, 0) == WAIT_OBJECT_0) return self;
449
+ }
450
+ raise_gle("TerminateProcess", gle);
451
+ }
452
+ return self;
453
+ }
454
+
455
+ /* Memoize the exit code if the process has exited (used by #close / PTY close,
456
+ * so exitstatus answers after the handle is gone). Best-effort; never raises. */
457
+ static void
458
+ process_memoize_exit(process_t *pr)
459
+ {
460
+ if (!pr->exited && pr->h != INVALID_HANDLE_VALUE) {
461
+ if (WaitForSingleObject(pr->h, 0) == WAIT_OBJECT_0) {
462
+ DWORD code = 0;
463
+ if (GetExitCodeProcess(pr->h, &code)) { pr->exit_code = code; pr->exited = 1; }
464
+ }
465
+ }
466
+ }
467
+
468
+ /* E-30 close discipline: set closing, wake any in-flight waiter, spin (no-GVL)
469
+ * until waiters drain, THEN close the handles. */
470
+ typedef struct { process_t *pr; } pclose_t;
471
+ static void *
472
+ pclose_spin_fn(void *p)
473
+ {
474
+ process_t *pr = ((pclose_t *)p)->pr;
475
+ while (pr->waiters) {
476
+ SetEvent(pr->cancel_event); /* re-wake: cover a waiter not yet blocked */
477
+ SleepEx(1, FALSE);
478
+ }
479
+ return NULL;
480
+ }
481
+
482
+ static VALUE
483
+ process_close(VALUE self)
484
+ {
485
+ process_t *pr = process_get(self);
486
+ if (pr->closed) return Qnil;
487
+ pr->closing = 1;
488
+ if (pr->cancel_event) {
489
+ pclose_t c; c.pr = pr;
490
+ SetEvent(pr->cancel_event);
491
+ rb_thread_call_without_gvl(pclose_spin_fn, &c, NULL, NULL);
492
+ }
493
+ process_memoize_exit(pr);
494
+ if (pr->h != INVALID_HANDLE_VALUE) { CloseHandle(pr->h); pr->h = INVALID_HANDLE_VALUE; }
495
+ if (pr->cancel_event) { CloseHandle(pr->cancel_event); pr->cancel_event = NULL; }
496
+ pr->closed = 1;
497
+ return Qnil;
498
+ }
499
+
500
+ static VALUE process_closed_p(VALUE self) { return process_get(self)->closed ? Qtrue : Qfalse; }
501
+
502
+ /* =====================================================================
503
+ * Winproc::Stream — one end of a redirection / ConPTY pipe (synchronous I/O)
504
+ * ===================================================================== */
505
+
506
+ typedef struct {
507
+ HANDLE h;
508
+ int writable; /* direction; ModeError enforcement */
509
+ volatile LONG inflight; /* 1 while a no-GVL read/write is running */
510
+ volatile LONG closing; /* set by close; in-flight op raises Closed */
511
+ HANDLE op_thread; /* DUPLICATED thread handle of the in-flight op
512
+ (published/retired ONLY under the GVL) */
513
+ int closed;
514
+ } stream_t;
515
+
516
+ static void
517
+ stream_free(void *p)
518
+ {
519
+ stream_t *s = (stream_t *)p;
520
+ /* GC can never run while an op is in flight — the op thread's stack
521
+ * references the object. */
522
+ if (s->h && s->h != INVALID_HANDLE_VALUE) CloseHandle(s->h);
523
+ if (s->op_thread) CloseHandle(s->op_thread);
524
+ xfree(s);
525
+ }
526
+
527
+ static const rb_data_type_t stream_type = {
528
+ "Winproc::Stream", { 0, stream_free, 0, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
529
+ };
530
+
531
+ static VALUE
532
+ stream_alloc(VALUE klass)
533
+ {
534
+ stream_t *s;
535
+ VALUE obj = TypedData_Make_Struct(klass, stream_t, &stream_type, s);
536
+ s->h = INVALID_HANDLE_VALUE;
537
+ s->writable = 0;
538
+ s->inflight = 0;
539
+ s->closing = 0;
540
+ s->op_thread = NULL;
541
+ s->closed = 0;
542
+ return obj;
543
+ }
544
+
545
+ static stream_t *
546
+ stream_get(VALUE self)
547
+ {
548
+ stream_t *s;
549
+ TypedData_Get_Struct(self, stream_t, &stream_type, s);
550
+ return s;
551
+ }
552
+
553
+ static stream_t *
554
+ stream_live(VALUE self)
555
+ {
556
+ stream_t *s = stream_get(self);
557
+ if (s->closed || s->h == INVALID_HANDLE_VALUE)
558
+ rb_raise(eClosed, "winproc: stream is closed");
559
+ return s;
560
+ }
561
+
562
+ static VALUE
563
+ stream_initialize(int argc, VALUE *argv, VALUE self)
564
+ {
565
+ (void)argc; (void)argv; (void)self;
566
+ rb_raise(eError, "winproc: streams are created by Winproc.spawn / Winproc.pty");
567
+ return self;
568
+ }
569
+
570
+ /* read/write run with the GVL released around a SYNCHRONOUS ReadFile/WriteFile,
571
+ * cancelable via CancelSynchronousIo(op_thread). The buffer is a private malloc
572
+ * (no RSTRING_PTR across the no-GVL region — GC-compaction rule). */
573
+ typedef struct {
574
+ stream_t *s;
575
+ char *buf;
576
+ DWORD cap; /* read: capacity / write: total length */
577
+ DWORD done; /* bytes transferred this call */
578
+ DWORD gle;
579
+ BOOL ok;
580
+ int is_write;
581
+ } sio_t;
582
+
583
+ static void *
584
+ sio_fn(void *p)
585
+ {
586
+ sio_t *io = (sio_t *)p;
587
+ DWORD n = 0;
588
+ if (io->is_write) {
589
+ DWORD off = 0;
590
+ io->ok = TRUE;
591
+ while (off < io->cap) {
592
+ if (!WriteFile(io->s->h, io->buf + off, io->cap - off, &n, NULL)) {
593
+ io->ok = FALSE; io->gle = GetLastError(); break;
594
+ }
595
+ off += n;
596
+ }
597
+ io->done = off;
598
+ } else {
599
+ io->ok = ReadFile(io->s->h, io->buf, io->cap, &n, NULL);
600
+ if (!io->ok) io->gle = GetLastError();
601
+ io->done = n;
602
+ }
603
+ /* clear inflight INSIDE the no-GVL fn so #close can spin on it w/o the GVL */
604
+ InterlockedExchange(&io->s->inflight, 0);
605
+ return NULL;
606
+ }
607
+
608
+ static void
609
+ sio_ubf(void *p)
610
+ {
611
+ sio_t *io = (sio_t *)p;
612
+ /* op's own per-iteration duplicate; valid exactly while the op sits in
613
+ * ReadFile/WriteFile (the inflight window). May be invoked repeatedly. */
614
+ if (io->s->op_thread) CancelSynchronousIo(io->s->op_thread);
615
+ }
616
+
617
+ /* Stream#_read(maxlen) -> binary String, or nil at EOF. */
618
+ static VALUE
619
+ stream_read(VALUE self, VALUE vmax)
620
+ {
621
+ stream_t *s = stream_live(self);
622
+ long max = NUM2LONG(vmax);
623
+ VALUE out;
624
+
625
+ if (s->writable) rb_raise(eModeError, "winproc: cannot read a write-only stream");
626
+ if (max < 1 || max > 0x7FFFFFFFL)
627
+ rb_raise(rb_eArgError, "winproc: read length must be 1..0x7FFFFFFF");
628
+
629
+ for (;;) {
630
+ sio_t io;
631
+ HANDLE dup = NULL;
632
+ char *buf;
633
+
634
+ if (s->closing) rb_raise(eClosed, "winproc: stream is closed");
635
+
636
+ /* publish op_thread (GVL held), malloc the private buffer */
637
+ if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
638
+ GetCurrentProcess(), &dup, 0, FALSE, DUPLICATE_SAME_ACCESS))
639
+ raise_gle("DuplicateHandle", GetLastError());
640
+ buf = (char *)malloc((size_t)max);
641
+ if (!buf) { CloseHandle(dup); rb_raise(rb_eNoMemError, "winproc: out of memory"); }
642
+
643
+ s->op_thread = dup;
644
+ InterlockedExchange(&s->inflight, 1);
645
+
646
+ io.s = s; io.buf = buf; io.cap = (DWORD)max; io.done = 0;
647
+ io.gle = 0; io.ok = FALSE; io.is_write = 0;
648
+ rb_thread_call_without_gvl(sio_fn, &io, sio_ubf, &io);
649
+
650
+ /* RETIRE resources before anything that can longjmp */
651
+ s->op_thread = NULL;
652
+ CloseHandle(dup);
653
+
654
+ if (io.ok) {
655
+ if (io.done == 0) { free(buf); rb_thread_check_ints(); continue; } /* E-27 */
656
+ out = rb_str_new(buf, (long)io.done);
657
+ free(buf);
658
+ rb_enc_associate(out, rb_ascii8bit_encoding());
659
+ return out;
660
+ }
661
+ /* failure */
662
+ {
663
+ DWORD gle = io.gle;
664
+ free(buf);
665
+ if (gle == ERROR_BROKEN_PIPE || gle == ERROR_PIPE_NOT_CONNECTED ||
666
+ gle == ERROR_NO_DATA || gle == ERROR_HANDLE_EOF)
667
+ return Qnil; /* clean EOF */
668
+ if (gle == ERROR_OPERATION_ABORTED) {
669
+ if (s->closing) rb_raise(eClosed, "winproc: stream is closed");
670
+ rb_thread_check_ints(); /* interrupt; may longjmp, else retry */
671
+ continue;
672
+ }
673
+ raise_gle("ReadFile", gle);
674
+ }
675
+ }
676
+ }
677
+
678
+ /* Stream#_write body + ensure: the private buffer is freed on EVERY exit,
679
+ * including a Thread#kill / Timeout longjmp out of rb_thread_check_ints. */
680
+ typedef struct { stream_t *s; char *buf; DWORD total; } swrite_args;
681
+
682
+ static VALUE
683
+ stream_write_body(VALUE vargs)
684
+ {
685
+ swrite_args *a = (swrite_args *)vargs;
686
+ stream_t *s = a->s;
687
+ for (;;) {
688
+ sio_t io;
689
+ HANDLE dup = NULL;
690
+
691
+ if (s->closing) rb_raise(eClosed, "winproc: stream is closed");
692
+ if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
693
+ GetCurrentProcess(), &dup, 0, FALSE, DUPLICATE_SAME_ACCESS))
694
+ raise_gle("DuplicateHandle", GetLastError());
695
+ s->op_thread = dup;
696
+ InterlockedExchange(&s->inflight, 1);
697
+
698
+ io.s = s; io.buf = a->buf; io.cap = a->total; io.done = 0;
699
+ io.gle = 0; io.ok = FALSE; io.is_write = 1;
700
+ rb_thread_call_without_gvl(sio_fn, &io, sio_ubf, &io);
701
+
702
+ s->op_thread = NULL;
703
+ CloseHandle(dup);
704
+
705
+ if (io.ok) return ULONG2NUM(io.done);
706
+ if (io.gle == ERROR_OPERATION_ABORTED) {
707
+ if (s->closing) rb_raise(eClosed, "winproc: stream is closed");
708
+ rb_thread_check_ints(); /* may longjmp; the ensure frees buf */
709
+ /* Note: a partial write before cancel is lost; retry writes the
710
+ * whole buffer again. Anonymous-pipe writes are not cancelled
711
+ * mid-stream in practice, so this is the documented residual (§11). */
712
+ continue;
713
+ }
714
+ if (io.gle == ERROR_BROKEN_PIPE || io.gle == ERROR_NO_DATA ||
715
+ io.gle == ERROR_PIPE_NOT_CONNECTED)
716
+ raise_code(eBrokenPipe, "WriteFile", io.gle);
717
+ raise_gle("WriteFile", io.gle);
718
+ }
719
+ }
720
+
721
+ static VALUE
722
+ stream_write_ensure(VALUE vargs)
723
+ {
724
+ free(((swrite_args *)vargs)->buf);
725
+ return Qnil;
726
+ }
727
+
728
+ /* Stream#_write(bytes) -> Integer bytes written (writes ALL bytes). */
729
+ static VALUE
730
+ stream_write(VALUE self, VALUE data)
731
+ {
732
+ stream_t *s = stream_live(self);
733
+ swrite_args a;
734
+ long len;
735
+
736
+ if (!s->writable) rb_raise(eModeError, "winproc: cannot write a read-only stream");
737
+ StringValue(data);
738
+ len = RSTRING_LEN(data);
739
+ if ((unsigned long long)len > 0x7FFFFFFFull)
740
+ rb_raise(rb_eArgError, "winproc: data too large (> 2 GiB)");
741
+ if (len == 0) return INT2FIX(0);
742
+
743
+ /* Copy bytes into a private buffer BEFORE releasing the GVL (no RSTRING_PTR
744
+ * across the no-GVL region). */
745
+ a.s = s;
746
+ a.buf = (char *)malloc((size_t)len);
747
+ if (!a.buf) rb_raise(rb_eNoMemError, "winproc: out of memory");
748
+ memcpy(a.buf, RSTRING_PTR(data), (size_t)len);
749
+ a.total = (DWORD)len;
750
+
751
+ return rb_ensure(stream_write_body, (VALUE)&a, stream_write_ensure, (VALUE)&a);
752
+ }
753
+
754
+ /* Stream#close — cancel any in-flight op (it raises Closed), then close. */
755
+ typedef struct { stream_t *s; HANDLE dup; } sclose_t;
756
+ static void *
757
+ sclose_spin_fn(void *p)
758
+ {
759
+ sclose_t *c = (sclose_t *)p;
760
+ while (c->s->inflight) {
761
+ if (c->dup) CancelSynchronousIo(c->dup);
762
+ SleepEx(1, FALSE);
763
+ }
764
+ return NULL;
765
+ }
766
+
767
+ static VALUE
768
+ stream_close(VALUE self)
769
+ {
770
+ stream_t *s = stream_get(self);
771
+ if (s->closed) return Qnil;
772
+ s->closing = 1;
773
+ if (s->inflight) {
774
+ sclose_t c;
775
+ HANDLE dup = NULL;
776
+ /* Take our OWN duplicate of op_thread under the GVL: it can never
777
+ * observe a retired/recycled handle, and keeps the thread object alive
778
+ * for the cancel below (§4, E-12). */
779
+ if (s->op_thread)
780
+ DuplicateHandle(GetCurrentProcess(), s->op_thread,
781
+ GetCurrentProcess(), &dup, 0, FALSE, DUPLICATE_SAME_ACCESS);
782
+ c.s = s; c.dup = dup;
783
+ rb_thread_call_without_gvl(sclose_spin_fn, &c, NULL, NULL);
784
+ if (dup) CloseHandle(dup);
785
+ }
786
+ if (s->h != INVALID_HANDLE_VALUE) { CloseHandle(s->h); s->h = INVALID_HANDLE_VALUE; }
787
+ s->closed = 1;
788
+ return Qnil;
789
+ }
790
+
791
+ static VALUE stream_closed_p(VALUE self) { return stream_get(self)->closed ? Qtrue : Qfalse; }
792
+ static VALUE stream_writable_p(VALUE self) { return stream_get(self)->writable ? Qtrue : Qfalse; }
793
+
794
+ /* =====================================================================
795
+ * Winproc::Job — anonymous job object + private IOCP
796
+ * ===================================================================== */
797
+
798
+ typedef struct {
799
+ HANDLE job;
800
+ HANDLE iocp; /* private completion port, associated at create */
801
+ volatile LONG waiting;/* wait_empty in-flight guard / E-30 drain flag */
802
+ volatile LONG closing;/* set by #close; woken waiter raises Closed */
803
+ int closed;
804
+ } job_t;
805
+
806
+ static void
807
+ job_free(void *p)
808
+ {
809
+ job_t *j = (job_t *)p;
810
+ /* Close the IOCP FIRST, then the job — closing the job last is what fires
811
+ * KILL_ON_JOB_CLOSE; this is the crash-safe tree reaper. */
812
+ if (j->iocp) CloseHandle(j->iocp);
813
+ if (j->job) CloseHandle(j->job);
814
+ xfree(j);
815
+ }
816
+
817
+ static const rb_data_type_t job_type = {
818
+ "Winproc::Job", { 0, job_free, 0, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
819
+ };
820
+
821
+ static VALUE
822
+ job_alloc(VALUE klass)
823
+ {
824
+ job_t *j;
825
+ VALUE obj = TypedData_Make_Struct(klass, job_t, &job_type, j);
826
+ j->job = NULL;
827
+ j->iocp = NULL;
828
+ j->waiting = 0;
829
+ j->closing = 0;
830
+ j->closed = 0;
831
+ return obj;
832
+ }
833
+
834
+ static job_t *
835
+ job_get(VALUE self)
836
+ {
837
+ job_t *j;
838
+ TypedData_Get_Struct(self, job_t, &job_type, j);
839
+ return j;
840
+ }
841
+
842
+ static job_t *
843
+ job_live(VALUE self)
844
+ {
845
+ job_t *j = job_get(self);
846
+ if (j->closed || !j->job) rb_raise(eClosed, "winproc: job is closed");
847
+ return j;
848
+ }
849
+
850
+ /* Job._create(kill_on_close, memory, process_memory, cpu_percent,
851
+ * active_processes, cpu_time_100ns) -> Job. nil = "unset". */
852
+ static VALUE
853
+ job_create(VALUE klass, VALUE v_kill, VALUE v_mem, VALUE v_pmem,
854
+ VALUE v_cpu, VALUE v_active, VALUE v_time)
855
+ {
856
+ VALUE obj = job_alloc(cJob);
857
+ job_t *j = job_get(obj);
858
+ JOBOBJECT_ASSOCIATE_COMPLETION_PORT acp;
859
+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION eli;
860
+ DWORD flags = 0;
861
+
862
+ j->job = CreateJobObjectW(NULL, NULL);
863
+ if (!j->job) raise_gle("CreateJobObject", GetLastError());
864
+
865
+ j->iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1);
866
+ if (!j->iocp) {
867
+ DWORD gle = GetLastError();
868
+ CloseHandle(j->job); j->job = NULL;
869
+ raise_gle("CreateIoCompletionPort", gle);
870
+ }
871
+
872
+ /* Associate the port while the job is provably inactive (E-14). */
873
+ memset(&acp, 0, sizeof(acp));
874
+ acp.CompletionKey = (PVOID)JOB_KEY;
875
+ acp.CompletionPort = j->iocp;
876
+ if (!SetInformationJobObject(j->job, JobObjectAssociateCompletionPortInformation,
877
+ &acp, sizeof(acp))) {
878
+ DWORD gle = GetLastError();
879
+ CloseHandle(j->iocp); j->iocp = NULL;
880
+ CloseHandle(j->job); j->job = NULL;
881
+ raise_gle("SetInformationJobObject(port)", gle);
882
+ }
883
+
884
+ /* One extended-limit Set combining KILL_ON_CLOSE / memory / time / active. */
885
+ memset(&eli, 0, sizeof(eli));
886
+ if (RTEST(v_kill)) flags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
887
+ if (!NIL_P(v_mem)) {
888
+ flags |= JOB_OBJECT_LIMIT_JOB_MEMORY;
889
+ eli.JobMemoryLimit = (SIZE_T)NUM2ULL(v_mem);
890
+ }
891
+ if (!NIL_P(v_pmem)) {
892
+ flags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY;
893
+ eli.ProcessMemoryLimit = (SIZE_T)NUM2ULL(v_pmem);
894
+ }
895
+ if (!NIL_P(v_active)) {
896
+ flags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
897
+ eli.BasicLimitInformation.ActiveProcessLimit = NUM2ULONG(v_active);
898
+ }
899
+ if (!NIL_P(v_time)) {
900
+ flags |= JOB_OBJECT_LIMIT_JOB_TIME;
901
+ eli.BasicLimitInformation.PerJobUserTimeLimit.QuadPart = (LONGLONG)NUM2LL(v_time);
902
+ }
903
+ eli.BasicLimitInformation.LimitFlags = flags;
904
+ if (flags && !SetInformationJobObject(j->job, JobObjectExtendedLimitInformation,
905
+ &eli, sizeof(eli))) {
906
+ DWORD gle = GetLastError();
907
+ /* job handle already owned by the TypedData; close() will fire
908
+ * KILL_ON_JOB_CLOSE (no processes yet) — consistent object. */
909
+ raise_gle("SetInformationJobObject(limits)", gle);
910
+ }
911
+
912
+ /* CPU rate control is the LAST Set so a raise (RDP/DFSS error 50 -> the
913
+ * Unsupported mapping) leaves a consistent, usable object (E-16). */
914
+ if (!NIL_P(v_cpu)) {
915
+ JOBOBJECT_CPU_RATE_CONTROL_INFORMATION crc;
916
+ memset(&crc, 0, sizeof(crc));
917
+ crc.ControlFlags = JOB_OBJECT_CPU_RATE_CONTROL_ENABLE |
918
+ JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP;
919
+ crc.CpuRate = (DWORD)(NUM2ULONG(v_cpu) * 100);
920
+ if (!SetInformationJobObject(j->job, JobObjectCpuRateControlInformation,
921
+ &crc, sizeof(crc)))
922
+ raise_gle("SetInformationJobObject(cpu)", GetLastError());
923
+ }
924
+ return obj;
925
+ }
926
+
927
+ static VALUE
928
+ job_assign(VALUE self, VALUE vprocess)
929
+ {
930
+ job_t *j = job_live(self);
931
+ process_t *pr;
932
+ if (!rb_typeddata_is_kind_of(vprocess, &process_type))
933
+ rb_raise(rb_eTypeError, "winproc: expected a Winproc::Process");
934
+ pr = process_get(vprocess);
935
+ if (pr->closed || pr->h == INVALID_HANDLE_VALUE)
936
+ rb_raise(eClosed, "winproc: process is closed");
937
+ if (!AssignProcessToJobObject(j->job, pr->h))
938
+ raise_gle("AssignProcessToJobObject", GetLastError());
939
+ return self;
940
+ }
941
+
942
+ static VALUE
943
+ job_terminate(int argc, VALUE *argv, VALUE self)
944
+ {
945
+ job_t *j = job_live(self);
946
+ VALUE vcode;
947
+ UINT code = 1;
948
+ rb_scan_args(argc, argv, "01", &vcode);
949
+ if (!NIL_P(vcode)) code = (UINT)NUM2UINT(vcode);
950
+ if (!TerminateJobObject(j->job, code))
951
+ raise_gle("TerminateJobObject", GetLastError());
952
+ return self;
953
+ }
954
+
955
+ static LONG
956
+ job_active_count(job_t *j)
957
+ {
958
+ JOBOBJECT_BASIC_ACCOUNTING_INFORMATION acct;
959
+ memset(&acct, 0, sizeof(acct));
960
+ if (!QueryInformationJobObject(j->job, JobObjectBasicAccountingInformation,
961
+ &acct, sizeof(acct), NULL))
962
+ raise_gle("QueryInformationJobObject", GetLastError());
963
+ return (LONG)acct.ActiveProcesses;
964
+ }
965
+
966
+ static VALUE
967
+ job_active_processes(VALUE self)
968
+ {
969
+ job_t *j = job_live(self);
970
+ return LONG2NUM(job_active_count(j));
971
+ }
972
+
973
+ /* --- interruptible, GVL-released wait on the job IOCP ---------------------- */
974
+
975
+ typedef struct {
976
+ job_t *j;
977
+ DWORD ms;
978
+ DWORD msg; /* lpNumberOfBytes (message id) */
979
+ ULONG_PTR key;
980
+ BOOL ok;
981
+ DWORD gle;
982
+ int timed_out;
983
+ } jwait_t;
984
+
985
+ static void *
986
+ jwait_fn(void *p)
987
+ {
988
+ jwait_t *w = (jwait_t *)p;
989
+ LPOVERLAPPED ov = NULL;
990
+ w->msg = 0; w->key = 0; w->gle = 0; w->timed_out = 0;
991
+ w->ok = GetQueuedCompletionStatus(w->j->iocp, &w->msg, &w->key, &ov, w->ms);
992
+ if (!w->ok) {
993
+ w->gle = GetLastError();
994
+ if (w->gle == WAIT_TIMEOUT) w->timed_out = 1;
995
+ }
996
+ return NULL;
997
+ }
998
+
999
+ static void
1000
+ jwait_ubf(void *p)
1001
+ {
1002
+ jwait_t *w = (jwait_t *)p;
1003
+ PostQueuedCompletionStatus(w->j->iocp, 0, WAKE_KEY, NULL);
1004
+ }
1005
+
1006
+ /* Body of Job#_wait_empty, run inside rb_ensure so the waiting guard is always
1007
+ * cleared — including a Thread#kill / Timeout longjmp out of
1008
+ * rb_thread_check_ints (otherwise #close would spin forever). */
1009
+ typedef struct { job_t *j; long long ms_in; } jwait_args;
1010
+
1011
+ static VALUE
1012
+ job_wait_empty_body(VALUE vargs)
1013
+ {
1014
+ jwait_args *a = (jwait_args *)vargs;
1015
+ job_t *j = a->j;
1016
+ int infinite = (a->ms_in < 0);
1017
+ ULONGLONG deadline;
1018
+
1019
+ /* short-circuit: already empty (accounting probe) */
1020
+ if (job_active_count(j) == 0) return Qtrue;
1021
+ deadline = infinite ? 0 : GetTickCount64() + (ULONGLONG)a->ms_in;
1022
+
1023
+ for (;;) {
1024
+ jwait_t w;
1025
+ DWORD slice;
1026
+ if (infinite) {
1027
+ slice = INFINITE;
1028
+ } else {
1029
+ ULONGLONG now = GetTickCount64();
1030
+ ULONGLONG left;
1031
+ if (now >= deadline) return Qfalse;
1032
+ left = deadline - now;
1033
+ /* Clamp below INFINITE; the deadline loop re-waits (E-Slice). */
1034
+ slice = (left >= (ULONGLONG)INFINITE) ? (INFINITE - 1) : (DWORD)left;
1035
+ }
1036
+ w.j = j; w.ms = slice;
1037
+ rb_thread_call_without_gvl(jwait_fn, &w, jwait_ubf, &w);
1038
+
1039
+ if (w.ok) {
1040
+ if (w.key == WAKE_KEY) {
1041
+ if (j->closing) rb_raise(eClosed, "winproc: job is closed");
1042
+ rb_thread_check_ints(); /* ubf interrupt; may longjmp */
1043
+ if (j->closing || j->closed || !j->iocp)
1044
+ rb_raise(eClosed, "winproc: job is closed");
1045
+ continue;
1046
+ }
1047
+ if (w.key == JOB_KEY && w.msg == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO) {
1048
+ /* re-verify accounting (stale-message guard, E-15) */
1049
+ if (job_active_count(j) == 0) return Qtrue;
1050
+ }
1051
+ /* other messages ignored */
1052
+ continue;
1053
+ }
1054
+ if (w.timed_out) {
1055
+ if (infinite) continue; /* spurious */
1056
+ { ULONGLONG now = GetTickCount64();
1057
+ if (now >= deadline) return Qfalse; }
1058
+ continue;
1059
+ }
1060
+ raise_gle("GetQueuedCompletionStatus", w.gle);
1061
+ }
1062
+ }
1063
+
1064
+ static VALUE
1065
+ job_wait_empty_ensure(VALUE vj)
1066
+ {
1067
+ job_t *j = (job_t *)vj;
1068
+ InterlockedExchange(&j->waiting, 0);
1069
+ return Qnil;
1070
+ }
1071
+
1072
+ /* Job#_wait_empty(ms) -> true (empty) | false (timeout). */
1073
+ static VALUE
1074
+ job_wait_empty(VALUE self, VALUE vms)
1075
+ {
1076
+ job_t *j = job_get(self);
1077
+ jwait_args a;
1078
+
1079
+ if (j->closed || !j->job) rb_raise(eClosed, "winproc: job is closed");
1080
+
1081
+ /* Convert the timeout BEFORE claiming the in-flight guard: a raise here
1082
+ * (e.g. a non-numeric ms) must not leave waiting==1 stuck — that would wedge
1083
+ * job_close's drain spin forever. NUM2LL is 64-bit so a multi-day ms never
1084
+ * overflows on LLP64 (where long is 32-bit); the wait loop clamps each slice
1085
+ * below INFINITE. (E-Slice / the close-wedge fix.) */
1086
+ a.j = j;
1087
+ a.ms_in = NUM2LL(vms);
1088
+
1089
+ if (InterlockedCompareExchange(&j->waiting, 1, 0) != 0)
1090
+ rb_raise(eError, "winproc: another wait_empty is already in flight");
1091
+
1092
+ return rb_ensure(job_wait_empty_body, (VALUE)&a, job_wait_empty_ensure, (VALUE)j);
1093
+ }
1094
+
1095
+ /* E-30 close: set closing, re-post WAKE until the waiter drains, then close
1096
+ * IOCP then job (kill-on-close fires here). */
1097
+ typedef struct { job_t *j; } jclose_t;
1098
+ static void *
1099
+ jclose_spin_fn(void *p)
1100
+ {
1101
+ job_t *j = ((jclose_t *)p)->j;
1102
+ while (j->waiting) {
1103
+ PostQueuedCompletionStatus(j->iocp, 0, WAKE_KEY, NULL);
1104
+ SleepEx(1, FALSE);
1105
+ }
1106
+ return NULL;
1107
+ }
1108
+
1109
+ static VALUE
1110
+ job_close(VALUE self)
1111
+ {
1112
+ job_t *j = job_get(self);
1113
+ if (j->closed) return Qnil;
1114
+ j->closing = 1;
1115
+ if (j->waiting && j->iocp) {
1116
+ jclose_t c; c.j = j;
1117
+ PostQueuedCompletionStatus(j->iocp, 0, WAKE_KEY, NULL);
1118
+ rb_thread_call_without_gvl(jclose_spin_fn, &c, NULL, NULL);
1119
+ }
1120
+ if (j->iocp) { CloseHandle(j->iocp); j->iocp = NULL; }
1121
+ if (j->job) { CloseHandle(j->job); j->job = NULL; } /* kill-on-close fires */
1122
+ j->closed = 1;
1123
+ return Qnil;
1124
+ }
1125
+
1126
+ static VALUE job_closed_p(VALUE self) { return job_get(self)->closed ? Qtrue : Qfalse; }
1127
+
1128
+ /* =====================================================================
1129
+ * Winproc::PTY — HPCON + (process/streams live as Ruby ivars)
1130
+ * ===================================================================== */
1131
+
1132
+ typedef struct {
1133
+ void *hpc; /* HPCON; NULL sentinel */
1134
+ int cols;
1135
+ int rows;
1136
+ int closed;
1137
+ } pty_t;
1138
+
1139
+ static void
1140
+ pty_free(void *p)
1141
+ {
1142
+ pty_t *t = (pty_t *)p;
1143
+ /* NEVER ClosePseudoConsole here — deliberately LEAK the HPCON (and its
1144
+ * conhost, until process exit). Sweep order is arbitrary, so the output
1145
+ * Stream may still be open/undrained; pre-24H2 ClosePseudoConsole would then
1146
+ * block INDEFINITELY inside GC with the GVL held. A bounded leak beats a
1147
+ * GVL-held hang. README: always close PTYs explicitly. */
1148
+ xfree(t);
1149
+ }
1150
+
1151
+ static const rb_data_type_t pty_type = {
1152
+ "Winproc::PTY", { 0, pty_free, 0, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
1153
+ };
1154
+
1155
+ static VALUE
1156
+ pty_alloc(VALUE klass)
1157
+ {
1158
+ pty_t *t;
1159
+ VALUE obj = TypedData_Make_Struct(klass, pty_t, &pty_type, t);
1160
+ t->hpc = NULL;
1161
+ t->cols = 0;
1162
+ t->rows = 0;
1163
+ t->closed = 0;
1164
+ return obj;
1165
+ }
1166
+
1167
+ static pty_t *
1168
+ pty_get(VALUE self)
1169
+ {
1170
+ pty_t *t;
1171
+ TypedData_Get_Struct(self, pty_t, &pty_type, t);
1172
+ return t;
1173
+ }
1174
+
1175
+ static pty_t *
1176
+ pty_live(VALUE self)
1177
+ {
1178
+ pty_t *t = pty_get(self);
1179
+ if (t->closed || !t->hpc) rb_raise(eClosed, "winproc: pty is closed");
1180
+ return t;
1181
+ }
1182
+
1183
+ static VALUE
1184
+ pty_initialize(int argc, VALUE *argv, VALUE self)
1185
+ {
1186
+ (void)argc; (void)argv; (void)self;
1187
+ rb_raise(eError, "winproc: pseudoconsoles are created by Winproc.pty");
1188
+ return self;
1189
+ }
1190
+
1191
+ static VALUE
1192
+ pty_resize(VALUE self, VALUE vcols, VALUE vrows)
1193
+ {
1194
+ pty_t *t = pty_live(self);
1195
+ int cols = NUM2INT(vcols), rows = NUM2INT(vrows);
1196
+ COORD size;
1197
+ HRESULT hr;
1198
+ if (cols < 1 || cols > 0x7FFF || rows < 1 || rows > 0x7FFF)
1199
+ rb_raise(rb_eArgError, "winproc: cols/rows must be 1..32767");
1200
+ size.X = (SHORT)cols; size.Y = (SHORT)rows;
1201
+ hr = p_ResizePseudoConsole(t->hpc, size);
1202
+ if (FAILED(hr)) raise_code(eOSError, "ResizePseudoConsole", (DWORD)(hr & 0xFFFF));
1203
+ t->cols = cols; t->rows = rows;
1204
+ return self;
1205
+ }
1206
+
1207
+ static VALUE pty_cols(VALUE self) { return INT2NUM(pty_get(self)->cols); }
1208
+ static VALUE pty_rows(VALUE self) { return INT2NUM(pty_get(self)->rows); }
1209
+
1210
+ /* PTY#_close_pty — no-GVL ClosePseudoConsole (the Ruby layer has already closed
1211
+ * the output Stream, so this cannot deadlock; bounded, no cancel API). */
1212
+ typedef struct { void *hpc; } pclosepty_t;
1213
+ static void *
1214
+ pty_close_fn(void *p)
1215
+ {
1216
+ p_ClosePseudoConsole(((pclosepty_t *)p)->hpc);
1217
+ return NULL;
1218
+ }
1219
+
1220
+ static VALUE
1221
+ pty_close_pty(VALUE self)
1222
+ {
1223
+ pty_t *t = pty_get(self);
1224
+ if (t->closed || !t->hpc) { t->closed = 1; return Qnil; }
1225
+ {
1226
+ pclosepty_t c; c.hpc = t->hpc;
1227
+ rb_thread_call_without_gvl(pty_close_fn, &c, NULL, NULL);
1228
+ }
1229
+ t->hpc = NULL;
1230
+ t->closed = 1;
1231
+ return Qnil;
1232
+ }
1233
+
1234
+ static VALUE pty_closed_p(VALUE self) { return pty_get(self)->closed ? Qtrue : Qfalse; }
1235
+
1236
+ /* =====================================================================
1237
+ * Winproc._spawn — the one CreateProcessW path (redirected & ConPTY modes)
1238
+ * ===================================================================== */
1239
+
1240
+ /* stdio slot kinds, kept in sync with the Ruby layer's encoding. */
1241
+ enum { SLOT_INHERIT = 0, SLOT_NULL = 1, SLOT_PIPE = 2, SLOT_IO = 3, SLOT_MERGE = 4 };
1242
+
1243
+ /* Everything acquired by _spawn lives here so a single fail: path cleans up. */
1244
+ typedef struct {
1245
+ /* inputs (borrowed; freed on both paths) */
1246
+ WCHAR *app;
1247
+ WCHAR *cmdline; /* writable copy (CreateProcessW may modify) */
1248
+ WCHAR *cwd;
1249
+ WCHAR *envblock;
1250
+
1251
+ /* attribute list */
1252
+ LPPROC_THREAD_ATTRIBUTE_LIST attr;
1253
+ SIZE_T attr_size;
1254
+ int attr_inited;
1255
+
1256
+ /* HANDLE_LIST (≤3, deduped) + job + hpcon — must outlive CreateProcess AND
1257
+ * DeleteProcThreadAttributeList (E-4). */
1258
+ HANDLE hlist[3];
1259
+ DWORD hcount;
1260
+ HANDLE hjob;
1261
+ void *hpcon;
1262
+
1263
+ /* per-slot child handles we created (to close after CreateProcess) */
1264
+ HANDLE child_in, child_out, child_err;
1265
+ /* parent ends to hand to Stream objects */
1266
+ HANDLE par_in_w, par_out_r, par_err_r;
1267
+ /* PTY: our pipe ends + pty-side ends */
1268
+ HANDLE pty_in_w, pty_out_r, pty_in_r, pty_out_w;
1269
+
1270
+ PROCESS_INFORMATION pi;
1271
+ int created; /* CreateProcessW succeeded */
1272
+
1273
+ /* pre-allocated Ruby objects (E-31): nothing raise-able after CreateProcess */
1274
+ VALUE proc_obj;
1275
+ VALUE in_obj, out_obj, err_obj; /* Streams for :pipe slots (redirected) */
1276
+ VALUE pty_obj, pty_in_obj, pty_out_obj;
1277
+ HANDLE cancel_event;
1278
+ } spawn_ctx;
1279
+
1280
+ static void
1281
+ spawn_cleanup_partial(spawn_ctx *c)
1282
+ {
1283
+ /* close created-but-unowned handles; called on the fail path */
1284
+ if (c->attr_inited) DeleteProcThreadAttributeList(c->attr);
1285
+ if (c->attr) free(c->attr);
1286
+ if (c->child_in) CloseHandle(c->child_in);
1287
+ if (c->child_out) CloseHandle(c->child_out);
1288
+ if (c->child_err && c->child_err != c->child_out) CloseHandle(c->child_err);
1289
+ if (c->par_in_w) CloseHandle(c->par_in_w);
1290
+ if (c->par_out_r) CloseHandle(c->par_out_r);
1291
+ if (c->par_err_r) CloseHandle(c->par_err_r);
1292
+ if (c->pty_in_w) CloseHandle(c->pty_in_w);
1293
+ if (c->pty_out_r) CloseHandle(c->pty_out_r);
1294
+ if (c->pty_in_r) CloseHandle(c->pty_in_r);
1295
+ if (c->pty_out_w) CloseHandle(c->pty_out_w);
1296
+ if (c->hpcon && p_ClosePseudoConsole) p_ClosePseudoConsole(c->hpcon);
1297
+ if (c->cancel_event) CloseHandle(c->cancel_event);
1298
+ if (c->app) xfree(c->app);
1299
+ if (c->cmdline) xfree(c->cmdline);
1300
+ if (c->cwd) xfree(c->cwd);
1301
+ if (c->envblock) xfree(c->envblock);
1302
+ }
1303
+
1304
+ /* Make an inheritable NUL handle (fallback for a parent without std handles). */
1305
+ static HANDLE
1306
+ open_nul_inheritable(void)
1307
+ {
1308
+ SECURITY_ATTRIBUTES sa;
1309
+ sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE;
1310
+ return CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE,
1311
+ FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, NULL);
1312
+ }
1313
+
1314
+ /* Duplicate a std handle as inheritable, falling back to NUL (E-7). std is one
1315
+ * of STD_INPUT_HANDLE/STD_OUTPUT_HANDLE/STD_ERROR_HANDLE. */
1316
+ static HANDLE
1317
+ inherit_std_or_nul(DWORD std)
1318
+ {
1319
+ HANDLE src = GetStdHandle(std);
1320
+ HANDLE dup = NULL;
1321
+ if (src == NULL || src == INVALID_HANDLE_VALUE)
1322
+ return open_nul_inheritable();
1323
+ if (DuplicateHandle(GetCurrentProcess(), src, GetCurrentProcess(), &dup,
1324
+ 0, TRUE, DUPLICATE_SAME_ACCESS))
1325
+ return dup;
1326
+ return open_nul_inheritable();
1327
+ }
1328
+
1329
+ /* Build one redirected stdio slot. spec_kind is SLOT_*; for SLOT_IO, vio is the
1330
+ * Ruby IO. Sets *child (the inheritable child end) and, for SLOT_PIPE, *parent
1331
+ * (our end) + *parent_writable. */
1332
+ static void
1333
+ build_slot(int spec_kind, VALUE vio, int is_input,
1334
+ HANDLE *child, HANDLE *parent, int *parent_writable)
1335
+ {
1336
+ *child = NULL; *parent = NULL; *parent_writable = 0;
1337
+ if (spec_kind == SLOT_PIPE) {
1338
+ HANDLE r = NULL, w = NULL;
1339
+ if (!CreatePipe(&r, &w, NULL, 0)) raise_gle("CreatePipe", GetLastError());
1340
+ if (is_input) {
1341
+ /* child reads r; parent writes w */
1342
+ SetHandleInformation(r, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
1343
+ *child = r; *parent = w; *parent_writable = 1;
1344
+ } else {
1345
+ /* child writes w; parent reads r */
1346
+ SetHandleInformation(w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
1347
+ *child = w; *parent = r; *parent_writable = 0;
1348
+ }
1349
+ } else if (spec_kind == SLOT_NULL) {
1350
+ HANDLE nul = open_nul_inheritable();
1351
+ if (nul == INVALID_HANDLE_VALUE) raise_gle("CreateFile(NUL)", GetLastError());
1352
+ *child = nul;
1353
+ } else if (spec_kind == SLOT_IO) {
1354
+ int fd = rb_io_descriptor(vio);
1355
+ HANDLE src = (HANDLE)_get_osfhandle(fd);
1356
+ HANDLE dup = NULL;
1357
+ if (src == INVALID_HANDLE_VALUE)
1358
+ rb_raise(rb_eArgError, "winproc: IO has no OS handle");
1359
+ if (!DuplicateHandle(GetCurrentProcess(), src, GetCurrentProcess(), &dup,
1360
+ 0, TRUE, DUPLICATE_SAME_ACCESS))
1361
+ raise_gle("DuplicateHandle", GetLastError());
1362
+ *child = dup;
1363
+ } else { /* SLOT_INHERIT in redirected mode: duplicate the parent's std */
1364
+ *child = inherit_std_or_nul(is_input ? STD_INPUT_HANDLE :
1365
+ /* caller passes the right std via vio==stdno marker */
1366
+ (DWORD)NUM2ULONG(vio));
1367
+ }
1368
+ }
1369
+
1370
+ /* Add h to the deduped HANDLE_LIST. */
1371
+ static void
1372
+ hlist_add(spawn_ctx *c, HANDLE h)
1373
+ {
1374
+ DWORD i;
1375
+ if (!h || h == INVALID_HANDLE_VALUE) return;
1376
+ for (i = 0; i < c->hcount; i++) if (c->hlist[i] == h) return;
1377
+ if (c->hcount < 3) c->hlist[c->hcount++] = h;
1378
+ }
1379
+
1380
+ /*
1381
+ * Winproc._spawn(app, cmdline, cwd, envblock, flags, job, pty_cols, pty_rows,
1382
+ * in_kind, in_io, out_kind, out_io, err_kind, err_io)
1383
+ *
1384
+ * flags: the extra CreationFlags bits the Ruby layer computed
1385
+ * (CREATE_NEW_PROCESS_GROUP / CREATE_NO_WINDOW). The base flags
1386
+ * (CREATE_UNICODE_ENVIRONMENT|EXTENDED_STARTUPINFO_PRESENT) are added here.
1387
+ * pty_cols > 0 selects ConPTY mode (redirection kwargs are absent in that path).
1388
+ * Returns the Process (redirected) or the PTY (pty mode).
1389
+ */
1390
+ static VALUE
1391
+ winproc_spawn(int argc, VALUE *argv, VALUE self)
1392
+ {
1393
+ spawn_ctx c;
1394
+ VALUE v_app, v_cmd, v_cwd, v_env, v_flags, v_job,
1395
+ v_pcols, v_prows, v_ik, v_iio, v_ok, v_oio, v_ek, v_eio;
1396
+ DWORD base_flags, extra_flags;
1397
+ int pty_mode;
1398
+ BOOL inherit;
1399
+ STARTUPINFOEXW si;
1400
+ DWORD attr_count = 0;
1401
+ DWORD need;
1402
+ process_t *pr;
1403
+ DWORD gle;
1404
+
1405
+ (void)self;
1406
+ if (argc != 14) rb_raise(rb_eArgError, "winproc: _spawn arity");
1407
+ v_app=argv[0]; v_cmd=argv[1]; v_cwd=argv[2]; v_env=argv[3]; v_flags=argv[4];
1408
+ v_job=argv[5]; v_pcols=argv[6]; v_prows=argv[7];
1409
+ v_ik=argv[8]; v_iio=argv[9]; v_ok=argv[10]; v_oio=argv[11]; v_ek=argv[12]; v_eio=argv[13];
1410
+
1411
+ memset(&c, 0, sizeof(c));
1412
+ c.proc_obj = c.in_obj = c.out_obj = c.err_obj = Qnil;
1413
+ c.pty_obj = c.pty_in_obj = c.pty_out_obj = Qnil;
1414
+ c.pi.hProcess = NULL; c.pi.hThread = NULL;
1415
+
1416
+ extra_flags = (DWORD)NUM2ULONG(v_flags);
1417
+ pty_mode = (NUM2INT(v_pcols) > 0);
1418
+
1419
+ /* ---- coerce strings up front (TypeError here: no HANDLE exists yet) ---- */
1420
+ if (!NIL_P(v_app)) c.app = to_wide(v_app);
1421
+ c.cmdline = to_wide(v_cmd); /* writable private copy (E-1) */
1422
+ if (!NIL_P(v_cwd)) c.cwd = to_wide(v_cwd);
1423
+ if (!NIL_P(v_env)) c.envblock = env_to_wide(v_env);
1424
+ if (!NIL_P(v_job)) {
1425
+ job_t *j;
1426
+ if (!rb_typeddata_is_kind_of(v_job, &job_type)) {
1427
+ spawn_cleanup_partial(&c);
1428
+ rb_raise(rb_eTypeError, "winproc: job: must be a Winproc::Job");
1429
+ }
1430
+ j = job_get(v_job);
1431
+ if (j->closed || !j->job) {
1432
+ spawn_cleanup_partial(&c);
1433
+ rb_raise(eClosed, "winproc: job is closed");
1434
+ }
1435
+ c.hjob = j->job;
1436
+ }
1437
+
1438
+ memset(&si, 0, sizeof(si));
1439
+ si.StartupInfo.cb = sizeof(STARTUPINFOEXW);
1440
+ base_flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT;
1441
+
1442
+ if (pty_mode) {
1443
+ COORD size;
1444
+ HRESULT hr;
1445
+ /* ConPTY availability is checked in Ruby (pty_available?); guard anyway. */
1446
+ if (!p_CreatePseudoConsole) {
1447
+ spawn_cleanup_partial(&c);
1448
+ raise_code(eUnsupported, "CreatePseudoConsole", ERROR_PROC_NOT_FOUND);
1449
+ }
1450
+ size.X = (SHORT)NUM2INT(v_pcols); size.Y = (SHORT)NUM2INT(v_prows);
1451
+ /* two pipes */
1452
+ if (!CreatePipe(&c.pty_in_r, &c.pty_in_w, NULL, 0)) {
1453
+ gle = GetLastError(); spawn_cleanup_partial(&c); raise_gle("CreatePipe", gle);
1454
+ }
1455
+ if (!CreatePipe(&c.pty_out_r, &c.pty_out_w, NULL, 0)) {
1456
+ gle = GetLastError(); spawn_cleanup_partial(&c); raise_gle("CreatePipe", gle);
1457
+ }
1458
+ hr = p_CreatePseudoConsole(size, c.pty_in_r, c.pty_out_w, 0, &c.hpcon);
1459
+ if (FAILED(hr)) {
1460
+ spawn_cleanup_partial(&c);
1461
+ raise_code(eOSError, "CreatePseudoConsole", (DWORD)(hr & 0xFFFF));
1462
+ }
1463
+ /* The PTY-side ends (pty_in_r / pty_out_w) are closed AFTER CreateProcess
1464
+ * (E-8 / creating-a-pseudoconsole-session: "Upon completion of the
1465
+ * CreateProcess call ... the handles given during creation should be
1466
+ * freed from this process"). Closing them BEFORE CreateProcess can leave
1467
+ * the child unattached. They are tracked in the ctx so the fail path also
1468
+ * closes them. */
1469
+ attr_count = 1 + (c.hjob ? 1 : 0);
1470
+ inherit = FALSE; /* no STARTF_USESTDHANDLES, no HANDLE_LIST (E-9) */
1471
+ } else {
1472
+ /* ---- redirected/inherit mode: build the 3 child stdio handles ---- */
1473
+ int ik = NUM2INT(v_ik), ok = NUM2INT(v_ok), ek = NUM2INT(v_ek);
1474
+ int any_redirect = (ik != SLOT_INHERIT || ok != SLOT_INHERIT || ek != SLOT_INHERIT);
1475
+ if (any_redirect) {
1476
+ int pw;
1477
+ HANDLE child, parent;
1478
+ /* stdin */
1479
+ if (ik == SLOT_INHERIT)
1480
+ child = inherit_std_or_nul(STD_INPUT_HANDLE), parent = NULL;
1481
+ else { build_slot(ik, v_iio, 1, &child, &parent, &pw); }
1482
+ c.child_in = child;
1483
+ if (ik == SLOT_PIPE) c.par_in_w = parent;
1484
+ /* stdout */
1485
+ if (ok == SLOT_INHERIT)
1486
+ child = inherit_std_or_nul(STD_OUTPUT_HANDLE), parent = NULL;
1487
+ else { build_slot(ok, v_oio, 0, &child, &parent, &pw); }
1488
+ c.child_out = child;
1489
+ if (ok == SLOT_PIPE) c.par_out_r = parent;
1490
+ /* stderr (SLOT_MERGE => reuse stdout child handle) */
1491
+ if (ek == SLOT_MERGE) {
1492
+ c.child_err = c.child_out;
1493
+ } else if (ek == SLOT_INHERIT) {
1494
+ c.child_err = inherit_std_or_nul(STD_ERROR_HANDLE);
1495
+ } else {
1496
+ build_slot(ek, v_eio, 0, &child, &parent, &pw);
1497
+ c.child_err = child;
1498
+ if (ek == SLOT_PIPE) c.par_err_r = parent;
1499
+ }
1500
+
1501
+ si.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
1502
+ si.StartupInfo.hStdInput = c.child_in;
1503
+ si.StartupInfo.hStdOutput = c.child_out;
1504
+ si.StartupInfo.hStdError = c.child_err;
1505
+ hlist_add(&c, c.child_in);
1506
+ hlist_add(&c, c.child_out);
1507
+ hlist_add(&c, c.child_err);
1508
+ inherit = TRUE;
1509
+ attr_count = 1 + (c.hjob ? 1 : 0); /* HANDLE_LIST + maybe JOB_LIST */
1510
+ } else {
1511
+ inherit = FALSE;
1512
+ attr_count = (c.hjob ? 1 : 0);
1513
+ }
1514
+ }
1515
+
1516
+ /* ---- attribute list (E-4 double-call idiom) ---- */
1517
+ if (attr_count > 0) {
1518
+ need = 0;
1519
+ InitializeProcThreadAttributeList(NULL, attr_count, 0, &c.attr_size);
1520
+ c.attr = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(c.attr_size);
1521
+ if (!c.attr) { spawn_cleanup_partial(&c); rb_raise(rb_eNoMemError, "winproc: out of memory"); }
1522
+ if (!InitializeProcThreadAttributeList(c.attr, attr_count, 0, &c.attr_size)) {
1523
+ gle = GetLastError(); spawn_cleanup_partial(&c); raise_gle("InitializeProcThreadAttributeList", gle);
1524
+ }
1525
+ c.attr_inited = 1;
1526
+ (void)need;
1527
+
1528
+ if (pty_mode) {
1529
+ if (!UpdateProcThreadAttribute(c.attr, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
1530
+ c.hpcon, sizeof(c.hpcon), NULL, NULL)) {
1531
+ gle = GetLastError(); spawn_cleanup_partial(&c); raise_gle("UpdateProcThreadAttribute(pty)", gle);
1532
+ }
1533
+ } else if (c.hcount > 0) {
1534
+ if (!UpdateProcThreadAttribute(c.attr, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
1535
+ c.hlist, c.hcount * sizeof(HANDLE), NULL, NULL)) {
1536
+ gle = GetLastError(); spawn_cleanup_partial(&c); raise_gle("UpdateProcThreadAttribute(handles)", gle);
1537
+ }
1538
+ }
1539
+ if (c.hjob) {
1540
+ if (!UpdateProcThreadAttribute(c.attr, 0, PROC_THREAD_ATTRIBUTE_JOB_LIST,
1541
+ &c.hjob, sizeof(HANDLE), NULL, NULL)) {
1542
+ gle = GetLastError(); spawn_cleanup_partial(&c); raise_gle("UpdateProcThreadAttribute(job)", gle);
1543
+ }
1544
+ }
1545
+ si.lpAttributeList = c.attr;
1546
+ }
1547
+
1548
+ /* ---- pre-allocate every raise-capable resource (E-31) ---- */
1549
+ c.cancel_event = CreateEventW(NULL, TRUE, FALSE, NULL);
1550
+ if (!c.cancel_event) { gle = GetLastError(); spawn_cleanup_partial(&c); raise_gle("CreateEvent", gle); }
1551
+ c.proc_obj = process_alloc(cProcess);
1552
+ if (pty_mode) {
1553
+ c.pty_obj = pty_alloc(cPTY);
1554
+ c.pty_in_obj = stream_alloc(cStream); /* writable */
1555
+ c.pty_out_obj = stream_alloc(cStream); /* readable */
1556
+ rb_ivar_set(c.pty_obj, rb_intern("@process"), c.proc_obj);
1557
+ rb_ivar_set(c.pty_obj, rb_intern("@input"), c.pty_in_obj);
1558
+ rb_ivar_set(c.pty_obj, rb_intern("@output"), c.pty_out_obj);
1559
+ } else {
1560
+ if (!NIL_P(v_ik) && NUM2INT(v_ik) == SLOT_PIPE) c.in_obj = stream_alloc(cStream);
1561
+ if (NUM2INT(v_ok) == SLOT_PIPE) c.out_obj = stream_alloc(cStream);
1562
+ if (NUM2INT(v_ek) == SLOT_PIPE) c.err_obj = stream_alloc(cStream);
1563
+ if (!NIL_P(c.in_obj)) rb_ivar_set(c.proc_obj, rb_intern("@stdin"), c.in_obj);
1564
+ if (!NIL_P(c.out_obj)) rb_ivar_set(c.proc_obj, rb_intern("@stdout"), c.out_obj);
1565
+ if (!NIL_P(c.err_obj)) rb_ivar_set(c.proc_obj, rb_intern("@stderr"), c.err_obj);
1566
+ }
1567
+
1568
+ /* ---- CreateProcessW (NO Ruby allocation past this point) ---- */
1569
+ if (!CreateProcessW(c.app, c.cmdline, NULL, NULL, inherit,
1570
+ base_flags | extra_flags,
1571
+ c.envblock, c.cwd, &si.StartupInfo, &c.pi)) {
1572
+ gle = GetLastError();
1573
+ spawn_cleanup_partial(&c);
1574
+ raise_gle("CreateProcessW", gle);
1575
+ }
1576
+ c.created = 1;
1577
+
1578
+ /* tidy attr list + child-side handles + pi.hThread (plain CloseHandle) */
1579
+ if (c.attr_inited) { DeleteProcThreadAttributeList(c.attr); c.attr_inited = 0; }
1580
+ if (c.attr) { free(c.attr); c.attr = NULL; }
1581
+ if (c.pi.hThread) { CloseHandle(c.pi.hThread); c.pi.hThread = NULL; }
1582
+ if (c.child_in) { CloseHandle(c.child_in); c.child_in = NULL; }
1583
+ if (c.child_err && c.child_err != c.child_out) { CloseHandle(c.child_err); c.child_err = NULL; }
1584
+ if (c.child_out) { CloseHandle(c.child_out); c.child_out = NULL; c.child_err = NULL; }
1585
+ /* PTY-side ends: now that the child is attached, drop our refs so our read
1586
+ * end can detect a broken channel (EOF) when the PTY tears down. */
1587
+ if (c.pty_in_r) { CloseHandle(c.pty_in_r); c.pty_in_r = NULL; }
1588
+ if (c.pty_out_w) { CloseHandle(c.pty_out_w); c.pty_out_w = NULL; }
1589
+
1590
+ /* free wide strings */
1591
+ if (c.app) { xfree(c.app); c.app = NULL; }
1592
+ if (c.cmdline) { xfree(c.cmdline); c.cmdline = NULL; }
1593
+ if (c.cwd) { xfree(c.cwd); c.cwd = NULL; }
1594
+ if (c.envblock) { xfree(c.envblock); c.envblock = NULL; }
1595
+
1596
+ /* ---- populate pre-allocated structs (plain stores; cannot raise) ---- */
1597
+ pr = process_get(c.proc_obj);
1598
+ pr->h = c.pi.hProcess;
1599
+ pr->cancel_event = c.cancel_event;
1600
+ pr->pid = c.pi.dwProcessId;
1601
+ c.cancel_event = NULL; /* ownership moved to the Process */
1602
+
1603
+ if (pty_mode) {
1604
+ pty_t *t = pty_get(c.pty_obj);
1605
+ stream_t *sin = stream_get(c.pty_in_obj);
1606
+ stream_t *sout = stream_get(c.pty_out_obj);
1607
+ t->hpc = c.hpcon; c.hpcon = NULL;
1608
+ t->cols = NUM2INT(v_pcols); t->rows = NUM2INT(v_prows);
1609
+ sin->h = c.pty_in_w; sin->writable = 1; c.pty_in_w = NULL;
1610
+ sout->h = c.pty_out_r; sout->writable = 0; c.pty_out_r = NULL;
1611
+ return c.pty_obj;
1612
+ } else {
1613
+ if (!NIL_P(c.in_obj)) { stream_t *s = stream_get(c.in_obj); s->h = c.par_in_w; s->writable = 1; c.par_in_w = NULL; }
1614
+ if (!NIL_P(c.out_obj)) { stream_t *s = stream_get(c.out_obj); s->h = c.par_out_r; s->writable = 0; c.par_out_r = NULL; }
1615
+ if (!NIL_P(c.err_obj)) { stream_t *s = stream_get(c.err_obj); s->h = c.par_err_r; s->writable = 0; c.par_err_r = NULL; }
1616
+ return c.proc_obj;
1617
+ }
1618
+ }
1619
+
1620
+ /* =====================================================================
1621
+ * Elevation helpers
1622
+ * ===================================================================== */
1623
+
1624
+ static VALUE
1625
+ winproc_elevated_p(VALUE self)
1626
+ {
1627
+ HANDLE tok = NULL;
1628
+ TOKEN_ELEVATION te;
1629
+ DWORD n = 0;
1630
+ BOOL elevated;
1631
+ (void)self;
1632
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tok))
1633
+ raise_gle("OpenProcessToken", GetLastError());
1634
+ if (!GetTokenInformation(tok, TokenElevation, &te, sizeof(te), &n)) {
1635
+ DWORD gle = GetLastError(); CloseHandle(tok);
1636
+ raise_gle("GetTokenInformation(Elevation)", gle);
1637
+ }
1638
+ elevated = (te.TokenIsElevated != 0);
1639
+ CloseHandle(tok);
1640
+ return elevated ? Qtrue : Qfalse;
1641
+ }
1642
+
1643
+ static VALUE
1644
+ winproc_admin_p(VALUE self)
1645
+ {
1646
+ HANDLE tok = NULL, linked = NULL, check = NULL;
1647
+ TOKEN_ELEVATION_TYPE et;
1648
+ DWORD n = 0;
1649
+ PSID sid = NULL;
1650
+ SID_IDENTIFIER_AUTHORITY nt = SECURITY_NT_AUTHORITY;
1651
+ BOOL member = FALSE;
1652
+ DWORD gle;
1653
+ (void)self;
1654
+
1655
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tok))
1656
+ raise_gle("OpenProcessToken", GetLastError());
1657
+ if (!GetTokenInformation(tok, TokenElevationType, &et, sizeof(et), &n)) {
1658
+ gle = GetLastError(); CloseHandle(tok); raise_gle("GetTokenInformation(ElevationType)", gle);
1659
+ }
1660
+ if (et == TokenElevationTypeLimited) {
1661
+ TOKEN_LINKED_TOKEN lt;
1662
+ memset(&lt, 0, sizeof(lt));
1663
+ if (!GetTokenInformation(tok, TokenLinkedToken, &lt, sizeof(lt), &n)) {
1664
+ gle = GetLastError(); CloseHandle(tok); raise_gle("GetTokenInformation(LinkedToken)", gle);
1665
+ }
1666
+ linked = lt.LinkedToken; /* impersonation token already */
1667
+ check = linked;
1668
+ } else {
1669
+ check = NULL; /* NULL => effective token */
1670
+ }
1671
+ CloseHandle(tok);
1672
+
1673
+ if (!AllocateAndInitializeSid(&nt, 2, SECURITY_BUILTIN_DOMAIN_RID,
1674
+ DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid)) {
1675
+ gle = GetLastError(); if (linked) CloseHandle(linked); raise_gle("AllocateAndInitializeSid", gle);
1676
+ }
1677
+ if (!CheckTokenMembership(check, sid, &member)) {
1678
+ gle = GetLastError(); FreeSid(sid); if (linked) CloseHandle(linked);
1679
+ raise_gle("CheckTokenMembership", gle);
1680
+ }
1681
+ FreeSid(sid);
1682
+ if (linked) CloseHandle(linked);
1683
+ return member ? Qtrue : Qfalse;
1684
+ }
1685
+
1686
+ static VALUE
1687
+ winproc_pty_available_p(VALUE self)
1688
+ {
1689
+ (void)self;
1690
+ return (p_CreatePseudoConsole && p_ResizePseudoConsole && p_ClosePseudoConsole)
1691
+ ? Qtrue : Qfalse;
1692
+ }
1693
+
1694
+ /* --- runas: ShellExecuteExW "runas" with COM init, GVL released ----------- */
1695
+
1696
+ typedef struct {
1697
+ WCHAR *file;
1698
+ WCHAR *params;
1699
+ WCHAR *dir;
1700
+ int nshow;
1701
+ BOOL ok;
1702
+ DWORD gle;
1703
+ HANDLE hproc;
1704
+ } runas_t;
1705
+
1706
+ static void *
1707
+ runas_fn(void *p)
1708
+ {
1709
+ runas_t *r = (runas_t *)p;
1710
+ SHELLEXECUTEINFOW sei;
1711
+ HRESULT hr;
1712
+ int balance_com = 0;
1713
+
1714
+ hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
1715
+ if (hr == S_OK || hr == S_FALSE) balance_com = 1;
1716
+ /* RPC_E_CHANGED_MODE: COM already initialized differently; proceed, no uninit */
1717
+
1718
+ memset(&sei, 0, sizeof(sei));
1719
+ sei.cbSize = sizeof(sei);
1720
+ sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC;
1721
+ sei.lpVerb = L"runas";
1722
+ sei.lpFile = r->file;
1723
+ sei.lpParameters = r->params;
1724
+ sei.lpDirectory = r->dir;
1725
+ sei.nShow = r->nshow;
1726
+
1727
+ r->ok = ShellExecuteExW(&sei);
1728
+ if (!r->ok) r->gle = GetLastError(); /* decode from GLE, never hInstApp (E-20) */
1729
+ else r->hproc = sei.hProcess; /* may be NULL even on success */
1730
+
1731
+ if (balance_com) CoUninitialize();
1732
+ return NULL;
1733
+ }
1734
+
1735
+ /* Winproc.__runas(exe_w8, params_w8, cwd_w8_or_nil, sw_int) -> Process | nil */
1736
+ static VALUE
1737
+ winproc_runas(VALUE self, VALUE vexe, VALUE vparams, VALUE vcwd, VALUE vsw)
1738
+ {
1739
+ runas_t r;
1740
+ VALUE obj;
1741
+ process_t *pr;
1742
+ (void)self;
1743
+
1744
+ memset(&r, 0, sizeof(r));
1745
+ r.file = to_wide(vexe);
1746
+ r.params = NIL_P(vparams) ? NULL : to_wide(vparams);
1747
+ r.dir = NIL_P(vcwd) ? NULL : to_wide(vcwd);
1748
+ r.nshow = NUM2INT(vsw);
1749
+
1750
+ rb_thread_call_without_gvl(runas_fn, &r, NULL, NULL);
1751
+
1752
+ {
1753
+ DWORD gle = r.gle;
1754
+ BOOL ok = r.ok;
1755
+ HANDLE hproc = r.hproc;
1756
+ if (r.file) xfree(r.file);
1757
+ if (r.params) xfree(r.params);
1758
+ if (r.dir) xfree(r.dir);
1759
+ if (!ok) raise_gle("ShellExecuteExW", gle); /* 1223 -> Canceled */
1760
+ if (hproc == NULL) return Qnil; /* launched without a handle */
1761
+
1762
+ obj = process_alloc(cProcess);
1763
+ pr = process_get(obj);
1764
+ pr->h = hproc;
1765
+ pr->pid = GetProcessId(hproc);
1766
+ pr->cancel_event = CreateEventW(NULL, TRUE, FALSE, NULL);
1767
+ if (!pr->cancel_event) {
1768
+ DWORD e = GetLastError();
1769
+ /* the Process owns hproc now; close it via process_close path */
1770
+ CloseHandle(hproc); pr->h = INVALID_HANDLE_VALUE; pr->closed = 1;
1771
+ raise_gle("CreateEvent", e);
1772
+ }
1773
+ return obj;
1774
+ }
1775
+ }
1776
+
1777
+ /* --- with_privilege: AdjustTokenPrivileges + the NOT_ALL_ASSIGNED check ---- */
1778
+
1779
+ /* Winproc.__privilege(se_name_w8, enable_bool) -> prev_enabled_bool */
1780
+ static VALUE
1781
+ winproc_privilege(VALUE self, VALUE vname, VALUE venable)
1782
+ {
1783
+ HANDLE tok = NULL;
1784
+ WCHAR *name = to_wide(vname);
1785
+ LUID luid;
1786
+ TOKEN_PRIVILEGES tp, prev;
1787
+ DWORD prev_len = sizeof(prev);
1788
+ int enable = RTEST(venable);
1789
+ int was_enabled;
1790
+ DWORD gle;
1791
+ (void)self;
1792
+
1793
+ if (!OpenProcessToken(GetCurrentProcess(),
1794
+ TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &tok)) {
1795
+ gle = GetLastError(); xfree(name); raise_gle("OpenProcessToken", gle);
1796
+ }
1797
+ if (!LookupPrivilegeValueW(NULL, name, &luid)) {
1798
+ gle = GetLastError(); xfree(name); CloseHandle(tok);
1799
+ raise_gle("LookupPrivilegeValue", gle);
1800
+ }
1801
+ xfree(name);
1802
+
1803
+ memset(&tp, 0, sizeof(tp));
1804
+ memset(&prev, 0, sizeof(prev));
1805
+ tp.PrivilegeCount = 1;
1806
+ tp.Privileges[0].Luid = luid;
1807
+ tp.Privileges[0].Attributes = enable ? SE_PRIVILEGE_ENABLED : 0;
1808
+
1809
+ if (!AdjustTokenPrivileges(tok, FALSE, &tp, sizeof(prev), &prev, &prev_len)) {
1810
+ gle = GetLastError(); CloseHandle(tok); raise_gle("AdjustTokenPrivileges", gle);
1811
+ }
1812
+ /* AdjustTokenPrivileges "succeeds" with ERROR_NOT_ALL_ASSIGNED even when the
1813
+ * token doesn't hold the privilege — check GLE after success (E-19). */
1814
+ gle = GetLastError();
1815
+ if (gle == ERROR_NOT_ALL_ASSIGNED) {
1816
+ CloseHandle(tok);
1817
+ raise_code(ePrivilegeNotHeld, "AdjustTokenPrivileges", gle);
1818
+ }
1819
+ /* previous enabled-state from PreviousState (empty count => was disabled) */
1820
+ was_enabled = (prev.PrivilegeCount >= 1 &&
1821
+ (prev.Privileges[0].Attributes & SE_PRIVILEGE_ENABLED)) ? 1 : 0;
1822
+ CloseHandle(tok);
1823
+ return was_enabled ? Qtrue : Qfalse;
1824
+ }
1825
+
1826
+ /* ----------------------------------------------------------------- Init --- */
1827
+
1828
+ void
1829
+ Init_winproc(void)
1830
+ {
1831
+ HMODULE k32;
1832
+
1833
+ mWinproc = rb_define_module("Winproc");
1834
+
1835
+ eError = rb_define_class_under(mWinproc, "Error", rb_eStandardError);
1836
+ eOSError = rb_define_class_under(mWinproc, "OSError", eError);
1837
+ eNotFound = rb_define_class_under(mWinproc, "NotFound", eOSError);
1838
+ eAccessDenied = rb_define_class_under(mWinproc, "AccessDenied", eOSError);
1839
+ eCanceled = rb_define_class_under(mWinproc, "Canceled", eOSError);
1840
+ eBrokenPipe = rb_define_class_under(mWinproc, "BrokenPipe", eOSError);
1841
+ eElevationRequired = rb_define_class_under(mWinproc, "ElevationRequired", eOSError);
1842
+ ePrivilegeNotHeld = rb_define_class_under(mWinproc, "PrivilegeNotHeld", eOSError);
1843
+ eUnsupported = rb_define_class_under(mWinproc, "Unsupported", eOSError);
1844
+ eModeError = rb_define_class_under(mWinproc, "ModeError", eError);
1845
+ eClosed = rb_define_class_under(mWinproc, "Closed", eError);
1846
+
1847
+ /* module functions (primitives; underscore-private, wrapped in Ruby) */
1848
+ rb_define_singleton_method(mWinproc, "_spawn", winproc_spawn, -1);
1849
+ rb_define_singleton_method(mWinproc, "__elevated", winproc_elevated_p, 0);
1850
+ rb_define_singleton_method(mWinproc, "__admin", winproc_admin_p, 0);
1851
+ rb_define_singleton_method(mWinproc, "__pty_available", winproc_pty_available_p, 0);
1852
+ rb_define_singleton_method(mWinproc, "__runas", winproc_runas, 4);
1853
+ rb_define_singleton_method(mWinproc, "__privilege", winproc_privilege, 2);
1854
+
1855
+ /* Process */
1856
+ cProcess = rb_define_class_under(mWinproc, "Process", rb_cObject);
1857
+ rb_define_alloc_func(cProcess, process_alloc);
1858
+ rb_define_method(cProcess, "initialize", process_initialize, -1);
1859
+ rb_define_method(cProcess, "_wait", process_do_wait, 1);
1860
+ rb_define_method(cProcess, "pid", process_pid, 0);
1861
+ rb_define_method(cProcess, "alive?", process_alive_p, 0);
1862
+ rb_define_method(cProcess, "exitstatus", process_exitstatus, 0);
1863
+ rb_define_method(cProcess, "_kill", process_kill, -1);
1864
+ rb_define_method(cProcess, "close", process_close, 0);
1865
+ rb_define_method(cProcess, "closed?", process_closed_p, 0);
1866
+
1867
+ /* Job */
1868
+ cJob = rb_define_class_under(mWinproc, "Job", rb_cObject);
1869
+ rb_define_alloc_func(cJob, job_alloc);
1870
+ rb_define_singleton_method(cJob, "_create", job_create, 6);
1871
+ rb_define_method(cJob, "_assign", job_assign, 1);
1872
+ rb_define_method(cJob, "_terminate", job_terminate, -1);
1873
+ rb_define_method(cJob, "_wait_empty", job_wait_empty, 1);
1874
+ rb_define_method(cJob, "active_processes", job_active_processes, 0);
1875
+ rb_define_method(cJob, "close", job_close, 0);
1876
+ rb_define_method(cJob, "closed?", job_closed_p, 0);
1877
+
1878
+ /* Stream */
1879
+ cStream = rb_define_class_under(mWinproc, "Stream", rb_cObject);
1880
+ rb_define_alloc_func(cStream, stream_alloc);
1881
+ rb_define_method(cStream, "initialize", stream_initialize, -1);
1882
+ rb_define_method(cStream, "_read", stream_read, 1);
1883
+ rb_define_method(cStream, "_write", stream_write, 1);
1884
+ rb_define_method(cStream, "writable?", stream_writable_p, 0);
1885
+ rb_define_method(cStream, "close", stream_close, 0);
1886
+ rb_define_method(cStream, "closed?", stream_closed_p, 0);
1887
+
1888
+ /* PTY */
1889
+ cPTY = rb_define_class_under(mWinproc, "PTY", rb_cObject);
1890
+ rb_define_alloc_func(cPTY, pty_alloc);
1891
+ rb_define_method(cPTY, "initialize", pty_initialize, -1);
1892
+ rb_define_method(cPTY, "_resize", pty_resize, 2);
1893
+ rb_define_method(cPTY, "cols", pty_cols, 0);
1894
+ rb_define_method(cPTY, "rows", pty_rows, 0);
1895
+ rb_define_method(cPTY, "_close_pty", pty_close_pty, 0);
1896
+ rb_define_method(cPTY, "closed?", pty_closed_p, 0);
1897
+
1898
+ /* SW_* show constants for runas (verified <winuser.h> values) */
1899
+ rb_define_const(mWinproc, "SW_HIDE", INT2FIX(SW_HIDE)); /* 0 */
1900
+ rb_define_const(mWinproc, "SW_SHOWNORMAL", INT2FIX(SW_SHOWNORMAL)); /* 1 */
1901
+ rb_define_const(mWinproc, "SW_SHOWMINIMIZED", INT2FIX(SW_SHOWMINIMIZED)); /* 2 */
1902
+ rb_define_const(mWinproc, "SW_SHOWMAXIMIZED", INT2FIX(SW_SHOWMAXIMIZED)); /* 3 */
1903
+
1904
+ /* CreateProcess creation-flag bits exposed to the Ruby layer (verified). */
1905
+ rb_define_const(mWinproc, "CREATE_NEW_PROCESS_GROUP", ULONG2NUM(CREATE_NEW_PROCESS_GROUP));
1906
+ rb_define_const(mWinproc, "CREATE_NO_WINDOW", ULONG2NUM(CREATE_NO_WINDOW));
1907
+
1908
+ /* stdio slot kinds shared with the Ruby layer */
1909
+ rb_define_const(mWinproc, "SLOT_INHERIT", INT2FIX(SLOT_INHERIT));
1910
+ rb_define_const(mWinproc, "SLOT_NULL", INT2FIX(SLOT_NULL));
1911
+ rb_define_const(mWinproc, "SLOT_PIPE", INT2FIX(SLOT_PIPE));
1912
+ rb_define_const(mWinproc, "SLOT_IO", INT2FIX(SLOT_IO));
1913
+ rb_define_const(mWinproc, "SLOT_MERGE", INT2FIX(SLOT_MERGE));
1914
+
1915
+ /* Resolve ConPTY entry points at runtime (absent on Windows < 10 1809). */
1916
+ k32 = GetModuleHandleW(L"kernel32.dll");
1917
+ if (k32) {
1918
+ p_CreatePseudoConsole = (PFN_CreatePseudoConsole)GetProcAddress(k32, "CreatePseudoConsole");
1919
+ p_ResizePseudoConsole = (PFN_ResizePseudoConsole)GetProcAddress(k32, "ResizePseudoConsole");
1920
+ p_ClosePseudoConsole = (PFN_ClosePseudoConsole)GetProcAddress(k32, "ClosePseudoConsole");
1921
+ }
1922
+ }