winsvc 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,1162 @@
1
+ /*
2
+ * winsvc — host a Ruby process as a Windows service (SERVICE_WIN32_OWN_PROCESS)
3
+ * with correct service-control-manager integration, plus a minimal installer.
4
+ *
5
+ * PURE C (no C++), so rb_raise/longjmp is the normal, safe error mechanism —
6
+ * the same discipline as winipc/winloop/phylax, and the opposite of the lithos
7
+ * /EHsc hazard. Two IRON RULES govern this file:
8
+ *
9
+ * (1) SCM-OWNED THREADS NEVER TOUCH A SINGLE RUBY API. ServiceMain, HandlerEx,
10
+ * and the dispatcher thread run on threads MRI did not create; MRI cannot
11
+ * attach a foreign native thread (thread.c: rb_thread_call_with_gvl "DOES
12
+ * NOT associate or convert a NON-Ruby thread to a Ruby thread"). Below the
13
+ * review banner "/* ===== SCM-thread code: NO rb_ IDENTIFIERS BELOW =====",
14
+ * no rb_* call may appear. Marshaling is memcpy -> fixed-size ring -> event
15
+ * -> a real Ruby pump Thread that drains it into a Thread::Queue.
16
+ * (2) SERVICE_STOPPED IS REPORTED EXACTLY ONCE, FROM ServiceMain, AFTER Ruby
17
+ * has fully unwound. The first STOPPED report closes the RPC context handle
18
+ * and "any subsequent calls can cause the process to crash" — so every
19
+ * guard-check + SetServiceStatus pair (the handler's pending reports, the
20
+ * Ruby status bridges, and ServiceMain's STOPPED report which sets
21
+ * stopped_reported in the SAME critical section as the report) executes
22
+ * atomically under the one host CRITICAL_SECTION.
23
+ *
24
+ * Native state is a process singleton (Win32 permits ONE StartServiceCtrlDispatcherW
25
+ * per process, ever), so all of it lives in one file-scope `static host_t g_host`,
26
+ * latched once by an interlocked one-shot and never freed (process-lifetime — the
27
+ * phylax CNG-handle precedent). The yielded Winsvc::Service is a plain Ruby object
28
+ * holding no native pointer. SCM-client calls (_install etc.) open and close every
29
+ * handle within a single C function call; nothing outlives a call.
30
+ *
31
+ * Arch-neutral: gated only on /mswin/ at extconf, _WIN64 here, ULONG_PTR for
32
+ * handle-sized values; no /MACHINE, no inline asm. Links advapi32 (+ kernel32
33
+ * by default). Include <ruby.h> before <windows.h>; never name a variable IN/OUT.
34
+ */
35
+
36
+ #include <ruby.h>
37
+ #include <ruby/thread.h>
38
+ #include <ruby/encoding.h>
39
+ #include <limits.h>
40
+
41
+ #define WIN32_LEAN_AND_MEAN
42
+ #include <windows.h>
43
+ #include <process.h> /* _beginthreadex */
44
+
45
+ /* ------------------------------------------------------------------ globals */
46
+
47
+ static VALUE mWinsvc;
48
+ static VALUE eError, eOSError, eAccessDenied, eExists, eNotFound,
49
+ eMarkedForDelete, eTimeout, eStateError;
50
+
51
+ /* ----------------------------------------------------------- tunables -------*/
52
+
53
+ #define WINSVC_RING_CAP 64 /* control-record ring slots */
54
+ #define WINSVC_DATA_MAX 512 /* truncation cap for POWERBROADCAST_SETTING */
55
+ #define WINSVC_DRAIN_MAX 8 /* records copied per CS hold in _wait_control */
56
+
57
+ /* Win32 numeric constants we use that may be absent from older SDK headers. */
58
+ #ifndef SERVICE_ACCEPT_PRESHUTDOWN
59
+ #define SERVICE_ACCEPT_PRESHUTDOWN 0x00000100
60
+ #endif
61
+ #ifndef SERVICE_CONTROL_PRESHUTDOWN
62
+ #define SERVICE_CONTROL_PRESHUTDOWN 0x0000000F
63
+ #endif
64
+ #ifndef SERVICE_CONFIG_DELAYED_AUTO_START_INFO
65
+ #define SERVICE_CONFIG_DELAYED_AUTO_START_INFO 3
66
+ #endif
67
+ #ifndef SERVICE_CONFIG_PRESHUTDOWN_INFO
68
+ #define SERVICE_CONFIG_PRESHUTDOWN_INFO 7
69
+ #endif
70
+ #ifndef SERVICE_CONFIG_FAILURE_ACTIONS_FLAG
71
+ #define SERVICE_CONFIG_FAILURE_ACTIONS_FLAG 4
72
+ #endif
73
+
74
+ /* ------------------------------------------------------------ native types -*/
75
+
76
+ /* Fixed-size control record; copied by value, never pointed-to. No pointers,
77
+ * nothing to free, no ABA. */
78
+ typedef struct {
79
+ DWORD control; /* SERVICE_CONTROL_* */
80
+ DWORD event_type; /* PBT_* / WTS_* or 0 */
81
+ DWORD session_id; /* dwSessionId, or 0xFFFFFFFF = none */
82
+ DWORD data_len; /* 0..WINSVC_DATA_MAX */
83
+ BYTE data[WINSVC_DATA_MAX]; /* POWERBROADCAST_SETTING bytes (truncated)*/
84
+ ULONGLONG tick; /* GetTickCount64 at enqueue (diagnostic) */
85
+ } ctl_rec_t;
86
+
87
+ typedef struct {
88
+ LONG used; /* one-shot latch (InterlockedCompareExchange) */
89
+ WCHAR *name_w; /* malloc'd; must outlive the dispatcher call */
90
+ DWORD accept_mask, start_hint, stop_hint;
91
+
92
+ /* SCM/dispatcher -> Ruby signalling */
93
+ HANDLE hStarted; /* manual-reset: ServiceMain entered (service) */
94
+ HANDLE hConsoleMode; /* manual-reset: dispatcher saw 1063 (console) */
95
+ HANDLE hDispatchFail; /* manual-reset: dispatcher failed (!= 1063) */
96
+ HANDLE hControl; /* auto-reset: ring has data */
97
+ HANDLE hPumpWake; /* auto-reset: ubf wake for _wait_control */
98
+ HANDLE hMainWake; /* auto-reset: ubf wake for _host_wait_mode */
99
+ HANDLE hRubyDone; /* manual-reset: Ruby unwound -> report STOPPED*/
100
+ HANDLE hDispatcher; /* _beginthreadex handle, joined in _host_done */
101
+
102
+ SERVICE_STATUS_HANDLE status; /* registered in ServiceMain; never closed */
103
+ CRITICAL_SECTION lock; /* guards ring + state/checkpoint + EVERY
104
+ SetServiceStatus (handler, bridges, ServiceMain) */
105
+
106
+ ctl_rec_t ring[WINSVC_RING_CAP];
107
+ unsigned head, count, dropped;
108
+
109
+ DWORD current_state, checkpoint; /* last reported state, for _checkpoint */
110
+ LONG stop_flag; /* set by handler on STOP/SHUTDOWN/PRESHUTDOWN */
111
+ LONG stopped_reported; /* hard guard; set under lock WITH the STOPPED report */
112
+ DWORD win32_exit, specific_exit;
113
+
114
+ DWORD dispatch_error; /* GetLastError when the dispatcher failed (!=1063) */
115
+
116
+ char **argv_utf8; /* ServiceMain args (plain malloc — no Ruby on */
117
+ int argc; /* an SCM thread; OOM must not longjmp) */
118
+ } host_t;
119
+
120
+ static host_t g_host; /* the process singleton; never freed */
121
+
122
+ /* ============================================================================
123
+ * SCM-thread code: NO rb_ IDENTIFIERS BELOW THIS BANNER (rule 1).
124
+ * These functions run on SCM-created native threads (ServiceMain, HandlerEx)
125
+ * or on our own dispatcher thread, which must never enter the Ruby VM either.
126
+ * Plain malloc/memcpy only; no xmalloc, no rb_*.
127
+ * ==========================================================================*/
128
+
129
+ /* Report status under the host lock. The caller decides the guard policy; this
130
+ * helper just fills the struct and calls SetServiceStatus. NOT locked here —
131
+ * callers hold the CS so the guard check + report are atomic. */
132
+ static void
133
+ report_status_locked(DWORD state, DWORD accepted, DWORD checkpoint,
134
+ DWORD wait_hint, DWORD win32_exit, DWORD specific)
135
+ {
136
+ SERVICE_STATUS ss;
137
+ ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
138
+ ss.dwCurrentState = state;
139
+ ss.dwControlsAccepted = accepted;
140
+ ss.dwWin32ExitCode = win32_exit;
141
+ ss.dwServiceSpecificExitCode = specific;
142
+ ss.dwCheckPoint = checkpoint;
143
+ ss.dwWaitHint = wait_hint;
144
+ if (g_host.status) SetServiceStatus(g_host.status, &ss);
145
+ g_host.current_state = state;
146
+ g_host.checkpoint = checkpoint;
147
+ }
148
+
149
+ /* Enqueue a control record into the ring. Shared VERBATIM by HandlerEx and the
150
+ * Ruby _inject bridge (which calls it under the same CS). Stop-class records
151
+ * overwrite the newest slot when full (guaranteed delivery — the latched
152
+ * stop_flag is the real signal anyway); anything else is dropped + counted.
153
+ * Caller need NOT hold the lock; this acquires it (the CS is recursive, so the
154
+ * handler's outer hold nests fine). Signals hControl after releasing. */
155
+ static int
156
+ enqueue_rec(const ctl_rec_t *rec)
157
+ {
158
+ int is_stop = (rec->control == SERVICE_CONTROL_STOP ||
159
+ rec->control == SERVICE_CONTROL_SHUTDOWN ||
160
+ rec->control == SERVICE_CONTROL_PRESHUTDOWN);
161
+ EnterCriticalSection(&g_host.lock);
162
+ if (g_host.count >= WINSVC_RING_CAP) {
163
+ if (is_stop) {
164
+ /* Overwrite the newest slot so the stop is never lost. The displaced
165
+ * record IS lost, so count it as dropped — keeping the invariant
166
+ * delivered + dropped == injected exact. */
167
+ unsigned newest = (g_host.head + g_host.count - 1) % WINSVC_RING_CAP;
168
+ g_host.ring[newest] = *rec;
169
+ g_host.dropped++;
170
+ } else {
171
+ g_host.dropped++;
172
+ }
173
+ } else {
174
+ unsigned slot = (g_host.head + g_host.count) % WINSVC_RING_CAP;
175
+ g_host.ring[slot] = *rec;
176
+ g_host.count++;
177
+ }
178
+ LeaveCriticalSection(&g_host.lock);
179
+ SetEvent(g_host.hControl);
180
+ return is_stop;
181
+ }
182
+
183
+ /* HandlerEx — the hot path. Pure C, microseconds. Runs on the dispatcher thread;
184
+ * the SCM serializes control delivery. */
185
+ static DWORD WINAPI
186
+ handler_ex(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
187
+ {
188
+ ctl_rec_t rec;
189
+ (void)lpContext;
190
+
191
+ switch (dwControl) {
192
+ case SERVICE_CONTROL_INTERROGATE:
193
+ /* The SCM caches the last reported status; no re-report needed. */
194
+ return NO_ERROR;
195
+
196
+ case SERVICE_CONTROL_STOP:
197
+ case SERVICE_CONTROL_SHUTDOWN:
198
+ case SERVICE_CONTROL_PRESHUTDOWN:
199
+ EnterCriticalSection(&g_host.lock);
200
+ if (!g_host.stopped_reported) {
201
+ report_status_locked(SERVICE_STOP_PENDING, 0, 1, g_host.stop_hint,
202
+ NO_ERROR, 0);
203
+ InterlockedExchange(&g_host.stop_flag, 1);
204
+ memset(&rec, 0, sizeof(rec));
205
+ rec.control = dwControl;
206
+ rec.session_id = 0xFFFFFFFF;
207
+ rec.tick = GetTickCount64();
208
+ enqueue_rec(&rec); /* recursive CS: nests fine */
209
+ }
210
+ LeaveCriticalSection(&g_host.lock);
211
+ return NO_ERROR;
212
+
213
+ case SERVICE_CONTROL_PAUSE:
214
+ case SERVICE_CONTROL_CONTINUE:
215
+ EnterCriticalSection(&g_host.lock);
216
+ if (!g_host.stopped_reported) {
217
+ DWORD pending = (dwControl == SERVICE_CONTROL_PAUSE)
218
+ ? SERVICE_PAUSE_PENDING : SERVICE_CONTINUE_PENDING;
219
+ report_status_locked(pending, 0, 1, g_host.stop_hint, NO_ERROR, 0);
220
+ memset(&rec, 0, sizeof(rec));
221
+ rec.control = dwControl;
222
+ rec.session_id = 0xFFFFFFFF;
223
+ rec.tick = GetTickCount64();
224
+ enqueue_rec(&rec);
225
+ }
226
+ LeaveCriticalSection(&g_host.lock);
227
+ return NO_ERROR;
228
+
229
+ case SERVICE_CONTROL_POWEREVENT:
230
+ memset(&rec, 0, sizeof(rec));
231
+ rec.control = dwControl;
232
+ rec.event_type = dwEventType;
233
+ rec.session_id = 0xFFFFFFFF;
234
+ rec.tick = GetTickCount64();
235
+ if (dwEventType == PBT_POWERSETTINGCHANGE && lpEventData) {
236
+ /* POWERBROADCAST_SETTING { GUID; DWORD DataLength; UCHAR Data[1] }.
237
+ * Copy the header + DataLength bytes, capped. The pointer is valid
238
+ * ONLY during this call. */
239
+ POWERBROADCAST_SETTING *pbs = (POWERBROADCAST_SETTING *)lpEventData;
240
+ DWORD hdr = (DWORD)(sizeof(GUID) + sizeof(DWORD));
241
+ DWORD total = hdr + pbs->DataLength;
242
+ if (total > WINSVC_DATA_MAX) total = WINSVC_DATA_MAX;
243
+ memcpy(rec.data, lpEventData, total);
244
+ rec.data_len = total;
245
+ }
246
+ enqueue_rec(&rec);
247
+ return NO_ERROR;
248
+
249
+ case SERVICE_CONTROL_SESSIONCHANGE:
250
+ memset(&rec, 0, sizeof(rec));
251
+ rec.control = dwControl;
252
+ rec.event_type = dwEventType;
253
+ rec.session_id = 0xFFFFFFFF;
254
+ rec.tick = GetTickCount64();
255
+ if (lpEventData) {
256
+ WTSSESSION_NOTIFICATION *wsn = (WTSSESSION_NOTIFICATION *)lpEventData;
257
+ rec.session_id = wsn->dwSessionId;
258
+ }
259
+ enqueue_rec(&rec);
260
+ return NO_ERROR;
261
+
262
+ default:
263
+ return ERROR_CALL_NOT_IMPLEMENTED;
264
+ }
265
+ }
266
+
267
+ /* ServiceMain — runs on an SCM-created thread. Pure C. */
268
+ static void WINAPI
269
+ service_main(DWORD dwArgc, LPWSTR *lpszArgv)
270
+ {
271
+ DWORD i;
272
+
273
+ g_host.status = RegisterServiceCtrlHandlerExW(g_host.name_w, handler_ex, NULL);
274
+ if (!g_host.status) {
275
+ /* Cannot report status without a handle. The dispatcher will fail the
276
+ * service; surface "console-ish" by setting hDispatchFail. */
277
+ g_host.dispatch_error = GetLastError();
278
+ SetEvent(g_host.hDispatchFail);
279
+ return;
280
+ }
281
+
282
+ /* Initial report: accepted MUST be 0 while START_PENDING, or the service
283
+ * can crash. */
284
+ EnterCriticalSection(&g_host.lock);
285
+ report_status_locked(SERVICE_START_PENDING, 0, 1, g_host.start_hint, NO_ERROR, 0);
286
+ LeaveCriticalSection(&g_host.lock);
287
+
288
+ /* Copy argv UTF-16 -> UTF-8 into plain-malloc'd buffers (no Ruby here). */
289
+ g_host.argc = (int)dwArgc;
290
+ if (dwArgc > 0) {
291
+ g_host.argv_utf8 = (char **)calloc(dwArgc, sizeof(char *));
292
+ if (g_host.argv_utf8) {
293
+ for (i = 0; i < dwArgc; i++) {
294
+ int n = WideCharToMultiByte(CP_UTF8, 0, lpszArgv[i], -1,
295
+ NULL, 0, NULL, NULL);
296
+ if (n > 0) {
297
+ char *s = (char *)malloc((size_t)n);
298
+ if (s) {
299
+ WideCharToMultiByte(CP_UTF8, 0, lpszArgv[i], -1,
300
+ s, n, NULL, NULL);
301
+ }
302
+ g_host.argv_utf8[i] = s; /* may be NULL on OOM; Ruby treats as "" */
303
+ }
304
+ }
305
+ } else {
306
+ g_host.argc = 0; /* OOM: present no args rather than crash */
307
+ }
308
+ }
309
+
310
+ SetEvent(g_host.hStarted);
311
+
312
+ /* Wait for Ruby to fully unwind, then report STOPPED exactly once. */
313
+ WaitForSingleObject(g_host.hRubyDone, INFINITE);
314
+
315
+ EnterCriticalSection(&g_host.lock);
316
+ g_host.stopped_reported = 1; /* set WITH the report, atomically (rule 2) */
317
+ report_status_locked(SERVICE_STOPPED, 0, 0, 0,
318
+ g_host.win32_exit, g_host.specific_exit);
319
+ LeaveCriticalSection(&g_host.lock);
320
+ /* No further work after STOPPED: the process may be terminated at any time. */
321
+ }
322
+
323
+ /* The dispatcher thread. _beginthreadex entry. Pure C. */
324
+ static unsigned __stdcall
325
+ dispatcher_thread(void *arg)
326
+ {
327
+ SERVICE_TABLE_ENTRYW table[2];
328
+ (void)arg;
329
+
330
+ table[0].lpServiceName = g_host.name_w; /* ignored for OWN_PROCESS, must be valid */
331
+ table[0].lpServiceProc = service_main;
332
+ table[1].lpServiceName = NULL;
333
+ table[1].lpServiceProc = NULL;
334
+
335
+ if (!StartServiceCtrlDispatcherW(table)) {
336
+ DWORD gle = GetLastError();
337
+ if (gle == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) {
338
+ SetEvent(g_host.hConsoleMode); /* 1063 — launched from a console/CI */
339
+ } else {
340
+ g_host.dispatch_error = gle;
341
+ SetEvent(g_host.hDispatchFail);
342
+ }
343
+ }
344
+ /* Success path: returns only once the service is STOPPED. */
345
+ return 0;
346
+ }
347
+
348
+ /* ============================================================================
349
+ * Ruby-thread code below: rb_* allowed again. These run on the Ruby main
350
+ * thread or the pump Thread.
351
+ * ==========================================================================*/
352
+
353
+ /* UTF-8 Ruby String -> freshly xmalloc'd NUL-terminated UTF-16. Caller xfrees.
354
+ * (winipc to_wide, verbatim.) */
355
+ static WCHAR *
356
+ to_wide(VALUE str)
357
+ {
358
+ int len, n;
359
+ WCHAR *w;
360
+ StringValue(str);
361
+ len = (int)RSTRING_LEN(str);
362
+ if (len == 0) {
363
+ w = (WCHAR *)xmalloc(sizeof(WCHAR));
364
+ w[0] = 0;
365
+ return w;
366
+ }
367
+ n = MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, NULL, 0);
368
+ if (n <= 0) rb_raise(eError, "winsvc: invalid UTF-8 in string");
369
+ w = (WCHAR *)xmalloc(sizeof(WCHAR) * (n + 1));
370
+ MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, w, n);
371
+ w[n] = 0;
372
+ return w;
373
+ }
374
+
375
+ /* Plain-malloc'd NUL-terminated UTF-16 (for the dispatcher's name_w, which must
376
+ * outlive the call and never be touched by Ruby's GC). Returns NULL on failure. */
377
+ static WCHAR *
378
+ to_wide_malloc(VALUE str)
379
+ {
380
+ int len, n;
381
+ WCHAR *w;
382
+ StringValue(str);
383
+ len = (int)RSTRING_LEN(str);
384
+ n = (len == 0) ? 0 : MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, NULL, 0);
385
+ if (len != 0 && n <= 0) return NULL;
386
+ w = (WCHAR *)malloc(sizeof(WCHAR) * (size_t)(n + 1));
387
+ if (!w) return NULL;
388
+ if (n > 0) MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, w, n);
389
+ w[n] = 0;
390
+ return w;
391
+ }
392
+
393
+ /* Build + raise a winsvc error carrying @code (the Win32 error). The code must
394
+ * be captured by the caller immediately after the failing syscall. (winipc
395
+ * raise_code, verbatim shape — release the OS buffer before any Ruby alloc.) */
396
+ static void
397
+ raise_code(VALUE klass, const char *api, DWORD code)
398
+ {
399
+ VALUE exc, msg;
400
+ WCHAR *buf = NULL;
401
+ char detail[512];
402
+ DWORD n;
403
+
404
+ detail[0] = 0;
405
+ n = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
406
+ FORMAT_MESSAGE_IGNORE_INSERTS, NULL, code,
407
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&buf, 0, NULL);
408
+ if (n && buf) {
409
+ while (n && (buf[n-1] == L'\r' || buf[n-1] == L'\n' || buf[n-1] == L'.')) buf[--n] = 0;
410
+ WideCharToMultiByte(CP_UTF8, 0, buf, -1, detail, (int)sizeof(detail), NULL, NULL);
411
+ detail[sizeof(detail) - 1] = 0;
412
+ }
413
+ if (buf) LocalFree(buf);
414
+
415
+ if (detail[0])
416
+ msg = rb_sprintf("%s: %s (error %lu)", api, detail, (unsigned long)code);
417
+ else
418
+ msg = rb_sprintf("%s failed (error %lu)", api, (unsigned long)code);
419
+ exc = rb_exc_new_str(klass, msg);
420
+ rb_iv_set(exc, "@code", ULONG2NUM(code));
421
+ rb_exc_raise(exc);
422
+ }
423
+
424
+ /* Pick the right subclass for a Win32 error and raise it. */
425
+ static void
426
+ raise_gle(const char *api, DWORD code)
427
+ {
428
+ VALUE klass = eOSError;
429
+ switch (code) {
430
+ case ERROR_ACCESS_DENIED: klass = eAccessDenied; break; /* 5 */
431
+ case ERROR_SERVICE_EXISTS: /* 1073 */
432
+ case ERROR_DUPLICATE_SERVICE_NAME: klass = eExists; break; /* 1078 */
433
+ case ERROR_SERVICE_DOES_NOT_EXIST: klass = eNotFound; break; /* 1060 */
434
+ case ERROR_SERVICE_MARKED_FOR_DELETE: klass = eMarkedForDelete; break; /* 1072 */
435
+ default: break;
436
+ }
437
+ raise_code(klass, api, code);
438
+ }
439
+
440
+ /* ----------------------------------------------------- _host_start ---------*/
441
+
442
+ /* Winsvc._host_start(name, accept_mask, start_hint, stop_hint) -> true
443
+ * Create events/CS/ring, spawn the dispatcher thread. One-shot interlocked
444
+ * latch: a second call raises StateError before touching the OS. */
445
+ static VALUE
446
+ host_start(VALUE mod, VALUE name, VALUE accept_mask, VALUE start_hint, VALUE stop_hint)
447
+ {
448
+ DWORD mask = NUM2ULONG(accept_mask);
449
+ DWORD shint = NUM2ULONG(start_hint);
450
+ DWORD phint = NUM2ULONG(stop_hint);
451
+ WCHAR *nw;
452
+ uintptr_t th;
453
+ (void)mod;
454
+
455
+ if (InterlockedCompareExchange(&g_host.used, 1, 0) != 0)
456
+ rb_raise(eStateError, "winsvc: Winsvc.run may be called only once per process");
457
+
458
+ nw = to_wide_malloc(name);
459
+ if (!nw) {
460
+ g_host.used = 1; /* stays latched; this process can never host again */
461
+ rb_raise(eError, "winsvc: invalid UTF-8 in service name");
462
+ }
463
+ g_host.name_w = nw;
464
+ g_host.accept_mask = mask;
465
+ g_host.start_hint = shint;
466
+ g_host.stop_hint = phint;
467
+
468
+ InitializeCriticalSection(&g_host.lock);
469
+
470
+ /* manual-reset: hStarted, hConsoleMode, hDispatchFail, hRubyDone.
471
+ * auto-reset: hControl, hPumpWake, hMainWake. (7 total) */
472
+ g_host.hStarted = CreateEventW(NULL, TRUE, FALSE, NULL);
473
+ g_host.hConsoleMode = CreateEventW(NULL, TRUE, FALSE, NULL);
474
+ g_host.hDispatchFail = CreateEventW(NULL, TRUE, FALSE, NULL);
475
+ g_host.hRubyDone = CreateEventW(NULL, TRUE, FALSE, NULL);
476
+ g_host.hControl = CreateEventW(NULL, FALSE, FALSE, NULL);
477
+ g_host.hPumpWake = CreateEventW(NULL, FALSE, FALSE, NULL);
478
+ g_host.hMainWake = CreateEventW(NULL, FALSE, FALSE, NULL);
479
+ if (!g_host.hStarted || !g_host.hConsoleMode || !g_host.hDispatchFail ||
480
+ !g_host.hRubyDone || !g_host.hControl || !g_host.hPumpWake || !g_host.hMainWake)
481
+ raise_gle("CreateEvent", GetLastError());
482
+
483
+ th = _beginthreadex(NULL, 0, dispatcher_thread, NULL, 0, NULL);
484
+ if (th == 0)
485
+ rb_raise(eError, "winsvc: failed to start the dispatcher thread");
486
+ g_host.hDispatcher = (HANDLE)th;
487
+
488
+ return Qtrue;
489
+ }
490
+
491
+ /* ----------------------------------------------- _host_wait_mode -----------*/
492
+
493
+ typedef struct { DWORD which; } mode_wait_t;
494
+
495
+ static void *
496
+ mode_wait_fn(void *p)
497
+ {
498
+ mode_wait_t *w = (mode_wait_t *)p;
499
+ HANDLE hs[4];
500
+ hs[0] = g_host.hStarted;
501
+ hs[1] = g_host.hConsoleMode;
502
+ hs[2] = g_host.hDispatchFail;
503
+ hs[3] = g_host.hMainWake;
504
+ w->which = WaitForMultipleObjects(4, hs, FALSE, INFINITE);
505
+ return NULL;
506
+ }
507
+
508
+ static void
509
+ mode_wait_ubf(void *p)
510
+ {
511
+ (void)p;
512
+ SetEvent(g_host.hMainWake);
513
+ }
514
+
515
+ /* Winsvc._host_wait_mode -> :service | :console (raises OSError on dispatch
516
+ * failure). GVL released; ubf wakes via hMainWake so Thread#kill / Ctrl-C break
517
+ * it, then we re-wait. */
518
+ static VALUE
519
+ host_wait_mode(VALUE mod)
520
+ {
521
+ (void)mod;
522
+ for (;;) {
523
+ mode_wait_t w;
524
+ w.which = WAIT_FAILED;
525
+ rb_thread_call_without_gvl(mode_wait_fn, &w, mode_wait_ubf, &w);
526
+
527
+ if (w.which == WAIT_OBJECT_0 + 0)
528
+ return ID2SYM(rb_intern("service"));
529
+ if (w.which == WAIT_OBJECT_0 + 1)
530
+ return ID2SYM(rb_intern("console"));
531
+ if (w.which == WAIT_OBJECT_0 + 2)
532
+ raise_gle("StartServiceCtrlDispatcher", g_host.dispatch_error);
533
+
534
+ /* hMainWake (index 3) or WAIT_FAILED: service interrupts, then re-wait. */
535
+ rb_thread_check_ints();
536
+ }
537
+ }
538
+
539
+ /* ----------------------------------------------------- _host_args ----------*/
540
+
541
+ /* Winsvc._host_args -> Array<String> (frozen UTF-8), the ServiceMain argv. */
542
+ static VALUE
543
+ host_args(VALUE mod)
544
+ {
545
+ VALUE ary;
546
+ int i;
547
+ (void)mod;
548
+ ary = rb_ary_new_capa(g_host.argc > 0 ? g_host.argc : 0);
549
+ for (i = 0; i < g_host.argc; i++) {
550
+ const char *s = g_host.argv_utf8 ? g_host.argv_utf8[i] : NULL;
551
+ VALUE str = rb_utf8_str_new_cstr(s ? s : "");
552
+ rb_str_freeze(str);
553
+ rb_ary_push(ary, str);
554
+ }
555
+ return ary;
556
+ }
557
+
558
+ /* ---------------------------------------------------- _wait_control --------*/
559
+
560
+ typedef struct { DWORD ms; DWORD which; } ctl_wait_t;
561
+
562
+ static void *
563
+ ctl_wait_fn(void *p)
564
+ {
565
+ ctl_wait_t *w = (ctl_wait_t *)p;
566
+ HANDLE hs[2];
567
+ hs[0] = g_host.hControl;
568
+ hs[1] = g_host.hPumpWake;
569
+ w->which = WaitForMultipleObjects(2, hs, FALSE, w->ms);
570
+ return NULL;
571
+ }
572
+
573
+ static void
574
+ ctl_wait_ubf(void *p)
575
+ {
576
+ (void)p;
577
+ SetEvent(g_host.hPumpWake);
578
+ }
579
+
580
+ /* Build a Ruby Array of raw-record arrays from one drain chunk. Called with the
581
+ * GVL held and NO critical section held (records already copied to the stack). */
582
+ static VALUE
583
+ build_records(const ctl_rec_t *recs, int n)
584
+ {
585
+ VALUE out = rb_ary_new_capa(n);
586
+ int i;
587
+ for (i = 0; i < n; i++) {
588
+ const ctl_rec_t *r = &recs[i];
589
+ VALUE row = rb_ary_new_capa(6);
590
+ VALUE data;
591
+ rb_ary_push(row, ULONG2NUM(r->control));
592
+ rb_ary_push(row, ULONG2NUM(r->event_type));
593
+ rb_ary_push(row, ULONG2NUM(r->session_id));
594
+ if (r->data_len > 0) {
595
+ data = rb_str_new((const char *)r->data, (long)r->data_len);
596
+ rb_enc_associate(data, rb_ascii8bit_encoding());
597
+ rb_str_freeze(data);
598
+ } else {
599
+ data = Qnil;
600
+ }
601
+ rb_ary_push(row, data);
602
+ rb_ary_push(row, ULL2NUM(r->tick));
603
+ rb_ary_freeze(row);
604
+ rb_ary_push(out, row);
605
+ }
606
+ return out;
607
+ }
608
+
609
+ /* Winsvc._wait_control(ms) -> Array of raw records, or nil on timeout.
610
+ * (-1 ms == INFINITE.) Drains up to WINSVC_DRAIN_MAX records per CS hold,
611
+ * building Ruby objects OUTSIDE the lock (no Ruby allocation while the CS is
612
+ * held — an OOM longjmp must not orphan the lock), looping until the ring is
613
+ * empty. */
614
+ static VALUE
615
+ wait_control(VALUE mod, VALUE vms)
616
+ {
617
+ long ms_in = NUM2LONG(vms);
618
+ ctl_wait_t w;
619
+ VALUE all = Qnil;
620
+ (void)mod;
621
+
622
+ w.ms = (ms_in < 0) ? INFINITE : (DWORD)ms_in;
623
+ w.which = WAIT_FAILED;
624
+ rb_thread_call_without_gvl(ctl_wait_fn, &w, ctl_wait_ubf, &w);
625
+
626
+ if (w.which == WAIT_TIMEOUT) {
627
+ rb_thread_check_ints();
628
+ return Qnil;
629
+ }
630
+ /* hPumpWake (ubf) or signaled hControl: service interrupts (delivers the
631
+ * teardown Thread#kill), then drain whatever is present. */
632
+ rb_thread_check_ints();
633
+
634
+ for (;;) {
635
+ ctl_rec_t chunk[WINSVC_DRAIN_MAX];
636
+ int got = 0;
637
+ VALUE part;
638
+
639
+ EnterCriticalSection(&g_host.lock);
640
+ while (got < WINSVC_DRAIN_MAX && g_host.count > 0) {
641
+ chunk[got++] = g_host.ring[g_host.head];
642
+ g_host.head = (g_host.head + 1) % WINSVC_RING_CAP;
643
+ g_host.count--;
644
+ }
645
+ LeaveCriticalSection(&g_host.lock);
646
+
647
+ if (got == 0) break;
648
+ part = build_records(chunk, got); /* Ruby alloc, lock released */
649
+ if (NIL_P(all)) all = part;
650
+ else rb_ary_concat(all, part);
651
+ if (got < WINSVC_DRAIN_MAX) break; /* ring drained this pass */
652
+ }
653
+ return all; /* may be nil if a spurious wake found nothing */
654
+ }
655
+
656
+ /* ------------------------------------------------------- _inject -----------*/
657
+
658
+ /* Winsvc._inject(control, event_type, session_id, data_or_nil) -> true
659
+ * Enqueue a record from Ruby (console Ctrl-C; tests) — the SAME enqueue path
660
+ * HandlerEx uses, under the same CS. Reports the pending transition for
661
+ * stop/pause/continue exactly like the handler, so console mode and tests see
662
+ * the real state machine. */
663
+ static VALUE
664
+ host_inject(VALUE mod, VALUE vcontrol, VALUE vevent, VALUE vsession, VALUE vdata)
665
+ {
666
+ ctl_rec_t rec;
667
+ DWORD control = NUM2ULONG(vcontrol);
668
+ (void)mod;
669
+
670
+ memset(&rec, 0, sizeof(rec));
671
+ rec.control = control;
672
+ rec.event_type = NIL_P(vevent) ? 0 : NUM2ULONG(vevent);
673
+ rec.session_id = NIL_P(vsession) ? 0xFFFFFFFF : NUM2ULONG(vsession);
674
+ rec.tick = GetTickCount64();
675
+ if (!NIL_P(vdata)) {
676
+ long len;
677
+ StringValue(vdata);
678
+ len = RSTRING_LEN(vdata);
679
+ if (len > WINSVC_DATA_MAX) len = WINSVC_DATA_MAX;
680
+ memcpy(rec.data, RSTRING_PTR(vdata), (size_t)len);
681
+ rec.data_len = (DWORD)len;
682
+ }
683
+
684
+ EnterCriticalSection(&g_host.lock);
685
+ if (!g_host.stopped_reported) {
686
+ if (control == SERVICE_CONTROL_STOP ||
687
+ control == SERVICE_CONTROL_SHUTDOWN ||
688
+ control == SERVICE_CONTROL_PRESHUTDOWN) {
689
+ if (g_host.status)
690
+ report_status_locked(SERVICE_STOP_PENDING, 0, 1, g_host.stop_hint, NO_ERROR, 0);
691
+ InterlockedExchange(&g_host.stop_flag, 1);
692
+ } else if (control == SERVICE_CONTROL_PAUSE) {
693
+ if (g_host.status)
694
+ report_status_locked(SERVICE_PAUSE_PENDING, 0, 1, g_host.stop_hint, NO_ERROR, 0);
695
+ } else if (control == SERVICE_CONTROL_CONTINUE) {
696
+ if (g_host.status)
697
+ report_status_locked(SERVICE_CONTINUE_PENDING, 0, 1, g_host.stop_hint, NO_ERROR, 0);
698
+ }
699
+ enqueue_rec(&rec); /* recursive CS */
700
+ }
701
+ LeaveCriticalSection(&g_host.lock);
702
+ return Qtrue;
703
+ }
704
+
705
+ /* ------------------------------------------ status bridges (running etc.) --*/
706
+
707
+ /* All status bridges: guard check + SetServiceStatus atomic under the host CS.
708
+ * In console mode g_host.status is NULL, so report_status_locked is a no-op —
709
+ * but the Ruby layer already no-ops these in console mode; the NULL guard is
710
+ * belt-and-suspenders. */
711
+
712
+ static VALUE
713
+ set_running(VALUE mod)
714
+ {
715
+ (void)mod;
716
+ EnterCriticalSection(&g_host.lock);
717
+ /* running!/ready! no-op once a stop-class control is latched or after STOPPED. */
718
+ if (!g_host.stopped_reported && !g_host.stop_flag && g_host.status)
719
+ report_status_locked(SERVICE_RUNNING, g_host.accept_mask, 0, 0, NO_ERROR, 0);
720
+ LeaveCriticalSection(&g_host.lock);
721
+ return Qnil;
722
+ }
723
+
724
+ static VALUE
725
+ set_paused(VALUE mod)
726
+ {
727
+ (void)mod;
728
+ EnterCriticalSection(&g_host.lock);
729
+ if (!g_host.stopped_reported && !g_host.stop_flag && g_host.status)
730
+ report_status_locked(SERVICE_PAUSED, g_host.accept_mask, 0, 0, NO_ERROR, 0);
731
+ LeaveCriticalSection(&g_host.lock);
732
+ return Qnil;
733
+ }
734
+
735
+ /* Winsvc._checkpoint(hint_or_-1) -> true. Re-reports the CURRENT pending state
736
+ * with ++checkpoint. No-op when no transition is pending (a checkpoint in
737
+ * RUNNING/PAUSED/STOPPED is invalid). Keeps working after a stop is latched —
738
+ * that is exactly what a draining stop needs. */
739
+ static VALUE
740
+ checkpoint(VALUE mod, VALUE vhint)
741
+ {
742
+ long hint_in = NUM2LONG(vhint);
743
+ (void)mod;
744
+ EnterCriticalSection(&g_host.lock);
745
+ if (!g_host.stopped_reported && g_host.status) {
746
+ DWORD st = g_host.current_state;
747
+ if (st == SERVICE_START_PENDING || st == SERVICE_STOP_PENDING ||
748
+ st == SERVICE_PAUSE_PENDING || st == SERVICE_CONTINUE_PENDING) {
749
+ DWORD hint = (hint_in < 0) ? g_host.stop_hint : (DWORD)hint_in;
750
+ report_status_locked(st, 0, g_host.checkpoint + 1, hint, NO_ERROR, 0);
751
+ }
752
+ }
753
+ LeaveCriticalSection(&g_host.lock);
754
+ return Qnil;
755
+ }
756
+
757
+ /* Winsvc._stop_requested -> true|false (lock-free interlocked read). */
758
+ static VALUE
759
+ stop_requested(VALUE mod)
760
+ {
761
+ (void)mod;
762
+ return InterlockedCompareExchange(&g_host.stop_flag, 0, 0) ? Qtrue : Qfalse;
763
+ }
764
+
765
+ /* Winsvc._dropped_count -> Integer (ring-overflow counter; diagnostics). */
766
+ static VALUE
767
+ dropped_count(VALUE mod)
768
+ {
769
+ unsigned d;
770
+ (void)mod;
771
+ EnterCriticalSection(&g_host.lock);
772
+ d = g_host.dropped;
773
+ LeaveCriticalSection(&g_host.lock);
774
+ return UINT2NUM(d);
775
+ }
776
+
777
+ /* --------------------------------------------------------- _host_done ------*/
778
+
779
+ typedef struct { HANDLE h; } join_t;
780
+
781
+ static void *
782
+ join_fn(void *p)
783
+ {
784
+ join_t *j = (join_t *)p;
785
+ WaitForSingleObject(j->h, INFINITE);
786
+ return NULL;
787
+ }
788
+
789
+ /* Winsvc._host_done(specific_exit) -> true. Record the exit code, SetEvent
790
+ * hRubyDone (ServiceMain reports STOPPED), then JOIN the dispatcher thread.
791
+ *
792
+ * NULL ubf, DELIBERATELY: hRubyDone is already set, so ServiceMain reports
793
+ * STOPPED and the dispatcher returns within milliseconds; this join MUST land
794
+ * so the process never outlives/abandons a live SCM connection (research trap
795
+ * 13). Bounded-in-practice, documented like phylax's PBKDF2.
796
+ *
797
+ * specific_exit: 0 ⇒ clean (NO_ERROR); nonzero ⇒ 1066 ERROR_SERVICE_SPECIFIC_ERROR
798
+ * with that dwServiceSpecificExitCode. In console mode the dispatcher already
799
+ * exited (1063), so the join returns immediately and no STOPPED is reported. */
800
+ static VALUE
801
+ host_done(VALUE mod, VALUE vspecific)
802
+ {
803
+ DWORD specific = NUM2ULONG(vspecific);
804
+ join_t j;
805
+ (void)mod;
806
+
807
+ EnterCriticalSection(&g_host.lock);
808
+ if (specific == 0) {
809
+ g_host.win32_exit = NO_ERROR;
810
+ g_host.specific_exit = 0;
811
+ } else {
812
+ g_host.win32_exit = ERROR_SERVICE_SPECIFIC_ERROR; /* 1066 */
813
+ g_host.specific_exit = specific;
814
+ }
815
+ LeaveCriticalSection(&g_host.lock);
816
+
817
+ SetEvent(g_host.hRubyDone);
818
+
819
+ if (g_host.hDispatcher) {
820
+ j.h = g_host.hDispatcher;
821
+ rb_thread_call_without_gvl(join_fn, &j, NULL, NULL);
822
+ CloseHandle(g_host.hDispatcher);
823
+ g_host.hDispatcher = NULL;
824
+ }
825
+ return Qtrue;
826
+ }
827
+
828
+ /* ============================================================================
829
+ * SCM-client primitives (§4). Each opens and closes every handle within the
830
+ * call; nothing outlives it. Handles are closed BEFORE any raise (the
831
+ * "nothing owned across a potential raise" invariant). GVL released for the
832
+ * LRPC calls (they can stall under the SCM database lock); NULL ubf (RPC is
833
+ * not per-thread cancelable; calls are bounded).
834
+ * ==========================================================================*/
835
+
836
+ /* ---- argument bundle for _install, marshalled in C before the no-GVL call --*/
837
+
838
+ typedef struct {
839
+ WCHAR *name, *display, *binpath, *description, *account, *password;
840
+ DWORD start_type; /* SERVICE_AUTO_START | SERVICE_DEMAND_START */
841
+ int delayed; /* delayed-auto flag */
842
+ int has_preshutdown;
843
+ DWORD preshutdown_ms;
844
+ int restart; /* configure failure actions */
845
+ DWORD restart_delay_ms, reset_secs;
846
+ int restart_on_non_crash;
847
+ /* outputs */
848
+ DWORD gle;
849
+ const char *failed_api; /* NULL on success */
850
+ } install_t;
851
+
852
+ /* Runs WITHOUT the GVL: no Ruby calls inside. All inputs are plain C. */
853
+ static void *
854
+ install_fn(void *p)
855
+ {
856
+ install_t *a = (install_t *)p;
857
+ SC_HANDLE scm = NULL, svc = NULL;
858
+ DWORD desired = SERVICE_CHANGE_CONFIG | SERVICE_START | DELETE;
859
+
860
+ a->failed_api = NULL;
861
+ a->gle = 0;
862
+
863
+ scm = OpenSCManagerW(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
864
+ if (!scm) { a->gle = GetLastError(); a->failed_api = "OpenSCManager"; return NULL; }
865
+
866
+ svc = CreateServiceW(scm, a->name, a->display, desired,
867
+ SERVICE_WIN32_OWN_PROCESS, a->start_type, SERVICE_ERROR_NORMAL,
868
+ a->binpath, NULL, NULL, NULL, a->account, a->password);
869
+ if (!svc) {
870
+ a->gle = GetLastError(); a->failed_api = "CreateService";
871
+ CloseServiceHandle(scm);
872
+ return NULL;
873
+ }
874
+
875
+ /* Config steps; any failure after creation rolls the service back. */
876
+ if (a->description) {
877
+ SERVICE_DESCRIPTIONW d;
878
+ d.lpDescription = a->description;
879
+ if (!ChangeServiceConfig2W(svc, SERVICE_CONFIG_DESCRIPTION, &d)) {
880
+ a->gle = GetLastError(); a->failed_api = "ChangeServiceConfig2(description)"; goto rollback;
881
+ }
882
+ }
883
+ if (a->delayed) {
884
+ SERVICE_DELAYED_AUTO_START_INFO di;
885
+ di.fDelayedAutostart = TRUE;
886
+ if (!ChangeServiceConfig2W(svc, SERVICE_CONFIG_DELAYED_AUTO_START_INFO, &di)) {
887
+ a->gle = GetLastError(); a->failed_api = "ChangeServiceConfig2(delayed)"; goto rollback;
888
+ }
889
+ }
890
+ if (a->has_preshutdown) {
891
+ SERVICE_PRESHUTDOWN_INFO pi;
892
+ pi.dwPreshutdownTimeout = a->preshutdown_ms;
893
+ if (!ChangeServiceConfig2W(svc, SERVICE_CONFIG_PRESHUTDOWN_INFO, &pi)) {
894
+ a->gle = GetLastError(); a->failed_api = "ChangeServiceConfig2(preshutdown)"; goto rollback;
895
+ }
896
+ }
897
+ if (a->restart) {
898
+ SC_ACTION actions[3];
899
+ SERVICE_FAILURE_ACTIONSW fa;
900
+ int i;
901
+ for (i = 0; i < 3; i++) {
902
+ actions[i].Type = SC_ACTION_RESTART;
903
+ actions[i].Delay = a->restart_delay_ms;
904
+ }
905
+ memset(&fa, 0, sizeof(fa));
906
+ fa.dwResetPeriod = a->reset_secs;
907
+ fa.lpRebootMsg = NULL;
908
+ fa.lpCommand = NULL;
909
+ fa.cActions = 3;
910
+ fa.lpsaActions = actions;
911
+ if (!ChangeServiceConfig2W(svc, SERVICE_CONFIG_FAILURE_ACTIONS, &fa)) {
912
+ a->gle = GetLastError(); a->failed_api = "ChangeServiceConfig2(failure_actions)"; goto rollback;
913
+ }
914
+ if (a->restart_on_non_crash) {
915
+ SERVICE_FAILURE_ACTIONS_FLAG ff;
916
+ ff.fFailureActionsOnNonCrashFailures = TRUE;
917
+ if (!ChangeServiceConfig2W(svc, SERVICE_CONFIG_FAILURE_ACTIONS_FLAG, &ff)) {
918
+ a->gle = GetLastError(); a->failed_api = "ChangeServiceConfig2(failure_flag)"; goto rollback;
919
+ }
920
+ }
921
+ }
922
+
923
+ CloseServiceHandle(svc);
924
+ CloseServiceHandle(scm);
925
+ return NULL;
926
+
927
+ rollback:
928
+ DeleteService(svc); /* best-effort: the create handle holds DELETE */
929
+ CloseServiceHandle(svc);
930
+ CloseServiceHandle(scm);
931
+ return NULL;
932
+ }
933
+
934
+ /* Winsvc._install(name, display, binpath, description|nil, account|nil,
935
+ * password|nil, start_type, delayed, preshutdown_ms|nil,
936
+ * restart, restart_delay_ms, reset_secs, on_non_crash) -> true */
937
+ static VALUE
938
+ svc_install(int argc, VALUE *argv, VALUE mod)
939
+ {
940
+ install_t a;
941
+ VALUE name, display, binpath, description, account, password;
942
+ VALUE start_type, delayed, preshutdown, restart, rdelay, reset, noncrash;
943
+ (void)mod;
944
+
945
+ if (argc != 13) rb_raise(rb_eArgError, "winsvc: _install expects 13 args, got %d", argc);
946
+ name = argv[0];
947
+ display = argv[1];
948
+ binpath = argv[2];
949
+ description = argv[3];
950
+ account = argv[4];
951
+ password = argv[5];
952
+ start_type = argv[6];
953
+ delayed = argv[7];
954
+ preshutdown = argv[8];
955
+ restart = argv[9];
956
+ rdelay = argv[10];
957
+ reset = argv[11];
958
+ noncrash = argv[12];
959
+
960
+ memset(&a, 0, sizeof(a));
961
+ /* Convert all strings up front (TypeError here owns no handle). */
962
+ a.name = to_wide(name);
963
+ a.display = to_wide(display);
964
+ a.binpath = to_wide(binpath);
965
+ a.description = NIL_P(description) ? NULL : to_wide(description);
966
+ a.account = NIL_P(account) ? NULL : to_wide(account);
967
+ a.password = NIL_P(password) ? NULL : to_wide(password);
968
+ a.start_type = NUM2ULONG(start_type);
969
+ a.delayed = RTEST(delayed) ? 1 : 0;
970
+ a.has_preshutdown = NIL_P(preshutdown) ? 0 : 1;
971
+ a.preshutdown_ms = NIL_P(preshutdown) ? 0 : NUM2ULONG(preshutdown);
972
+ a.restart = RTEST(restart) ? 1 : 0;
973
+ a.restart_delay_ms = NIL_P(rdelay) ? 0 : NUM2ULONG(rdelay);
974
+ a.reset_secs = NIL_P(reset) ? 0 : NUM2ULONG(reset);
975
+ a.restart_on_non_crash = RTEST(noncrash) ? 1 : 0;
976
+
977
+ rb_thread_call_without_gvl(install_fn, &a, NULL, NULL);
978
+
979
+ /* Free every wide buffer before any raise. */
980
+ xfree(a.name); xfree(a.display); xfree(a.binpath);
981
+ if (a.description) xfree(a.description);
982
+ if (a.account) xfree(a.account);
983
+ if (a.password) xfree(a.password);
984
+
985
+ if (a.failed_api) raise_gle(a.failed_api, a.gle);
986
+ return Qtrue;
987
+ }
988
+
989
+ /* ---- _service_state ------------------------------------------------------ */
990
+
991
+ typedef struct {
992
+ WCHAR *name;
993
+ DWORD state; /* SERVICE_* current state */
994
+ DWORD gle;
995
+ const char *failed_api;
996
+ } state_t;
997
+
998
+ static void *
999
+ state_fn(void *p)
1000
+ {
1001
+ state_t *a = (state_t *)p;
1002
+ SC_HANDLE scm = NULL, svc = NULL;
1003
+ SERVICE_STATUS_PROCESS ssp;
1004
+ DWORD needed = 0;
1005
+
1006
+ a->failed_api = NULL; a->gle = 0; a->state = 0;
1007
+
1008
+ scm = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT);
1009
+ if (!scm) { a->gle = GetLastError(); a->failed_api = "OpenSCManager"; return NULL; }
1010
+ svc = OpenServiceW(scm, a->name, SERVICE_QUERY_STATUS);
1011
+ if (!svc) { a->gle = GetLastError(); a->failed_api = "OpenService"; CloseServiceHandle(scm); return NULL; }
1012
+
1013
+ if (!QueryServiceStatusEx(svc, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssp,
1014
+ sizeof(ssp), &needed)) {
1015
+ a->gle = GetLastError(); a->failed_api = "QueryServiceStatusEx";
1016
+ } else {
1017
+ a->state = ssp.dwCurrentState;
1018
+ }
1019
+ CloseServiceHandle(svc);
1020
+ CloseServiceHandle(scm);
1021
+ return NULL;
1022
+ }
1023
+
1024
+ /* Winsvc._service_state(name) -> Integer (SERVICE_* state). Raises NotFound etc. */
1025
+ static VALUE
1026
+ svc_service_state(VALUE mod, VALUE name)
1027
+ {
1028
+ state_t a;
1029
+ (void)mod;
1030
+ memset(&a, 0, sizeof(a));
1031
+ a.name = to_wide(name);
1032
+ rb_thread_call_without_gvl(state_fn, &a, NULL, NULL);
1033
+ xfree(a.name);
1034
+ if (a.failed_api) raise_gle(a.failed_api, a.gle);
1035
+ return ULONG2NUM(a.state);
1036
+ }
1037
+
1038
+ /* ---- _control_stop ------------------------------------------------------- */
1039
+
1040
+ typedef struct {
1041
+ WCHAR *name;
1042
+ int result; /* 0 = sent, 1 = already stopped (1062), 2 = retry (1061) */
1043
+ DWORD gle;
1044
+ const char *failed_api;
1045
+ } ctlstop_t;
1046
+
1047
+ static void *
1048
+ ctlstop_fn(void *p)
1049
+ {
1050
+ ctlstop_t *a = (ctlstop_t *)p;
1051
+ SC_HANDLE scm = NULL, svc = NULL;
1052
+ SERVICE_STATUS ss;
1053
+
1054
+ a->failed_api = NULL; a->gle = 0; a->result = 0;
1055
+
1056
+ scm = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT);
1057
+ if (!scm) { a->gle = GetLastError(); a->failed_api = "OpenSCManager"; return NULL; }
1058
+ svc = OpenServiceW(scm, a->name, SERVICE_STOP);
1059
+ if (!svc) { a->gle = GetLastError(); a->failed_api = "OpenService"; CloseServiceHandle(scm); return NULL; }
1060
+
1061
+ if (!ControlService(svc, SERVICE_CONTROL_STOP, &ss)) {
1062
+ DWORD e = GetLastError();
1063
+ if (e == ERROR_SERVICE_NOT_ACTIVE) a->result = 1; /* 1062: already stopped */
1064
+ else if (e == ERROR_SERVICE_CANNOT_ACCEPT_CTRL) a->result = 2; /* 1061: pending — retry */
1065
+ else { a->gle = e; a->failed_api = "ControlService"; }
1066
+ }
1067
+ CloseServiceHandle(svc);
1068
+ CloseServiceHandle(scm);
1069
+ return NULL;
1070
+ }
1071
+
1072
+ /* Winsvc._control_stop(name) -> :sent | :stopped | :retry. Raises on real errors. */
1073
+ static VALUE
1074
+ svc_control_stop(VALUE mod, VALUE name)
1075
+ {
1076
+ ctlstop_t a;
1077
+ (void)mod;
1078
+ memset(&a, 0, sizeof(a));
1079
+ a.name = to_wide(name);
1080
+ rb_thread_call_without_gvl(ctlstop_fn, &a, NULL, NULL);
1081
+ xfree(a.name);
1082
+ if (a.failed_api) raise_gle(a.failed_api, a.gle);
1083
+ if (a.result == 1) return ID2SYM(rb_intern("stopped"));
1084
+ if (a.result == 2) return ID2SYM(rb_intern("retry"));
1085
+ return ID2SYM(rb_intern("sent"));
1086
+ }
1087
+
1088
+ /* ---- _delete_service ----------------------------------------------------- */
1089
+
1090
+ typedef struct {
1091
+ WCHAR *name;
1092
+ DWORD gle;
1093
+ const char *failed_api;
1094
+ } del_t;
1095
+
1096
+ static void *
1097
+ del_fn(void *p)
1098
+ {
1099
+ del_t *a = (del_t *)p;
1100
+ SC_HANDLE scm = NULL, svc = NULL;
1101
+
1102
+ a->failed_api = NULL; a->gle = 0;
1103
+
1104
+ scm = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT);
1105
+ if (!scm) { a->gle = GetLastError(); a->failed_api = "OpenSCManager"; return NULL; }
1106
+ svc = OpenServiceW(scm, a->name, DELETE);
1107
+ if (!svc) { a->gle = GetLastError(); a->failed_api = "OpenService"; CloseServiceHandle(scm); return NULL; }
1108
+ if (!DeleteService(svc)) { a->gle = GetLastError(); a->failed_api = "DeleteService"; }
1109
+ CloseServiceHandle(svc);
1110
+ CloseServiceHandle(scm);
1111
+ return NULL;
1112
+ }
1113
+
1114
+ /* Winsvc._delete_service(name) -> true. Raises NotFound / MarkedForDelete / etc. */
1115
+ static VALUE
1116
+ svc_delete_service(VALUE mod, VALUE name)
1117
+ {
1118
+ del_t a;
1119
+ (void)mod;
1120
+ memset(&a, 0, sizeof(a));
1121
+ a.name = to_wide(name);
1122
+ rb_thread_call_without_gvl(del_fn, &a, NULL, NULL);
1123
+ xfree(a.name);
1124
+ if (a.failed_api) raise_gle(a.failed_api, a.gle);
1125
+ return Qtrue;
1126
+ }
1127
+
1128
+ /* ----------------------------------------------------------------- Init --- */
1129
+
1130
+ void
1131
+ Init_winsvc(void)
1132
+ {
1133
+ mWinsvc = rb_define_module("Winsvc");
1134
+
1135
+ eError = rb_define_class_under(mWinsvc, "Error", rb_eStandardError);
1136
+ eOSError = rb_define_class_under(mWinsvc, "OSError", eError);
1137
+ eAccessDenied = rb_define_class_under(mWinsvc, "AccessDenied", eOSError);
1138
+ eExists = rb_define_class_under(mWinsvc, "Exists", eOSError);
1139
+ eNotFound = rb_define_class_under(mWinsvc, "NotFound", eOSError);
1140
+ eMarkedForDelete = rb_define_class_under(mWinsvc, "MarkedForDelete", eOSError);
1141
+ eTimeout = rb_define_class_under(mWinsvc, "TimeoutError", eOSError);
1142
+ eStateError = rb_define_class_under(mWinsvc, "StateError", eError);
1143
+
1144
+ /* Host bridges (validated by the Ruby layer; hidden via private_class_method). */
1145
+ rb_define_singleton_method(mWinsvc, "_host_start", host_start, 4);
1146
+ rb_define_singleton_method(mWinsvc, "_host_wait_mode", host_wait_mode, 0);
1147
+ rb_define_singleton_method(mWinsvc, "_host_args", host_args, 0);
1148
+ rb_define_singleton_method(mWinsvc, "_wait_control", wait_control, 1);
1149
+ rb_define_singleton_method(mWinsvc, "_inject", host_inject, 4);
1150
+ rb_define_singleton_method(mWinsvc, "_set_running", set_running, 0);
1151
+ rb_define_singleton_method(mWinsvc, "_set_paused", set_paused, 0);
1152
+ rb_define_singleton_method(mWinsvc, "_checkpoint", checkpoint, 1);
1153
+ rb_define_singleton_method(mWinsvc, "_stop_requested", stop_requested, 0);
1154
+ rb_define_singleton_method(mWinsvc, "_dropped_count", dropped_count, 0);
1155
+ rb_define_singleton_method(mWinsvc, "_host_done", host_done, 1);
1156
+
1157
+ /* SCM-client bridges. */
1158
+ rb_define_singleton_method(mWinsvc, "_install", svc_install, -1);
1159
+ rb_define_singleton_method(mWinsvc, "_service_state", svc_service_state, 1);
1160
+ rb_define_singleton_method(mWinsvc, "_control_stop", svc_control_stop, 1);
1161
+ rb_define_singleton_method(mWinsvc, "_delete_service", svc_delete_service, 1);
1162
+ }