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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +53 -0
- data/LICENSE.txt +21 -0
- data/README.md +270 -0
- data/ext/winsvc/extconf.rb +22 -0
- data/ext/winsvc/winsvc.c +1162 -0
- data/lib/winsvc/version.rb +5 -0
- data/lib/winsvc.rb +723 -0
- metadata +122 -0
data/ext/winsvc/winsvc.c
ADDED
|
@@ -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
|
+
}
|