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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +312 -0
- data/ext/winproc/extconf.rb +33 -0
- data/ext/winproc/winproc.c +1922 -0
- data/lib/winproc/version.rb +5 -0
- data/lib/winproc.rb +454 -0
- metadata +122 -0
|
@@ -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(<, 0, sizeof(lt));
|
|
1663
|
+
if (!GetTokenInformation(tok, TokenLinkedToken, <, 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
|
+
}
|