yamine 0.10.3 → 0.11.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 +53 -0
- data/lib/yamine/certs.rb +68 -0
- data/lib/yamine/cli/boot.rb +76 -10
- data/lib/yamine/doctor.rb +17 -0
- data/lib/yamine/readiness.rb +60 -3
- data/lib/yamine/runner.rb +31 -6
- 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: 1efaa2b9488a10007f6a386cf874eb78b3e8489e9b772efb2e88713286e98e2d
|
|
4
|
+
data.tar.gz: 410ac56d153c94589d1d36fd1a6b5670ac215c6c5999ca956590acd3d7c46a98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed28898e26abce37591fb15902697e2f96d2dc4ed4bc20d951872c48f586e19482affdafb24606bbe8c95cd0016b63a637d45f23c15fd7778f6ebd9a2415e0b7
|
|
7
|
+
data.tar.gz: 075d1d80e12a0578377d956421bfbc7ac9dc61e414552262d0ffd61ea9613f1382732135c5b8e7c7af8fbfb9d54e02905d05bc53347cba7054ced38879fef0d4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0] — 2026-09-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Ruby, git, and curl now trust the local CA — so an app can call
|
|
8
|
+
another app at `https://<name>.localhost` with no configuration.** The
|
|
9
|
+
gap surfaced when anywaye (a Rails app) tried to reach anymark at
|
|
10
|
+
`https://anymark-directory.localhost`: `certificate verify failed`. The
|
|
11
|
+
CA was trusted everywhere a human looks (Safari, Chrome, curl) and
|
|
12
|
+
nowhere a child process looks — only Node was covered, via
|
|
13
|
+
`NODE_EXTRA_CA_CERTS`.
|
|
14
|
+
|
|
15
|
+
The naive fix is wrong in a way that shows up much later. `SSL_CERT_FILE`
|
|
16
|
+
does not *add* to OpenSSL's roots, it *replaces* them, so pointing it at
|
|
17
|
+
`~/.yamine/ca.pem` would break every public HTTPS call the app makes —
|
|
18
|
+
rubygems, payment APIs, an LLM provider — while fixing the local one. So
|
|
19
|
+
yamine now builds `~/.yamine/bundle.pem`: the system's roots plus its own
|
|
20
|
+
CA, regenerated only when either changes, and exports it as
|
|
21
|
+
`SSL_CERT_FILE` and `GIT_SSL_CAINFO` for every child. Node keeps getting
|
|
22
|
+
the CA alone, because its variable really is additive.
|
|
23
|
+
|
|
24
|
+
`yamine doctor` reports the bundle (`bundle: 129 certificates`), so a
|
|
25
|
+
missing or stale one is visible before an app fails on it, and a real
|
|
26
|
+
TLS handshake against a yamine-minted certificate is pinned by a test —
|
|
27
|
+
not just the presence of the environment variables.
|
|
28
|
+
|
|
29
|
+
## [0.10.4] — 2026-09-11
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **A stale `tmp/pids/server.pid` no longer kills the boot 2s in.** The
|
|
34
|
+
anywaye incident: an interrupted earlier run left puma alive, Rails
|
|
35
|
+
refused to start ("A server is already running (pid: N)"), `web` died
|
|
36
|
+
before its first healthcheck, and the failure line only said "process
|
|
37
|
+
exited" — the actual reason sat in the log file. Now:
|
|
38
|
+
- Pre-flight `Readiness.server_pid_conflict` checks the pidfile BEFORE
|
|
39
|
+
spawning. A live holder is a conflict, not a mystery crash; a dead
|
|
40
|
+
pid is fine (Rails overwrites it).
|
|
41
|
+
- When the holder is this app's own leftover puma (`puma ... [app]`),
|
|
42
|
+
yamine reaps it automatically. Anything else fails with the exact
|
|
43
|
+
fix (`kill N && rm tmp/pids/server.pid`), or `--force` to take over.
|
|
44
|
+
- `Readiness.fatal_line` recognizes the common killers (already
|
|
45
|
+
running, address in use, missing gems, missing database) in the
|
|
46
|
+
process's own log and puts the real reason in the failure line.
|
|
47
|
+
- **No more orphans on any failure path.** The incident left puma +
|
|
48
|
+
solid-queue + ask-app-server running for 24 minutes with no routes
|
|
49
|
+
after the CLI exited. Now `Runner#boot_run` reaps the backend when
|
|
50
|
+
registration is refused (quota/conflict), `boot_concurrent` tracks
|
|
51
|
+
every spawned HTTP backend in `children` before adopt, and `boot_all`
|
|
52
|
+
wraps spawn+boot so a raise mid-boot kills every child and removes
|
|
53
|
+
every route already registered. Tests pin all three (with `expects`
|
|
54
|
+
so they cannot pass vacuously).
|
|
55
|
+
|
|
3
56
|
## [0.10.3] — 2026-09-11
|
|
4
57
|
|
|
5
58
|
### Added
|
data/lib/yamine/certs.rb
CHANGED
|
@@ -44,6 +44,12 @@ module Yamine
|
|
|
44
44
|
{ cert: File.join(dir, "ca.pem"), key: File.join(dir, "ca-key.pem") }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# The CA certificate's path on disk. Generated on first ask.
|
|
48
|
+
def ca_cert_path(dir = state_dir)
|
|
49
|
+
ensure_ca(dir)
|
|
50
|
+
ca_paths(dir)[:cert]
|
|
51
|
+
end
|
|
52
|
+
|
|
47
53
|
def ensure_ca(dir = state_dir)
|
|
48
54
|
FileUtils.mkdir_p(dir, mode: 0o755)
|
|
49
55
|
Ownership.fix(dir)
|
|
@@ -79,6 +85,68 @@ module Yamine
|
|
|
79
85
|
OpenSSL::PKey.read(File.read(paths[:key]))]
|
|
80
86
|
end
|
|
81
87
|
|
|
88
|
+
# A CA bundle a child process can verify against: the system roots
|
|
89
|
+
# *plus* this CA.
|
|
90
|
+
#
|
|
91
|
+
# Why a bundle and not just the CA: `SSL_CERT_FILE` (Ruby/OpenSSL,
|
|
92
|
+
# curl, git) *replaces* the default store rather than adding to it, so
|
|
93
|
+
# pointing it at a file containing only our CA makes every public
|
|
94
|
+
# HTTPS request in that process fail verification. Concatenating gives
|
|
95
|
+
# both — yamine's names verify and rubygems, APIs, and git-over-https
|
|
96
|
+
# keep working.
|
|
97
|
+
#
|
|
98
|
+
# Built fresh when the CA or the system store changes, and cached
|
|
99
|
+
# otherwise: the bundle is ~230 KB and regenerating it on every boot
|
|
100
|
+
# would be pure waste, while serving a stale one after `yamine trust`
|
|
101
|
+
# regenerates the CA would silently break every TLS connection in the
|
|
102
|
+
# app.
|
|
103
|
+
def bundle_path(dir = state_dir)
|
|
104
|
+
File.join(dir, "bundle.pem")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The system's own roots, in the order OpenSSL would consider them.
|
|
108
|
+
# SSL_CERT_FILE wins when set (that is the variable we are about to
|
|
109
|
+
# write, and a caller may have pointed it somewhere deliberately),
|
|
110
|
+
# else OpenSSL's compiled-in file, else the usual macOS/Linux paths.
|
|
111
|
+
def system_bundle
|
|
112
|
+
candidates = [ENV["SSL_CERT_FILE"],
|
|
113
|
+
OpenSSL::X509::DEFAULT_CERT_FILE,
|
|
114
|
+
"/etc/ssl/cert.pem",
|
|
115
|
+
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
116
|
+
"/etc/ssl/certs/ca-certificates.crt"].compact
|
|
117
|
+
candidates.find { |path| File.file?(path) && File.size(path).positive? }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ensure_bundle(dir = state_dir)
|
|
121
|
+
paths = ensure_ca(dir)
|
|
122
|
+
target = bundle_path(dir)
|
|
123
|
+
ca = File.read(paths[:cert])
|
|
124
|
+
system_file = system_bundle
|
|
125
|
+
# Regenerate when the CA changed, when the system store changed, or
|
|
126
|
+
# when the bundle is missing. mtime comparison is enough: these files
|
|
127
|
+
# are written by installers, not edited in place.
|
|
128
|
+
return target if bundle_current?(target, paths[:cert], system_file)
|
|
129
|
+
|
|
130
|
+
FileUtils.mkdir_p(dir)
|
|
131
|
+
content = +""
|
|
132
|
+
content << File.read(system_file) if system_file
|
|
133
|
+
content << "\n" unless content.empty? || content.end_with?("\n")
|
|
134
|
+
content << ca
|
|
135
|
+
File.write(target, content, mode: "w", perm: 0o644)
|
|
136
|
+
Ownership.fix(target)
|
|
137
|
+
target
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def bundle_current?(target, ca_path, system_file)
|
|
141
|
+
return false unless File.file?(target)
|
|
142
|
+
|
|
143
|
+
target_mtime = File.mtime(target)
|
|
144
|
+
return false if File.mtime(ca_path) > target_mtime
|
|
145
|
+
return false if system_file && File.mtime(system_file) > target_mtime
|
|
146
|
+
|
|
147
|
+
true
|
|
148
|
+
end
|
|
149
|
+
|
|
82
150
|
# Mint a leaf cert for one hostname, signed by the CA.
|
|
83
151
|
def mint_host(hostname, ca_cert, ca_key)
|
|
84
152
|
key = OpenSSL::PKey::EC.generate("prime256v1")
|
data/lib/yamine/cli/boot.rb
CHANGED
|
@@ -90,6 +90,28 @@ module Yamine
|
|
|
90
90
|
exit 1
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
# Pre-flight: a live tmp/pids/server.pid makes Rails refuse to
|
|
94
|
+
# boot ("A server is already running") the moment web starts.
|
|
95
|
+
# When the holder is this app's own puma (`puma ... [app]`), take
|
|
96
|
+
# over automatically — that is one of ours. Anything else is not
|
|
97
|
+
# ours to kill: fail with the exact fix before spawning.
|
|
98
|
+
conflict_ok, conflict_detail, conflict_pid = Readiness.server_pid_conflict(Dir.pwd)
|
|
99
|
+
unless conflict_ok
|
|
100
|
+
if own_orphan_puma?(conflict_pid, resolved.app, Dir.pwd)
|
|
101
|
+
say opts, " reaping own orphaned server (pid #{conflict_pid})"
|
|
102
|
+
reap_stale_server(conflict_pid)
|
|
103
|
+
elsif opts[:force]
|
|
104
|
+
say opts, " --force: taking over pid #{conflict_pid}"
|
|
105
|
+
reap_stale_server(conflict_pid)
|
|
106
|
+
else
|
|
107
|
+
$stderr.puts "Error: #{conflict_detail}."
|
|
108
|
+
$stderr.puts " Rails will refuse to boot while it runs. Free it first:"
|
|
109
|
+
$stderr.puts " kill #{conflict_pid} && rm tmp/pids/server.pid"
|
|
110
|
+
$stderr.puts " Or re-run with --force to let yamine take over that pid."
|
|
111
|
+
exit 1
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
93
115
|
runner = Runner.new(store: ctx.store)
|
|
94
116
|
children = []
|
|
95
117
|
routes_registered = []
|
|
@@ -111,12 +133,21 @@ module Yamine
|
|
|
111
133
|
with_wait = !opts[:no_wait]
|
|
112
134
|
spawn_plan = collect_spawns(ctx, runner, resolved, opts, db_url, children)
|
|
113
135
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
begin
|
|
137
|
+
if with_wait
|
|
138
|
+
boot_concurrent(ctx, runner, resolved, opts, spawn_plan, children,
|
|
139
|
+
routes_registered, db_url, db_name, db_created, events: events)
|
|
140
|
+
else
|
|
141
|
+
boot_sequential(ctx, runner, resolved, opts, spawn_plan, children,
|
|
142
|
+
routes_registered, db_url, events: events)
|
|
143
|
+
end
|
|
144
|
+
rescue StandardError
|
|
145
|
+
# A raise mid-boot (route conflict, quota, unexpected error)
|
|
146
|
+
# must never leave half a tree running: reap every child and
|
|
147
|
+
# every route already registered, then surface the error.
|
|
148
|
+
children.each { |c| stop_spawned_pid(c[:pid]) }
|
|
149
|
+
cleanup_routes(ctx, routes_registered.flat_map { |r| r[:hostnames] })
|
|
150
|
+
raise
|
|
120
151
|
end
|
|
121
152
|
|
|
122
153
|
background = processes.select { |_, v| v["proxy"] == false }
|
|
@@ -137,6 +168,35 @@ module Yamine
|
|
|
137
168
|
supervise_tree(ctx, all_hostnames, named_pids, reporter: events)
|
|
138
169
|
end
|
|
139
170
|
|
|
171
|
+
# TERM the old server and make sure the pidfile no longer names
|
|
172
|
+
# a live process before we spawn (wait a moment; then remove the
|
|
173
|
+
# file if the process is still refusing to die, so Rails can boot
|
|
174
|
+
# over it — the socket it held is already ours to take).
|
|
175
|
+
def reap_stale_server(pid)
|
|
176
|
+
Process.kill("TERM", pid)
|
|
177
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
|
|
178
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
179
|
+
break unless ProxyControl.pid_alive?(pid)
|
|
180
|
+
sleep 0.2
|
|
181
|
+
end
|
|
182
|
+
FileUtils.rm_f(File.join(Dir.pwd, "tmp", "pids", "server.pid"))
|
|
183
|
+
rescue SystemCallError
|
|
184
|
+
FileUtils.rm_f(File.join(Dir.pwd, "tmp", "pids", "server.pid"))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Is this pid a leftover backend of THIS app? `puma (tcp://...)
|
|
188
|
+
# [app]` names the app; we also accept a command containing the
|
|
189
|
+
# app dir. Only then may we reap it unasked.
|
|
190
|
+
def own_orphan_puma?(pid, app, dir)
|
|
191
|
+
_out, status = Open3.capture2("ps", "-p", pid.to_s, "-o", "command=")
|
|
192
|
+
return false unless status.success?
|
|
193
|
+
|
|
194
|
+
cmd = _out.to_s
|
|
195
|
+
cmd.include?("[#{app}]") || cmd.include?(File.expand_path(dir))
|
|
196
|
+
rescue SystemCallError
|
|
197
|
+
false
|
|
198
|
+
end
|
|
199
|
+
|
|
140
200
|
# One pass over config: background processes spawn immediately
|
|
141
201
|
# (no route to wait on), HTTP processes become a spawn plan the
|
|
142
202
|
# boot mode executes. Compound-line refusal happens here so both
|
|
@@ -198,12 +258,18 @@ module Yamine
|
|
|
198
258
|
apps = {}
|
|
199
259
|
plan.each do |item|
|
|
200
260
|
say opts, " [#{item[:name]}] #{item[:url]}"
|
|
261
|
+
app = runner.spawn_http(name: item[:name], hostname: item[:hostname],
|
|
262
|
+
url: item[:url], dir: Dir.pwd, command: item[:command],
|
|
263
|
+
port: item[:port], rails_dev_host: item[:hostname],
|
|
264
|
+
database_url: db_url, force: opts[:force])
|
|
265
|
+
# Tracked before registration so any later failure (a raise
|
|
266
|
+
# during adopt, a crash while another process waits) can
|
|
267
|
+
# always reap it. Duplicate names in named_pids are harmless.
|
|
268
|
+
children << { name: item[:name], pid: app.pid }
|
|
201
269
|
apps[item[:name]] = {
|
|
202
270
|
item: item,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
port: item[:port], rails_dev_host: item[:hostname],
|
|
206
|
-
database_url: db_url, force: opts[:force])
|
|
271
|
+
log_path: File.join(Dir.pwd, "log", "yamine-#{item[:name]}.log"),
|
|
272
|
+
app: app
|
|
207
273
|
}
|
|
208
274
|
end
|
|
209
275
|
wait_result = Readiness.wait_all(apps, sink: events)
|
data/lib/yamine/doctor.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Yamine
|
|
|
33
33
|
checks << check_routes(store)
|
|
34
34
|
checks << check_dns(store)
|
|
35
35
|
checks << check_ca
|
|
36
|
+
checks << check_ca_bundle
|
|
36
37
|
checks
|
|
37
38
|
end
|
|
38
39
|
|
|
@@ -195,6 +196,22 @@ module Yamine
|
|
|
195
196
|
end
|
|
196
197
|
end
|
|
197
198
|
|
|
199
|
+
# The child-process trust bundle. Its absence is not theoretical: an
|
|
200
|
+
# app whose process was started before the bundle existed (or with a
|
|
201
|
+
# regenerated CA) fails every call to another *.localhost with
|
|
202
|
+
# "certificate verify failed". Regenerating it here is safe and is
|
|
203
|
+
# exactly what the next boot would do anyway.
|
|
204
|
+
def check_ca_bundle
|
|
205
|
+
dir = Certs.state_dir
|
|
206
|
+
return Check.new(name: "bundle", ok: false, message: "no CA yet — run: yamine trust") unless File.file?(Certs.ca_paths(dir)[:cert])
|
|
207
|
+
|
|
208
|
+
target = Certs.ensure_bundle(dir)
|
|
209
|
+
count = File.read(target).scan("BEGIN CERTIFICATE").size
|
|
210
|
+
Check.new(name: "bundle", ok: true, message: "#{count} certificates (system roots + yamine CA)")
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
Check.new(name: "bundle", ok: false, message: "could not build #{Certs.bundle_path(dir)}: #{e.message}")
|
|
213
|
+
end
|
|
214
|
+
|
|
198
215
|
# How many trusted certificates carry one of our CA names without
|
|
199
216
|
# being the CA currently on disk. Best-effort: 0 when the keychain
|
|
200
217
|
# cannot be read (doctor must never fail on a read-only probe).
|
data/lib/yamine/readiness.rb
CHANGED
|
@@ -168,8 +168,7 @@ module Yamine
|
|
|
168
168
|
until Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
169
169
|
unless process_alive?(app.pid)
|
|
170
170
|
ms = elapsed_ms(started)
|
|
171
|
-
return fail_result(name, started, ms,
|
|
172
|
-
"process exited before becoming healthy — see log/yamine-#{name}.log", sink)
|
|
171
|
+
return fail_result(name, started, ms, died_detail(name, slot), sink)
|
|
173
172
|
end
|
|
174
173
|
ok, detail = probe(item[:hostname], item[:port], path: path)
|
|
175
174
|
if ok
|
|
@@ -182,13 +181,24 @@ module Yamine
|
|
|
182
181
|
ms = elapsed_ms(started)
|
|
183
182
|
status = process_alive?(app.pid) ? "timeout" : "fail"
|
|
184
183
|
detail = process_alive?(app.pid) ? "no healthy response within #{timeout}s (#{last_error})" :
|
|
185
|
-
|
|
184
|
+
died_detail(name, slot)
|
|
186
185
|
event = Event.new(phase: "process", action: name, status: status,
|
|
187
186
|
duration_ms: ms, detail: detail)
|
|
188
187
|
sink&.event(event)
|
|
189
188
|
{ name: name, status: status, phase: "process", detail: detail, duration_ms: ms }
|
|
190
189
|
end
|
|
191
190
|
|
|
191
|
+
# Why a process that died before answering died: the recognized
|
|
192
|
+
# fatal line when there is one, else point at its log.
|
|
193
|
+
def died_detail(name, slot)
|
|
194
|
+
reason = fatal_line(slot[:log_path])
|
|
195
|
+
if reason
|
|
196
|
+
"process exited: #{reason} (log/yamine-#{name}.log)"
|
|
197
|
+
else
|
|
198
|
+
"process exited before becoming healthy — see log/yamine-#{name}.log"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
192
202
|
def ok_result(name, started, ms, path, sink)
|
|
193
203
|
detail = path ? "healthcheck #{path} returned 2xx-3xx" : "port accepted connection"
|
|
194
204
|
event = Event.new(phase: "process", action: name, status: "ok",
|
|
@@ -204,6 +214,53 @@ module Yamine
|
|
|
204
214
|
{ name: name, status: "fail", phase: "process", detail: detail, duration_ms: ms }
|
|
205
215
|
end
|
|
206
216
|
|
|
217
|
+
# Rails refuses to boot when tmp/pids/server.pid names a live
|
|
218
|
+
# process ("A server is already running (pid: N)") — the classic
|
|
219
|
+
# interrupted-run leftover. Report the exact reason and the fix
|
|
220
|
+
# BEFORE spawning anything, instead of letting `web` die 2s later.
|
|
221
|
+
# Returns [ok, detail, pid]; ok is true when there is nothing in the
|
|
222
|
+
# way (no file, unreadable, or a dead pid — Rails overwrites those).
|
|
223
|
+
def server_pid_conflict(dir = Dir.pwd)
|
|
224
|
+
path = File.join(dir, "tmp", "pids", "server.pid")
|
|
225
|
+
return [true, nil, nil] unless File.file?(path)
|
|
226
|
+
|
|
227
|
+
pid = File.read(path).strip.to_i
|
|
228
|
+
return [true, nil, nil] unless pid.positive? && process_alive?(pid)
|
|
229
|
+
|
|
230
|
+
[false, "a Rails server is already running (pid #{pid}, from #{path})", pid]
|
|
231
|
+
rescue SystemCallError, ArgumentError
|
|
232
|
+
[true, nil, nil]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Known fatal patterns in a dying backend's log. The log line that
|
|
236
|
+
# killed the process is more useful than "process exited" — agents
|
|
237
|
+
# should not have to open the file to learn why.
|
|
238
|
+
FATAL_PATTERNS = [
|
|
239
|
+
[/A server is already running \(pid:\s*(\d+)/i,
|
|
240
|
+
->(m) { "another server is already running (pid #{m[1]}); stale tmp/pids/server.pid" }],
|
|
241
|
+
[/Address already in use/i,
|
|
242
|
+
->(_m) { "address already in use; another process holds the port" }],
|
|
243
|
+
[/Bundler::GemNotFound|Could not find .* locally installed gems|run `?bundle install/i,
|
|
244
|
+
->(_m) { "gems missing; run `bundle install`" }],
|
|
245
|
+
[/ActiveRecord::NoDatabaseError|PG::ConnectionBad|database .* does not exist/i,
|
|
246
|
+
->(_m) { "database unreachable or missing; check DATABASE_URL / the database server" }]
|
|
247
|
+
].freeze
|
|
248
|
+
|
|
249
|
+
def fatal_line(path)
|
|
250
|
+
return nil unless path && File.file?(path)
|
|
251
|
+
|
|
252
|
+
lines = File.readlines(path).last(80)
|
|
253
|
+
FATAL_PATTERNS.each do |pattern, describe|
|
|
254
|
+
match = lines.reverse.find { |l| l.match?(pattern) }
|
|
255
|
+
next unless match
|
|
256
|
+
|
|
257
|
+
return describe.call(match.match(pattern))
|
|
258
|
+
end
|
|
259
|
+
nil
|
|
260
|
+
rescue SystemCallError
|
|
261
|
+
nil
|
|
262
|
+
end
|
|
263
|
+
|
|
207
264
|
def process_alive?(pid)
|
|
208
265
|
Process.kill(0, pid)
|
|
209
266
|
true
|
data/lib/yamine/runner.rb
CHANGED
|
@@ -103,10 +103,17 @@ module Yamine
|
|
|
103
103
|
Process.detach(pid)
|
|
104
104
|
target = "127.0.0.1:#{port}"
|
|
105
105
|
if register
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
begin
|
|
107
|
+
spec ||= { "dir" => File.expand_path(dir), "proc" => name }
|
|
108
|
+
@store.add_route(hostname, target, Process.pid, kind: "tcp",
|
|
109
|
+
force: force, spec: spec)
|
|
110
|
+
write_backend_pid(hostname, pid)
|
|
111
|
+
rescue StandardError
|
|
112
|
+
# Registration refused (quota, conflict): the backend is
|
|
113
|
+
# already spawned — never leave it running without a route.
|
|
114
|
+
stop_pid(pid)
|
|
115
|
+
raise
|
|
116
|
+
end
|
|
110
117
|
end
|
|
111
118
|
App.new(name: name, hostname: hostname, url: url, pid: pid,
|
|
112
119
|
target: target, kind: "tcp", command: command)
|
|
@@ -148,8 +155,26 @@ module Yamine
|
|
|
148
155
|
env = { "YAMINE_URL" => url }
|
|
149
156
|
env["PORT"] = port.to_s if port
|
|
150
157
|
env["HOST"] = "127.0.0.1"
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
|
|
159
|
+
# TLS: yamine's names present certificates signed by its own CA, so
|
|
160
|
+
# every process that talks to one of them has to be told to trust it.
|
|
161
|
+
# Three clients, three conventions — and getting this wrong is not
|
|
162
|
+
# subtle. A Ruby app calling https://other-app.localhost fails with
|
|
163
|
+
# "certificate verify failed", which reads as a broken app rather
|
|
164
|
+
# than a missing trust setting:
|
|
165
|
+
#
|
|
166
|
+
# * Ruby/OpenSSL, curl, and git honor SSL_CERT_FILE — but it
|
|
167
|
+
# *replaces* the store, so it points at the bundle (the system's
|
|
168
|
+
# roots plus our CA), never at our CA alone; a CA-only file would
|
|
169
|
+
# break every public HTTPS call the app makes.
|
|
170
|
+
# * git takes GIT_SSL_CAINFO as its own override.
|
|
171
|
+
# * Node ignores both and reads NODE_EXTRA_CA_CERTS, which is
|
|
172
|
+
# additive — the CA by itself is right there.
|
|
173
|
+
bundle = Certs.ensure_bundle
|
|
174
|
+
env["NODE_EXTRA_CA_CERTS"] = Certs.ca_cert_path
|
|
175
|
+
env["SSL_CERT_FILE"] = bundle
|
|
176
|
+
env["GIT_SSL_CAINFO"] = bundle
|
|
177
|
+
|
|
153
178
|
# Rails blocks unknown Host headers in development. Allow the
|
|
154
179
|
# proxied hostname so Rails apps boot behind yamine with zero
|
|
155
180
|
# config — this replaces the yamine-rails hosts patch.
|
data/lib/yamine/version.rb
CHANGED