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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b80eebd3a6fc287a6843a7fda819379f1db2fce75da360b1c2b1c3c81471f034
|
|
4
|
+
data.tar.gz: 2768b981eaf7131757821528dd9e61a3f2a2d3bcadf1e1241cb522b8151d5222
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4c5cb525932c94d6209358ec5ec0af346f65de1435ba7f71db8515f9fbaca6c563990b6f99a0be7c58569faba434855ab28d0941a642936060477d944da3eb5b
|
|
7
|
+
data.tar.gz: ca5e198736f677289ee45bdd8ca2c825af101e073ff36fe61d46678c071e774d38914fc2f6ac494f89fc841bb6b3fa9b350209bc259b6a0904fc113389a3e131
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-06-27
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- **`Winsvc.run(name, accept:, start_wait_hint:, stop_wait_hint:, manual_ready:,
|
|
8
|
+
log:) { |svc| ... }`** — host a Ruby process as a
|
|
9
|
+
`SERVICE_WIN32_OWN_PROCESS` Windows service. The control handler is pure C (no
|
|
10
|
+
Ruby ever runs on an SCM thread); controls are marshalled through a fixed-size
|
|
11
|
+
C ring + kernel event into a real Ruby pump thread that drains them onto a
|
|
12
|
+
`Thread::Queue`. The identical block runs unchanged as a **console program**
|
|
13
|
+
(dispatcher failure 1063 ⇒ console mode), where Ctrl-C/Ctrl-Break injects a
|
|
14
|
+
synthetic `:stop` and a second Ctrl-C terminates. `SERVICE_STOPPED` is reported
|
|
15
|
+
**exactly once**, from `ServiceMain`, after Ruby has fully unwound; every
|
|
16
|
+
status report executes atomically under one host critical section. A block
|
|
17
|
+
exception writes its backtrace to the log and flushes it **before** STOPPED,
|
|
18
|
+
then reports `1066`/specific `1` and re-raises.
|
|
19
|
+
- **`Winsvc::Service`** — the yielded object: `wait(timeout:)` (parks the fiber
|
|
20
|
+
under a scheduler, blocks the thread without one), `stop_requested?`
|
|
21
|
+
(interlocked, earlier than queue delivery), `each_control`, `ready!` /
|
|
22
|
+
`running!` / `paused!` / `checkpoint!(wait_hint:)` (no-ops in console mode,
|
|
23
|
+
after a latched stop, and after STOPPED — guarded under the host lock),
|
|
24
|
+
`args` (start parameters / ARGV), `service?` / `console?`.
|
|
25
|
+
- **`Winsvc::Control`** — a frozen struct (`control`, `event_type`, `session_id`,
|
|
26
|
+
`data`, `time`, `stop?`) for `:stop`, `:shutdown`, `:preshutdown`, `:pause`,
|
|
27
|
+
`:continue`, `:power` (raw `POWERBROADCAST_SETTING` bytes, ≤ 512, truncated),
|
|
28
|
+
and `:session_change` (`WTS_*` + session id).
|
|
29
|
+
- **`Winsvc.install` / `Winsvc.uninstall` / `Winsvc.install_command`** — a
|
|
30
|
+
minimal installer over `CreateServiceW` with `:demand` / `:auto` /
|
|
31
|
+
`:delayed_auto` start, account/password, description, `preshutdown_timeout:`,
|
|
32
|
+
and `restart_on_failure:` (three `SC_ACTION_RESTART` entries +
|
|
33
|
+
failure-actions-on-non-crash flag, composing with the 1066 exception exit).
|
|
34
|
+
`install` composes binPath with correct quoting and refuses embedded double
|
|
35
|
+
quotes; config-step failures roll the create back via `DeleteService`.
|
|
36
|
+
`uninstall` stops the service first (bounded poll, retrying 1061/1062) and
|
|
37
|
+
raises `TimeoutError` rather than zombifying a running service.
|
|
38
|
+
`install_command` emits the correctly quoted `sc.exe` incantation(s) and is a
|
|
39
|
+
pure function sharing the binPath composer with `install`.
|
|
40
|
+
- **Cooperative under [winloop](https://github.com/main-path/winloop)** with zero
|
|
41
|
+
winloop-specific code — the integration point is `Thread::Queue`. Blocking C
|
|
42
|
+
waits release the GVL with a cancel-capable unblock function, so `Thread#kill` /
|
|
43
|
+
Ctrl-C / VM shutdown break them.
|
|
44
|
+
- Error taxonomy under `Winsvc::Error`: `OSError` (with `#code`) → `AccessDenied`,
|
|
45
|
+
`Exists`, `NotFound`, `MarkedForDelete`, `TimeoutError`; plus `StateError` for
|
|
46
|
+
API misuse.
|
|
47
|
+
|
|
48
|
+
Pure C extension over advapi32 (+ kernel32) — `rb_raise`/longjmp-safe; native
|
|
49
|
+
state is a documented process singleton (one `StartServiceCtrlDispatcherW` per
|
|
50
|
+
process, ever). Requires Ruby **3.2+** (`Thread::Queue#pop(timeout:)`). Windows
|
|
51
|
+
MSVC (mswin) Ruby only.
|
|
52
|
+
|
|
53
|
+
[0.1.0]: https://github.com/main-path/winsvc/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ned
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# winsvc
|
|
2
|
+
|
|
3
|
+
**Host Ruby as a Windows service — real SCM integration with a console dev mode, no Ruby ever on a service-control thread.**
|
|
4
|
+
|
|
5
|
+
Shipping a Ruby program as a Windows service is a minefield. The service control
|
|
6
|
+
manager (SCM) gives your process **30 seconds** to connect to it or you get a
|
|
7
|
+
cryptic `Error 1053: The service did not respond to the start or control request
|
|
8
|
+
in a timely fashion`. Your control handler must return in well under 30 seconds
|
|
9
|
+
or the SCM declares the service hung. `STDOUT`/`STDERR` are invalid handles, so a
|
|
10
|
+
stray `puts` is a landmine. And `SetServiceStatus(SERVICE_STOPPED)` must be called
|
|
11
|
+
**exactly once** — a second call closes the RPC context handle and can crash the
|
|
12
|
+
process.
|
|
13
|
+
|
|
14
|
+
The existing `win32-service` gem is a cautionary tale: its control callbacks run
|
|
15
|
+
on **SCM-owned native threads**, calling into Ruby from threads MRI never created
|
|
16
|
+
(illegal-by-fragility — MRI cannot attach a foreign thread, so `ffi` papers over
|
|
17
|
+
it by spawning an ephemeral Ruby thread per callback). It **polls** for controls
|
|
18
|
+
once a second, and it always reports checkpoint and wait-hint `0`, so a slow stop
|
|
19
|
+
looks hung to the SCM.
|
|
20
|
+
|
|
21
|
+
winsvc fixes all of that. Its control handler is **pure C** — a memcpy, an event
|
|
22
|
+
signal, and an immediate return — drained by a real Ruby thread into a
|
|
23
|
+
`Thread::Queue`. No Ruby ever runs on an SCM thread. `SERVICE_STOPPED` is reported
|
|
24
|
+
exactly once, from `ServiceMain`, after Ruby has fully unwound. Checkpoints are
|
|
25
|
+
only ever honest (no background pumper faking progress). And because the
|
|
26
|
+
integration point is a `Thread::Queue`, the same body **cooperates with a fiber
|
|
27
|
+
scheduler** ([winloop](https://github.com/main-path/winloop)) when one is active
|
|
28
|
+
and works standalone otherwise. Best of all, the **identical block runs unchanged
|
|
29
|
+
as a console program** for development — `ruby service.rb` and Ctrl-C just work.
|
|
30
|
+
|
|
31
|
+
| What | API |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Run (host or console) | `Winsvc.run(name, accept:, ...) { |svc| ... }` |
|
|
34
|
+
| The yielded object | `svc.wait`, `svc.stop_requested?`, `svc.each_control`, `svc.ready!`, `svc.checkpoint!`, `svc.args` |
|
|
35
|
+
| A control message | `Winsvc::Control` (`#control`, `#event_type`, `#session_id`, `#data`, `#time`, `#stop?`) |
|
|
36
|
+
| Install | `Winsvc.install(name, script:, ...)` |
|
|
37
|
+
| Uninstall | `Winsvc.uninstall(name, timeout:)` |
|
|
38
|
+
| sc.exe generator | `Winsvc.install_command(name, script:, ...)` |
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- Windows + a native **MSVC (mswin) Ruby**, **3.2 or newer** (`Service#wait` uses
|
|
43
|
+
`Thread::Queue#pop(timeout:)`, which exists from Ruby 3.2).
|
|
44
|
+
- Not supported on MinGW/UCRT Ruby.
|
|
45
|
+
- Visual Studio 2017+ or the Build Tools with the **"Desktop development with
|
|
46
|
+
C++"** workload (cl.exe). No Developer Command Prompt is needed — the build
|
|
47
|
+
loads the MSVC environment automatically via
|
|
48
|
+
[vcvars](https://github.com/main-path/vcvars); point failures at `vcvars doctor`.
|
|
49
|
+
- Elevation (an Administrator shell) is needed **only** for `install` /
|
|
50
|
+
`uninstall`. Running and console mode need no special rights.
|
|
51
|
+
|
|
52
|
+
Supported platform: x64-mswin64. arm64-mswin is expected to work (all code is
|
|
53
|
+
arch-neutral) but is untested and unsupported until an arm64-mswin Ruby
|
|
54
|
+
distribution exists.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
gem install winsvc
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# service.rb — a minimal service. The IDENTICAL code runs as a console program.
|
|
66
|
+
require "winsvc" # require winsvc EARLY: the SCM gives the process 30 s to reach
|
|
67
|
+
# the dispatcher; heavy requires belong INSIDE the block.
|
|
68
|
+
|
|
69
|
+
Winsvc.run("myapp", log: "C:/ProgramData/myapp/service.log") do |svc|
|
|
70
|
+
require_relative "app" # heavy app boot AFTER START_PENDING is reported
|
|
71
|
+
app = MyApp.new(svc.args) # svc.args: start parameters (service) / ARGV (console)
|
|
72
|
+
app.start
|
|
73
|
+
|
|
74
|
+
control = svc.wait # parks here; Ctrl-C in a console injects :stop
|
|
75
|
+
svc.checkpoint! # honest progress while draining
|
|
76
|
+
app.drain_connections
|
|
77
|
+
app.stop
|
|
78
|
+
end # block returned ⇒ STOPPED reported, run returns
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
ruby service.rb # console mode: prints to your terminal, Ctrl-C stops it
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> **The 30-second rule.** A Ruby service pays interpreter boot + `require`s before
|
|
86
|
+
> reaching `Winsvc.run`. Keep the top of the script tiny: `require "winsvc"` and
|
|
87
|
+
> call `Winsvc.run` first; do heavy `require`s and app init **inside the block**
|
|
88
|
+
> (`START_PENDING` is already reported by then, and the start watchdog relaxes to
|
|
89
|
+
> 80 s + your wait hint). If you see `Error 1053` at `sc start`, this is why.
|
|
90
|
+
|
|
91
|
+
## Controls
|
|
92
|
+
|
|
93
|
+
`accept:` selects which SCM controls the service accepts. It must include `:stop`
|
|
94
|
+
(an unstoppable service is a footgun).
|
|
95
|
+
|
|
96
|
+
| Symbol | Accept bit | Default | Notes |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| `:stop` | `SERVICE_ACCEPT_STOP` | **on** | required |
|
|
99
|
+
| `:shutdown` | `SERVICE_ACCEPT_SHUTDOWN` | **on** | budget ≈ 5 s (`WaitToKillServiceTimeout`); persist fast, skip niceties |
|
|
100
|
+
| `:preshutdown` | `SERVICE_ACCEPT_PRESHUTDOWN` | opt-in | default timeout 10 s since Win10 1703; raise it with `preshutdown_timeout:`. A service that accepts preshutdown will **not** also receive shutdown |
|
|
101
|
+
| `:pause_continue` | `SERVICE_ACCEPT_PAUSE_CONTINUE` | opt-in | call `svc.paused!` after `:pause`, `svc.running!` after `:continue` |
|
|
102
|
+
| `:power` | `SERVICE_ACCEPT_POWEREVENT` | opt-in | `Control#event_type` is `PBT_*`; raw `POWERBROADCAST_SETTING` bytes in `#data` |
|
|
103
|
+
| `:session_change` | `SERVICE_ACCEPT_SESSIONCHANGE` | opt-in | `Control#event_type` is `WTS_*`, `#session_id` is the session |
|
|
104
|
+
|
|
105
|
+
Each `svc.wait` returns the next `Winsvc::Control` (or `nil` on timeout / after
|
|
106
|
+
teardown). `svc.each_control` is sugar — it `wait`s in a loop, yields every
|
|
107
|
+
control including the final stop-class one, then returns it:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
Winsvc.run("watcher", accept: [:stop, :shutdown, :pause_continue, :session_change],
|
|
111
|
+
manual_ready: true) do |svc|
|
|
112
|
+
state = warm_caches { svc.checkpoint! } # progress during slow init
|
|
113
|
+
svc.ready! # now RUNNING; the SCM stop button works
|
|
114
|
+
svc.each_control do |c|
|
|
115
|
+
case c.control
|
|
116
|
+
when :pause then state.quiesce; svc.paused!
|
|
117
|
+
when :continue then state.resume; svc.running!
|
|
118
|
+
when :session_change then log "session #{c.session_id}: WTS event #{c.event_type}"
|
|
119
|
+
end # :stop/:shutdown end the loop after the yield
|
|
120
|
+
end
|
|
121
|
+
state.shutdown
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For tight work loops that never call `wait`, poll `svc.stop_requested?` — it is
|
|
126
|
+
true from the instant the C handler saw a stop, earlier than queue delivery.
|
|
127
|
+
|
|
128
|
+
## Installing the service
|
|
129
|
+
|
|
130
|
+
`install` / `uninstall` require an elevated shell; they map
|
|
131
|
+
`ERROR_ACCESS_DENIED` to a clear `Winsvc::AccessDenied` ("run elevated").
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
require "winsvc"
|
|
135
|
+
Winsvc.install("myapp",
|
|
136
|
+
script: "C:/app/service.rb",
|
|
137
|
+
display_name: "My Application",
|
|
138
|
+
description: "Does app things.",
|
|
139
|
+
start: :delayed_auto, # :demand (default) | :auto | :delayed_auto
|
|
140
|
+
account: "NT AUTHORITY\\LocalService", # nil ⇒ LocalSystem
|
|
141
|
+
restart_on_failure: true) # three RESTART actions; recovers on crash + nonzero exit
|
|
142
|
+
# => true (raises Winsvc::AccessDenied unless elevated)
|
|
143
|
+
|
|
144
|
+
Winsvc.uninstall("myapp") # stops it first (bounded), then deletes
|
|
145
|
+
# => true
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`restart_on_failure: true` is shorthand for `{ delay: 5_000, reset: 86_400,
|
|
149
|
+
on_non_crash: true }`: three `SC_ACTION_RESTART` entries at `delay` ms each, a
|
|
150
|
+
`reset` second reset period, and the failure-actions flag that makes recovery fire
|
|
151
|
+
on winsvc's own exception exit (see Logging). A queued restart **cannot be
|
|
152
|
+
canceled** — a manually stopped service can restart unexpectedly when the delay
|
|
153
|
+
elapses.
|
|
154
|
+
|
|
155
|
+
When your deployment tooling insists on `sc.exe`, generate the correctly quoted
|
|
156
|
+
incantation (this gets the two community traps right: the mandatory space after
|
|
157
|
+
every `option=`, and binPath's nested quoting):
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
puts Winsvc.install_command("myapp", script: "C:/app/service.rb")
|
|
161
|
+
# => sc create myapp binpath= "\"C:\ruby\bin\ruby.exe\" \"C:\app\service.rb\"" start= demand
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`install_command` is a pure function — no OS calls, no elevation — and shares the
|
|
165
|
+
binPath composer with `install`, so the two can never drift. Passwords appear in
|
|
166
|
+
plain text in the returned string.
|
|
167
|
+
|
|
168
|
+
**MarkedForDelete.** `DeleteService` only *marks* a service for deletion; the
|
|
169
|
+
entry lingers until every open handle closes (an open services.msc is the classic
|
|
170
|
+
cause), and recreating it before then raises `Winsvc::MarkedForDelete`. Close the
|
|
171
|
+
holder and retry.
|
|
172
|
+
|
|
173
|
+
## Logging
|
|
174
|
+
|
|
175
|
+
Services run in **session 0** with no console, so the three standard streams are
|
|
176
|
+
invalid handles — an unredirected `puts` is a landmine. In **service mode** winsvc
|
|
177
|
+
reopens them before your code runs (this is silently ignored in console mode, so
|
|
178
|
+
the identical code runs in both):
|
|
179
|
+
|
|
180
|
+
- `log: nil` (default): `STDIN`/`STDOUT`/`STDERR` → `NUL`.
|
|
181
|
+
- `log: "C:/path/to.log"`: `STDOUT`/`STDERR` append to the file (`sync = true`);
|
|
182
|
+
`STDIN` → `NUL`. A bad path fails the start visibly instead of running mute.
|
|
183
|
+
- `log: some_io`: `STDOUT`/`STDERR` → that IO.
|
|
184
|
+
|
|
185
|
+
If the block raises, winsvc writes the exception class, message, and backtrace to
|
|
186
|
+
the redirected stderr and **flushes it before `STOPPED` is reported** — the
|
|
187
|
+
diagnostic is guaranteed, not left to best-effort at-exit printing. It then reports
|
|
188
|
+
`STOPPED` with `dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR (1066)` and
|
|
189
|
+
`dwServiceSpecificExitCode = 1`, so the SCM writes System event log **EventID
|
|
190
|
+
7023** and `restart_on_failure: { on_non_crash: true }` recovery fires.
|
|
191
|
+
|
|
192
|
+
For structured logging, point your app's logger at the file too; ETW/event-log
|
|
193
|
+
emission is [winlog](https://github.com/main-path/winlog)'s job, not winsvc's.
|
|
194
|
+
|
|
195
|
+
## Cooperative under winloop
|
|
196
|
+
|
|
197
|
+
winloop is **not** a dependency and is never required; cooperation is structural —
|
|
198
|
+
`Service#wait` is `Thread::Queue#pop`, which parks the calling **fiber** under a
|
|
199
|
+
live `Fiber.scheduler` and blocks the **thread** without one. Identical code,
|
|
200
|
+
correct either way.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
require "winsvc"
|
|
204
|
+
require "winloop"
|
|
205
|
+
|
|
206
|
+
Winsvc.run("echo") do |svc| # Winsvc.run is the OUTERMOST frame, wrapping Winloop.run
|
|
207
|
+
Winloop.run do
|
|
208
|
+
server = TCPServer.new("127.0.0.1", 9292)
|
|
209
|
+
Fiber.schedule do
|
|
210
|
+
svc.wait # fiber parks; the pump's push wakes the IOCP loop
|
|
211
|
+
server.close # unblocks the accept fiber ⇒ all fibers can finish
|
|
212
|
+
end
|
|
213
|
+
Fiber.schedule do
|
|
214
|
+
loop { conn = server.accept; Fiber.schedule { conn.write(conn.readpartial(4096)); conn.close } }
|
|
215
|
+
rescue IOError # server closed — done
|
|
216
|
+
end
|
|
217
|
+
end # Winloop.run returns when every fiber finished
|
|
218
|
+
end # ⇒ STOPPED
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**The stop contract.** `Winloop.run` returns only when every scheduled fiber
|
|
222
|
+
finishes — there is no force-stop. So dedicate one fiber to `svc.wait`; when it
|
|
223
|
+
returns a stop-class control, make all other fibers terminate (close listeners,
|
|
224
|
+
cancel/drain connection fibers). During a long drain, any fiber may call
|
|
225
|
+
`svc.checkpoint!` (a cheap non-blocking call on the loop thread) to keep the SCM's
|
|
226
|
+
`STOP_PENDING` wait hint honest. Call `Winsvc.run` from the main thread, *outside*
|
|
227
|
+
`Winloop.run` — never inside a `Fiber.schedule`.
|
|
228
|
+
|
|
229
|
+
## Errors
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
StandardError
|
|
233
|
+
└─ Winsvc::Error
|
|
234
|
+
├─ Winsvc::OSError # a Windows API failed; #code is GetLastError
|
|
235
|
+
│ ├─ Winsvc::AccessDenied # 5 — run elevated
|
|
236
|
+
│ ├─ Winsvc::Exists # 1073 / 1078 — name taken
|
|
237
|
+
│ ├─ Winsvc::NotFound # 1060 — no such service
|
|
238
|
+
│ ├─ Winsvc::MarkedForDelete # 1072 — a previous delete is still pending
|
|
239
|
+
│ └─ Winsvc::TimeoutError # uninstall stop-wait deadline exceeded
|
|
240
|
+
└─ Winsvc::StateError # API misuse (run called twice, run without a block)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`OSError#code` returns the captured Win32 error. Plain argument errors raise
|
|
244
|
+
Ruby's own `ArgumentError`/`TypeError`.
|
|
245
|
+
|
|
246
|
+
## Notes
|
|
247
|
+
|
|
248
|
+
- **`STOPPED` is reported exactly once**, from `ServiceMain`, after Ruby has fully
|
|
249
|
+
unwound — guaranteed by construction (the only report site) plus an interlocked
|
|
250
|
+
guard set under the same critical section as the report. A stray `checkpoint!`
|
|
251
|
+
from a leaked thread, or a stop the SCM delivers mid-unwind, can never make the
|
|
252
|
+
crash-inducing second report.
|
|
253
|
+
- **No background checkpoints, by design.** Checkpoints fire only when your code
|
|
254
|
+
calls `checkpoint!`, and only while a transition is pending — the documented
|
|
255
|
+
anti-pattern (a timer thread faking progress) is exactly what hangs starts.
|
|
256
|
+
- **One `Winsvc.run` per process** (Win32: one `StartServiceCtrlDispatcherW` per
|
|
257
|
+
process). A second call raises `Winsvc::StateError`.
|
|
258
|
+
- **Do not call `exit!` or `exec` inside the block** — winsvc must join the
|
|
259
|
+
dispatcher so the process never exits while the SCM connection is live.
|
|
260
|
+
Anything that must happen on stop belongs inside the block, before it returns
|
|
261
|
+
(post-`STOPPED` Ruby, including `at_exit`, is best-effort by MS's own docs).
|
|
262
|
+
- **Session 0**: no UI, no interactivity. Use `:session_change` to observe user
|
|
263
|
+
sessions. A service is only notified of a logon if it is fully loaded before the
|
|
264
|
+
logon attempt.
|
|
265
|
+
- Supported on x64-mswin64. All code is arch-neutral; arm64-mswin is expected to
|
|
266
|
+
work but is untested until an arm64-mswin Ruby exists.
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
[MIT](LICENSE.txt).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# extconf.rb for the winsvc extension — hosts a Ruby process as a Windows
|
|
4
|
+
# service (SERVICE_WIN32_OWN_PROCESS) with correct SCM integration.
|
|
5
|
+
|
|
6
|
+
require "mkmf"
|
|
7
|
+
|
|
8
|
+
unless RbConfig::CONFIG["target_os"] =~ /mswin/
|
|
9
|
+
abort <<~MSG
|
|
10
|
+
winsvc requires a native Windows MSVC (mswin) Ruby — it binds the Windows
|
|
11
|
+
service control manager (StartServiceCtrlDispatcherW / SetServiceStatus /
|
|
12
|
+
CreateServiceW) and is built with cl.exe. Your Ruby is "#{RbConfig::CONFIG['arch']}".
|
|
13
|
+
MSG
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Pure C — no -EHsc, so rb_raise/longjmp is the normal, safe error mechanism.
|
|
17
|
+
# System import libs (bare "NAME.lib" tokens on mswin); kernel32 (events, CS,
|
|
18
|
+
# _beginthreadex via the CRT) is linked by default:
|
|
19
|
+
# advapi32 - all winsvc.h APIs (dispatcher, handler registration, status, SCM client)
|
|
20
|
+
$libs = [$libs, "advapi32.lib"].join(" ")
|
|
21
|
+
|
|
22
|
+
create_makefile("winsvc/winsvc")
|