yamine 0.8.2 → 0.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52c2213e83a301befe5e048b7a83eec6e6c945f23475334928a298f81d9e8369
4
- data.tar.gz: 39e2c51e536b6fa3d50a956bce2b20187899bf52a2c86a3b82ab110877a485fc
3
+ metadata.gz: 5059de08c17cccfe2d24ce5217a2c3c70fb5cc004a6eb528856eedf9b5389409
4
+ data.tar.gz: 19da6abd533f3f80cbdfda8553427ed987e1ba18f83d24cf37934c906d03caef
5
5
  SHA512:
6
- metadata.gz: 543d3ba0a552e5cc2deef0784322d225c99a45ea19e35af090f6d5691d5cfc5b9637e8831755db5509e024a352fa33de70843aef2734353acf9bacfc867e805a
7
- data.tar.gz: 57213387e578ba71b6d4140dd139d10fe2e7bf5168fe74ad85a3ba2b87089b69959284fc7f531f38034838dc67cb70b86b92f13b926aa638c1291a36bdbb1376
6
+ metadata.gz: 8a31f0124a322242398ef9e416fe46a7dd6ab13aac2265fb6ebc00743da14b82ef20ef30af8f3f018627bf6534779e074173a4795936c392de6b1df41c654a6a
7
+ data.tar.gz: 922a0dd3ce552a6a76f931ae50385a5f98bf5d87b7fcee13e44367b209c193dad3be8a526e23585950251251a10e8cbc324375ba71d06291c2d12964441a48ac
data/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.1] — 2026-09-10
4
+
5
+ ### Fixed
6
+
7
+ - **`proxy stop` no longer says "not running" while a root service is
8
+ serving.** With nothing recorded — the shape left by a service
9
+ installed by a gem version that did not record state — it returned
10
+ `:not_running` even though 443 answered, sending people to hunt for a
11
+ process that was right there. It now detects the serving proxy and
12
+ points at `sudo yamine service uninstall`.
13
+
14
+
15
+ ## [0.9.0] — 2026-09-10
16
+
17
+ ### Fixed
18
+
19
+ - **The service proxy now records its own state.** The launchd/systemd
20
+ service runs `proxy start --foreground`, and that path recorded
21
+ nothing: `write_pid` was reachable only from `spawn_daemon`. So after
22
+ `yamine setup`, the pid/port/scheme files still held the PREVIOUS
23
+ daemon's values. On a machine whose root service was serving clean
24
+ `https://<app>.localhost` on 443, the recorded port still said 8443 —
25
+ `yamine start` printed and baked `:8443` into `YAMINE_URL`, doctor
26
+ warned about the long-dead 8443, and `proxy stop` aimed at a pid that
27
+ was already gone. `setup` even contradicted itself in one run:
28
+ "proxy is up on port 443" followed by "listening on port 8443". The
29
+ foreground path now writes pid/port/tls plus a new `proxy.version`
30
+ marker BEFORE it serves.
31
+ - The launchd plist and systemd unit pass `--port 443` explicitly. They
32
+ relied on the default, which consults `YAMINE_PORT` — one stray export
33
+ would silently move the boot service off 443.
34
+ - **`proxy stop` no longer claims success on a root-owned proxy.** The
35
+ kill raised `Errno::EPERM`, which was swallowed by a blanket rescue:
36
+ the state files were cleared and "Proxy stopped." printed while the
37
+ service kept serving — now with nothing on disk to find or stop it by.
38
+ It reports `needs_root` and points at `sudo yamine service uninstall`.
39
+
40
+ ### Added
41
+
42
+ - `doctor` gains a `proxy state` check and now finds the proxy that is
43
+ actually serving (`ProxyControl.serving_port`, ownership-proven)
44
+ instead of trusting the recorded port. It warns when the recorded port
45
+ disagrees with reality, and when the running proxy is an older gem
46
+ version than the CLI (the root service keeps serving old code after an
47
+ upgrade until `sudo yamine service install` re-registers it).
48
+ - The clean default port wins when our proxy serves there, so a leftover
49
+ daemon on another port can no longer make the machine report a
50
+ downgraded URL as its state. A non-default port is still reported when
51
+ it is the only proxy — the deliberate CI/sandbox case.
52
+
53
+
3
54
  ## [0.8.2] — 2026-09-10
4
55
 
5
56
  ### Fixed
@@ -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
- recorded = ProxyControl.proxy_port(store)
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
@@ -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?
@@ -156,17 +156,33 @@ module Yamine
156
156
  nil
157
157
  end
158
158
 
159
- def write_pid(store, pid, port, tls)
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
- Ownership.fix(store.pid_path, store.port_path, File.join(store.dir, "proxy.tls"))
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
- write_pid(store, pid, port, tls)
272
+ write_proxy_state(store, pid: pid, port: port, tls: tls)
209
273
  pid
210
274
  end
211
275
 
@@ -221,6 +285,12 @@ module Yamine
221
285
  pid = read_pid(store)
222
286
  port = proxy_port(store)
223
287
  if pid.nil?
288
+ # Nothing recorded, but a service can still be serving — a
289
+ # root-installed proxy from an older gem recorded no state at all,
290
+ # and "Proxy is not running." while 443 answers would be a lie
291
+ # that sends people looking for a process that is right there.
292
+ return :needs_root if serving_port(store)
293
+
224
294
  return :not_running unless port && listening?(port)
225
295
 
226
296
  return :unknown_process
@@ -231,6 +301,11 @@ module Yamine
231
301
  end
232
302
  begin
233
303
  Process.kill("TERM", pid)
304
+ rescue Errno::EPERM
305
+ # The proxy runs as root (launchd/systemd service) and we do not.
306
+ # Clearing the state here would be a lie: the proxy keeps serving,
307
+ # now with nothing on disk to find or stop it by.
308
+ return :needs_root
234
309
  rescue SystemCallError
235
310
  clear_pid(store)
236
311
  return :stale
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yamine
4
- VERSION = "0.8.2"
4
+ VERSION = "0.9.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yamine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto