yamine 0.8.2 → 0.9.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 +4 -4
- data/CHANGELOG.md +39 -0
- data/lib/yamine/cli/context.rb +1 -4
- data/lib/yamine/cli/system.rb +17 -1
- data/lib/yamine/doctor.rb +42 -1
- data/lib/yamine/proxy_control.rb +72 -3
- data/lib/yamine/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65dc796c2e121f6376d6a76b1baa7bb1ada3598c7facf8634262c83915c9cf2c
|
|
4
|
+
data.tar.gz: 0a2cea7a624133d68512b79e5e1060a96337a7da4948504313ea24ba6ecce080
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1f73c73193f3e9c4eae8615ad0b790111bd2c6f29615a19f35c380e99db2d2a1a239e38b10c9328795a6f1c7cf1b28b0ecce721cb5414710769ba63a99b622b
|
|
7
|
+
data.tar.gz: 2200ab3b518fac757902b25e7ec2e6933d08e2183830aba2bbf5d893622b2ded021cc7bf4f07064e6efe9ff7500dad408ea73f7bc30a4c83c25b645829bdfdf2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0] — 2026-09-10
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The service proxy now records its own state.** The launchd/systemd
|
|
8
|
+
service runs `proxy start --foreground`, and that path recorded
|
|
9
|
+
nothing: `write_pid` was reachable only from `spawn_daemon`. So after
|
|
10
|
+
`yamine setup`, the pid/port/scheme files still held the PREVIOUS
|
|
11
|
+
daemon's values. On a machine whose root service was serving clean
|
|
12
|
+
`https://<app>.localhost` on 443, the recorded port still said 8443 —
|
|
13
|
+
`yamine start` printed and baked `:8443` into `YAMINE_URL`, doctor
|
|
14
|
+
warned about the long-dead 8443, and `proxy stop` aimed at a pid that
|
|
15
|
+
was already gone. `setup` even contradicted itself in one run:
|
|
16
|
+
"proxy is up on port 443" followed by "listening on port 8443". The
|
|
17
|
+
foreground path now writes pid/port/tls plus a new `proxy.version`
|
|
18
|
+
marker BEFORE it serves.
|
|
19
|
+
- The launchd plist and systemd unit pass `--port 443` explicitly. They
|
|
20
|
+
relied on the default, which consults `YAMINE_PORT` — one stray export
|
|
21
|
+
would silently move the boot service off 443.
|
|
22
|
+
- **`proxy stop` no longer claims success on a root-owned proxy.** The
|
|
23
|
+
kill raised `Errno::EPERM`, which was swallowed by a blanket rescue:
|
|
24
|
+
the state files were cleared and "Proxy stopped." printed while the
|
|
25
|
+
service kept serving — now with nothing on disk to find or stop it by.
|
|
26
|
+
It reports `needs_root` and points at `sudo yamine service uninstall`.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `doctor` gains a `proxy state` check and now finds the proxy that is
|
|
31
|
+
actually serving (`ProxyControl.serving_port`, ownership-proven)
|
|
32
|
+
instead of trusting the recorded port. It warns when the recorded port
|
|
33
|
+
disagrees with reality, and when the running proxy is an older gem
|
|
34
|
+
version than the CLI (the root service keeps serving old code after an
|
|
35
|
+
upgrade until `sudo yamine service install` re-registers it).
|
|
36
|
+
- The clean default port wins when our proxy serves there, so a leftover
|
|
37
|
+
daemon on another port can no longer make the machine report a
|
|
38
|
+
downgraded URL as its state. A non-default port is still reported when
|
|
39
|
+
it is the only proxy — the deliberate CI/sandbox case.
|
|
40
|
+
|
|
41
|
+
|
|
3
42
|
## [0.8.2] — 2026-09-10
|
|
4
43
|
|
|
5
44
|
### Fixed
|
data/lib/yamine/cli/context.rb
CHANGED
|
@@ -28,10 +28,7 @@ module Yamine
|
|
|
28
28
|
# listening there. A dead proxy leaves the machine on the clean
|
|
29
29
|
# default (443), and an explicit YAMINE_PORT always wins.
|
|
30
30
|
def proxy_port
|
|
31
|
-
|
|
32
|
-
return recorded if recorded && ProxyControl.listening?(recorded)
|
|
33
|
-
|
|
34
|
-
ProxyControl.default_port(proxy_tls)
|
|
31
|
+
ProxyControl.active_port(store)
|
|
35
32
|
end
|
|
36
33
|
|
|
37
34
|
def proxy_tls
|
data/lib/yamine/cli/system.rb
CHANGED
|
@@ -70,6 +70,14 @@ module Yamine
|
|
|
70
70
|
if foreground
|
|
71
71
|
write_tls_marker(ctx, tls)
|
|
72
72
|
write_tlds_file(ctx, tlds)
|
|
73
|
+
# Record BEFORE serving. This process (the launchd/systemd
|
|
74
|
+
# service, or a hand-run foreground proxy) IS the proxy, and
|
|
75
|
+
# every reader trusts these files: doctor, Context's URL
|
|
76
|
+
# building, the supervisor, `proxy stop`. Not recording left
|
|
77
|
+
# the previous proxy's port on disk, so a machine serving
|
|
78
|
+
# clean 443 still advertised :8443 everywhere.
|
|
79
|
+
ProxyControl.write_proxy_state(ctx.store,
|
|
80
|
+
pid: Process.pid, port: port, tls: tls)
|
|
73
81
|
sup = Supervisor.new(store: ctx.store, runner: Runner.new(store: ctx.store),
|
|
74
82
|
on_event: ->(m) { warn m })
|
|
75
83
|
sup.start
|
|
@@ -85,6 +93,13 @@ module Yamine
|
|
|
85
93
|
when :stale then puts "Removed stale proxy state."
|
|
86
94
|
when :not_running then puts "Proxy is not running."
|
|
87
95
|
when :unknown_process then puts "Port in use by an unknown process."
|
|
96
|
+
when :needs_root
|
|
97
|
+
# A root service keeps serving; say so rather than implying
|
|
98
|
+
# success. Uninstalling is the real answer — signalling it
|
|
99
|
+
# would just leave launchd's KeepAlive restarting it.
|
|
100
|
+
$stderr.puts "The proxy runs as root (the launchd/systemd service)."
|
|
101
|
+
$stderr.puts " Uninstall it: sudo yamine service uninstall"
|
|
102
|
+
exit 1
|
|
88
103
|
end
|
|
89
104
|
else
|
|
90
105
|
raise Error, "Usage: yamine proxy [start|stop]"
|
|
@@ -282,6 +297,7 @@ module Yamine
|
|
|
282
297
|
<string>#{RbConfig.ruby}</string>
|
|
283
298
|
<string>#{ProxyControl.bin_path}</string>
|
|
284
299
|
<string>proxy</string><string>start</string><string>--foreground</string>
|
|
300
|
+
<string>--port</string><string>#{ProxyControl::DEFAULT_TLS_PORT}</string>
|
|
285
301
|
</array>
|
|
286
302
|
<key>EnvironmentVariables</key>
|
|
287
303
|
<dict>
|
|
@@ -357,7 +373,7 @@ module Yamine
|
|
|
357
373
|
After=network.target
|
|
358
374
|
|
|
359
375
|
[Service]
|
|
360
|
-
ExecStart=#{RbConfig.ruby} #{ProxyControl.bin_path} proxy start --foreground
|
|
376
|
+
ExecStart=#{RbConfig.ruby} #{ProxyControl.bin_path} proxy start --foreground --port #{ProxyControl::DEFAULT_TLS_PORT}
|
|
361
377
|
Environment=YAMINE_STATE_DIR=#{state_dir}
|
|
362
378
|
Environment=HOME=#{home}
|
|
363
379
|
|
data/lib/yamine/doctor.rb
CHANGED
|
@@ -18,12 +18,18 @@ module Yamine
|
|
|
18
18
|
|
|
19
19
|
def run(store:, port: nil, tls: nil)
|
|
20
20
|
checks = []
|
|
21
|
-
port ||= ProxyControl.proxy_port(store)
|
|
22
21
|
tls = ProxyControl.proxy_tls(store) if tls.nil?
|
|
22
|
+
# Prove ownership rather than trusting the port file: doctor is
|
|
23
|
+
# where a wrong answer is expensive, and a connection is cheap. This
|
|
24
|
+
# finds the proxy on the scheme default even when a stale port file
|
|
25
|
+
# says otherwise, which is how a root service serving clean 443 went
|
|
26
|
+
# unreported while doctor warned about a dead 8443.
|
|
27
|
+
port ||= ProxyControl.serving_port(store)
|
|
23
28
|
|
|
24
29
|
checks << check_state_dir(store)
|
|
25
30
|
checks << check_disk(store)
|
|
26
31
|
checks << check_proxy(port, tls: tls)
|
|
32
|
+
checks << check_proxy_state(store, port)
|
|
27
33
|
checks << check_routes(store)
|
|
28
34
|
checks << check_dns(store)
|
|
29
35
|
checks << check_ca
|
|
@@ -93,6 +99,41 @@ module Yamine
|
|
|
93
99
|
end
|
|
94
100
|
end
|
|
95
101
|
|
|
102
|
+
# The recorded port, and whether the proxy serving is the version we
|
|
103
|
+
# have installed.
|
|
104
|
+
#
|
|
105
|
+
# A file that disagrees with reality is the failure mode this whole
|
|
106
|
+
# area keeps producing: a stopped daemon's port outliving it made
|
|
107
|
+
# `yamine start` bake :8443 into URLs while the machine served clean
|
|
108
|
+
# 443, and a root service left from an old gem keeps serving old code
|
|
109
|
+
# after an upgrade (`sudo yamine service install` re-registers it).
|
|
110
|
+
# Neither is visible from the URL bar, so both are named here.
|
|
111
|
+
def check_proxy_state(store, serving)
|
|
112
|
+
recorded = ProxyControl.proxy_port(store)
|
|
113
|
+
version = ProxyControl.proxy_version(store)
|
|
114
|
+
problems = []
|
|
115
|
+
|
|
116
|
+
if recorded && serving && recorded != serving
|
|
117
|
+
problems << "state says port #{recorded} but the proxy is on #{serving}" \
|
|
118
|
+
" — run: yamine proxy stop"
|
|
119
|
+
elsif recorded && serving.nil?
|
|
120
|
+
problems << "state says port #{recorded}, but no yamine proxy is serving" \
|
|
121
|
+
" — run: yamine proxy stop"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if version && version != Yamine::VERSION
|
|
125
|
+
problems << "the running proxy is v#{version}, this CLI is v#{Yamine::VERSION}" \
|
|
126
|
+
" — re-register it: sudo yamine service install"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if problems.empty?
|
|
130
|
+
Check.new(name: "proxy state", ok: true, message: "consistent")
|
|
131
|
+
else
|
|
132
|
+
Check.new(name: "proxy state", ok: true, warn: true,
|
|
133
|
+
message: problems.join("; "))
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
96
137
|
def check_routes(store)
|
|
97
138
|
routes = store.load_routes
|
|
98
139
|
if routes.empty?
|
data/lib/yamine/proxy_control.rb
CHANGED
|
@@ -156,17 +156,33 @@ module Yamine
|
|
|
156
156
|
nil
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
# Record this process as THE running proxy: pid, port, scheme, and the
|
|
160
|
+
# gem version serving it.
|
|
161
|
+
#
|
|
162
|
+
# Every reader trusts these files — doctor reports the port, Context
|
|
163
|
+
# and the supervisor build URLs from it, `proxy stop` signals the pid
|
|
164
|
+
# — so a proxy that serves without recording is worse than one that
|
|
165
|
+
# fails to start: it leaves the previous proxy's state on disk and the
|
|
166
|
+
# whole CLI acts on it. The launchd/systemd service used to take
|
|
167
|
+
# exactly that path (it runs `proxy start --foreground`, which
|
|
168
|
+
# recorded nothing), so a stopped 8443 daemon's port stayed on disk
|
|
169
|
+
# and `yamine start` baked :8443 into URLs while the machine served
|
|
170
|
+
# clean https://<app>.localhost on 443.
|
|
171
|
+
def write_proxy_state(store, pid:, port:, tls:)
|
|
160
172
|
store.ensure_dir
|
|
173
|
+
version_path = File.join(store.dir, "proxy.version")
|
|
161
174
|
File.write(store.pid_path, "#{pid}\n")
|
|
162
175
|
File.write(store.port_path, "#{port}\n")
|
|
163
176
|
File.write(File.join(store.dir, "proxy.tls"), tls ? "1" : "0")
|
|
164
|
-
|
|
177
|
+
File.write(version_path, "#{Yamine::VERSION}\n")
|
|
178
|
+
Ownership.fix(store.pid_path, store.port_path,
|
|
179
|
+
File.join(store.dir, "proxy.tls"), version_path)
|
|
165
180
|
end
|
|
166
181
|
|
|
167
182
|
def clear_pid(store)
|
|
168
183
|
FileUtils.rm_f(store.pid_path)
|
|
169
184
|
FileUtils.rm_f(store.port_path)
|
|
185
|
+
FileUtils.rm_f(File.join(store.dir, "proxy.version"))
|
|
170
186
|
end
|
|
171
187
|
|
|
172
188
|
def proxy_port(store)
|
|
@@ -178,6 +194,54 @@ module Yamine
|
|
|
178
194
|
nil
|
|
179
195
|
end
|
|
180
196
|
|
|
197
|
+
# The gem version of the proxy that recorded its state, or nil when
|
|
198
|
+
# nothing is recorded. Compared against Yamine::VERSION so a service
|
|
199
|
+
# left behind by an older install is visible instead of silently
|
|
200
|
+
# serving stale code.
|
|
201
|
+
def proxy_version(store)
|
|
202
|
+
path = File.join(store.dir, "proxy.version")
|
|
203
|
+
return nil unless File.file?(path)
|
|
204
|
+
|
|
205
|
+
version = File.read(path).strip
|
|
206
|
+
version.empty? ? nil : version
|
|
207
|
+
rescue SystemCallError
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Cheap resolution for the hot paths (URL building on every boot):
|
|
212
|
+
# the recorded port while something is listening there, else the
|
|
213
|
+
# scheme default. Proves liveness, not ownership — a foreign process
|
|
214
|
+
# on the recorded port still wins here and is then caught by
|
|
215
|
+
# `ensure_proxy!` ("port in use by another process"). Use
|
|
216
|
+
# `serving_port` when the answer must be OUR proxy.
|
|
217
|
+
def active_port(store)
|
|
218
|
+
recorded = proxy_port(store)
|
|
219
|
+
return recorded if recorded && listening?(recorded)
|
|
220
|
+
|
|
221
|
+
default_port(proxy_tls(store))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Where OUR proxy is actually serving, or nil when it is not running.
|
|
225
|
+
# Proves ownership, so it costs a probe — doctor-only, where an
|
|
226
|
+
# occasional connection is free and a wrong answer is not.
|
|
227
|
+
#
|
|
228
|
+
# The default port wins when our proxy serves there, because that is
|
|
229
|
+
# the configuration yamine exists to produce: after `yamine setup`
|
|
230
|
+
# installs the 443 service, a leftover daemon on another port must not
|
|
231
|
+
# make doctor report the downgraded URL as the state of the machine.
|
|
232
|
+
# Only when the default has no proxy do we report the recorded port —
|
|
233
|
+
# which is the deliberate CI/sandbox case (`proxy start -p 1355`).
|
|
234
|
+
def serving_port(store)
|
|
235
|
+
tls = proxy_tls(store)
|
|
236
|
+
default = default_port(tls)
|
|
237
|
+
return default if ours?(default, tls: tls)
|
|
238
|
+
|
|
239
|
+
recorded = proxy_port(store)
|
|
240
|
+
return recorded if recorded && recorded != default && ours?(recorded, tls: tls)
|
|
241
|
+
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
|
|
181
245
|
# Start the proxy as a detached daemon. Sudo is used for privileged
|
|
182
246
|
# ports (portless auto-elevate pattern); the state dir is passed
|
|
183
247
|
# explicitly because sudo does not preserve the environment.
|
|
@@ -205,7 +269,7 @@ module Yamine
|
|
|
205
269
|
|
|
206
270
|
sleep 0.25
|
|
207
271
|
end
|
|
208
|
-
|
|
272
|
+
write_proxy_state(store, pid: pid, port: port, tls: tls)
|
|
209
273
|
pid
|
|
210
274
|
end
|
|
211
275
|
|
|
@@ -231,6 +295,11 @@ module Yamine
|
|
|
231
295
|
end
|
|
232
296
|
begin
|
|
233
297
|
Process.kill("TERM", pid)
|
|
298
|
+
rescue Errno::EPERM
|
|
299
|
+
# The proxy runs as root (launchd/systemd service) and we do not.
|
|
300
|
+
# Clearing the state here would be a lie: the proxy keeps serving,
|
|
301
|
+
# now with nothing on disk to find or stop it by.
|
|
302
|
+
return :needs_root
|
|
234
303
|
rescue SystemCallError
|
|
235
304
|
clear_pid(store)
|
|
236
305
|
return :stale
|
data/lib/yamine/version.rb
CHANGED