tina4ruby 3.13.82 → 3.13.84
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 +28 -7
- data/lib/tina4/cli.rb +70 -7
- data/lib/tina4/rack_app.rb +13 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +25 -2
- metadata +30 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a44456ebacba7cc7b8a9b8ffe8a973411c13f8055e3297a9bfb1cbb370d4397b
|
|
4
|
+
data.tar.gz: 1fce4c668f1439fd7c057a4b94e804670d7ce4672a0641f0a325e343e44bba3b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d18d9143115b94beb1f6a4c4e067702f3127c2f05b24dc29a0c9efe04419622d2bb3c8bf94a19c61d1dad38f75ba2cc76d3d15fd530e51ee22198990584173d
|
|
7
|
+
data.tar.gz: 506e937dc17782c347efc91af039b21ea9e73e76fd92457bc8eab345ab27a7f6bc454eafca2d999a930053494926450f5d615eb871463577f2b5983d127f5f6c
|
data/CHANGELOG.md
CHANGED
|
@@ -12,13 +12,6 @@ UNRELEASED work. When a version ships, its notes go to the release notes above.
|
|
|
12
12
|
|
|
13
13
|
## Unreleased
|
|
14
14
|
|
|
15
|
-
### Added
|
|
16
|
-
|
|
17
|
-
- **MQTT 3.1.1 client** (`Tina4::Mqtt` / `Tina4::MqttMessage`), zero-dependency (stdlib `socket` +
|
|
18
|
-
lazily-required `openssl`), verified against a real broker with no mocks. publish/subscribe/consume,
|
|
19
|
-
QoS 0/1, retained, Last Will, per-client TLS, QoS 2 refused loudly. Ruby ships all 97 shared
|
|
20
|
-
features plus its native ERB engine, for **98 built-in features**.
|
|
21
|
-
|
|
22
15
|
### Changed
|
|
23
16
|
|
|
24
17
|
- Internal: the SQL dialect-translation file is renamed
|
|
@@ -28,6 +21,34 @@ UNRELEASED work. When a version ships, its notes go to the release notes above.
|
|
|
28
21
|
filename alignment, not an API change. Only code that bypassed the gem's own entry point with
|
|
29
22
|
a direct `require "tina4/sql_translation"` needs to update the path.
|
|
30
23
|
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **`tina4 deploy docker` produced images that could not start.** Of the eight
|
|
27
|
+
Dockerfile generators in the stack (four templates in the `tina4` CLI plus one
|
|
28
|
+
in each framework's own CLI), exactly one was correct. Python named
|
|
29
|
+
`python -m tina4_python.cli`, a package with no `__main__.py`, so the container
|
|
30
|
+
died on startup; PHP ran `php index.php <addr>`, but `App::run(?host, port)`
|
|
31
|
+
never reads argv so the address was dropped and production never engaged;
|
|
32
|
+
Node named a path that exists only inside the tina4-nodejs monorepo and
|
|
33
|
+
depended on tsx, which `npm ci --omit=dev` strips. Every generator now names a
|
|
34
|
+
published entry point and requests production. Verified by scaffolding,
|
|
35
|
+
generating, building and running a container for all four languages.
|
|
36
|
+
- **`serve` no longer kills PID 1.** The port-reclaim step read `lsof -ti`
|
|
37
|
+
without validating it. Where lsof prints a different shape, a non-numeric field
|
|
38
|
+
coerced to 0 or 1 -- and signalling PID 0 hits every process in the caller's
|
|
39
|
+
own process group. In a container the server IS PID 1, so it killed itself
|
|
40
|
+
(Node logged "Killed existing process on port 7148 (PID: 1 ...)" then exited
|
|
41
|
+
143; PHP logged the same attempt and survived by luck). Reclaiming is now
|
|
42
|
+
skipped inside a container, only all-digit PIDs are accepted, and PID 0, PID 1
|
|
43
|
+
and the current process are never signalled.
|
|
44
|
+
- **The Ruby image builds.** The builder stage was `ruby:3.3-slim`, which has no
|
|
45
|
+
compiler, so `bundle install` failed with `Gem::Ext::BuildError` on the first
|
|
46
|
+
native extension (date, json, nio4r, sqlite3 are all in the default set). The
|
|
47
|
+
builder is now the full `ruby:3.3` image and the slim runtime installs
|
|
48
|
+
`libsqlite3-0`. Bundler's `deployment true` is gone: it demands a lockfile
|
|
49
|
+
matching the image's bundler exactly, so a lockfile from a newer bundler on a
|
|
50
|
+
developer machine hard-failed the build.
|
|
51
|
+
|
|
31
52
|
## Earlier history (pre-3.x)
|
|
32
53
|
|
|
33
54
|
Kept for reference only. The versions below are from the 0.x line, long before the unified
|
data/lib/tina4/cli.rb
CHANGED
|
@@ -340,18 +340,81 @@ module Tina4
|
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
# Kill any process listening on the given port. Returns true if killed.
|
|
343
|
+
# True when this process is running inside a container.
|
|
344
|
+
#
|
|
345
|
+
# Reclaiming a port makes sense on a dev machine, where a previous
|
|
346
|
+
# `tina4 serve` may still hold it. Inside a container the server IS the
|
|
347
|
+
# container, so there is no stale sibling to reclaim from -- and trying is
|
|
348
|
+
# actively dangerous (see #kill_process_on_port).
|
|
349
|
+
def in_container?
|
|
350
|
+
return true if File.exist?("/.dockerenv") || File.exist?("/run/.containerenv")
|
|
351
|
+
|
|
352
|
+
blob = File.read("/proc/1/cgroup")
|
|
353
|
+
blob.include?("docker") || blob.include?("containerd") || blob.include?("kubepods")
|
|
354
|
+
rescue SystemCallError
|
|
355
|
+
false
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Kill any process listening on `port`. Returns true if anything was killed.
|
|
359
|
+
#
|
|
360
|
+
# Every PID is validated before use. `pid.to_i` on a non-numeric field
|
|
361
|
+
# yields 0, and Process.kill("TERM", 0) signals EVERY process in the
|
|
362
|
+
# caller's own process group -- the server kills itself. That is exactly
|
|
363
|
+
# what happened in a container: "Killed existing process on port 7148
|
|
364
|
+
# (PID: 1 ...)" followed by exit 143.
|
|
365
|
+
#
|
|
366
|
+
# So: skip entirely in a container, accept only all-digit PIDs, and never
|
|
367
|
+
# signal 0 (our process group), 1 (init), or ourselves.
|
|
368
|
+
# The PIDs from `lsof -ti` output that are safe to signal.
|
|
369
|
+
#
|
|
370
|
+
# Pure so the safety rule can be tested directly. An unvalidated parse is a
|
|
371
|
+
# footgun with real teeth: where lsof prints a different shape than -ti
|
|
372
|
+
# implies, a non-numeric field becomes 0, and signalling PID 0 hits EVERY
|
|
373
|
+
# process in the caller's own process group -- the server kills itself.
|
|
374
|
+
#
|
|
375
|
+
# Accept only all-digit tokens; never PID 0 (our group), PID 1 (init),
|
|
376
|
+
# ourselves, or our own process group.
|
|
377
|
+
def selectable_pids(lsof_output, me, my_group = nil)
|
|
378
|
+
pids = []
|
|
379
|
+
lsof_output.split(/\s+/).each do |token|
|
|
380
|
+
next unless token.match?(/\A\d+\z/) # never coerce junk into a PID
|
|
381
|
+
|
|
382
|
+
pid = token.to_i
|
|
383
|
+
next if pid <= 1 || pid == me # 0 = our group, 1 = init, me = suicide
|
|
384
|
+
next if !my_group.nil? && pid == my_group
|
|
385
|
+
|
|
386
|
+
pids << pid unless pids.include?(pid)
|
|
387
|
+
end
|
|
388
|
+
pids
|
|
389
|
+
end
|
|
390
|
+
|
|
343
391
|
def kill_process_on_port(port)
|
|
392
|
+
return false if in_container?
|
|
393
|
+
|
|
344
394
|
result = `lsof -ti :#{port} 2>/dev/null`.strip
|
|
345
395
|
return false if result.empty?
|
|
346
396
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
Process.
|
|
350
|
-
rescue
|
|
351
|
-
|
|
397
|
+
me = Process.pid
|
|
398
|
+
my_group = begin
|
|
399
|
+
Process.getpgrp
|
|
400
|
+
rescue StandardError
|
|
401
|
+
nil
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
killed = []
|
|
405
|
+
selectable_pids(result, me, my_group).each do |pid|
|
|
406
|
+
begin
|
|
407
|
+
Process.kill("TERM", pid)
|
|
408
|
+
killed << pid.to_s
|
|
409
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
410
|
+
# Process already gone or no permission
|
|
411
|
+
end
|
|
352
412
|
end
|
|
413
|
+
|
|
414
|
+
return false if killed.empty?
|
|
415
|
+
|
|
353
416
|
sleep 0.5
|
|
354
|
-
puts " Killed existing process on port #{port} (PID: #{
|
|
417
|
+
puts " Killed existing process on port #{port} (PID: #{killed.join(', ')})"
|
|
355
418
|
true
|
|
356
419
|
rescue Errno::ENOENT
|
|
357
420
|
false
|
|
@@ -3136,7 +3199,7 @@ module Tina4
|
|
|
3136
3199
|
ENV SWAGGER_DESCRIPTION="Auto-generated API documentation"
|
|
3137
3200
|
|
|
3138
3201
|
# Start the server on all interfaces
|
|
3139
|
-
CMD ["bundle", "exec", "tina4ruby", "start", "-p", "7147", "-h", "0.0.0.0"]
|
|
3202
|
+
CMD ["bundle", "exec", "tina4ruby", "start", "-p", "7147", "-h", "0.0.0.0", "--production"]
|
|
3140
3203
|
DOCKERFILE
|
|
3141
3204
|
end
|
|
3142
3205
|
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -418,6 +418,19 @@ module Tina4
|
|
|
418
418
|
def try_static(path, env = nil)
|
|
419
419
|
return nil if path.include?("..")
|
|
420
420
|
|
|
421
|
+
# The framework ships the Swagger UI as STATIC assets under
|
|
422
|
+
# lib/tina4/public/swagger/. Static serving is independent of the gated
|
|
423
|
+
# /swagger handler, so without this check the UI stays reachable in
|
|
424
|
+
# production via '/swagger/index.html' or '/swagger/oauth2-redirect.html'
|
|
425
|
+
# even when swagger is disabled -- silently bypassing
|
|
426
|
+
# TINA4_SWAGGER_ENABLED / TINA4_DEBUG. (A bare '/swagger' and '/swagger/'
|
|
427
|
+
# are already intercepted by the gated handler, which is why this leak hid.)
|
|
428
|
+
normalised_path = path.start_with?("/") ? path : "/#{path}"
|
|
429
|
+
if (normalised_path == "/swagger" || normalised_path.start_with?("/swagger/")) &&
|
|
430
|
+
!Tina4::Swagger.enabled?
|
|
431
|
+
return nil
|
|
432
|
+
end
|
|
433
|
+
|
|
421
434
|
@static_roots.each do |root|
|
|
422
435
|
full_path = File.join(root, path)
|
|
423
436
|
if File.file?(full_path)
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4.rb
CHANGED
|
@@ -156,6 +156,25 @@ module Tina4
|
|
|
156
156
|
# models with a Symbol/String `self.db` resolve against it.
|
|
157
157
|
def databases = (@databases ||= {})
|
|
158
158
|
|
|
159
|
+
# Build the startup banner's optional surface lines (issue #99).
|
|
160
|
+
#
|
|
161
|
+
# Only advertise a surface that is actually REACHABLE. In production, or with
|
|
162
|
+
# TINA4_DEBUG off, /swagger and /__dev return 404 -- printing them anyway
|
|
163
|
+
# both misleads an operator into believing a dev surface is exposed and sends
|
|
164
|
+
# a developer to a dead link.
|
|
165
|
+
#
|
|
166
|
+
# Kept as a pure function of (port, two booleans) so the contract is unit
|
|
167
|
+
# testable without booting a server and grepping stdout. Parity: Python
|
|
168
|
+
# banner_surface_lines, PHP App::bannerSurfaceLines, Node bannerSurfaceLines.
|
|
169
|
+
#
|
|
170
|
+
# @return [Array<String>] zero, one or two ready-to-puts banner rows.
|
|
171
|
+
def banner_surface_lines(port, swagger_enabled:, dev_admin_enabled:)
|
|
172
|
+
lines = []
|
|
173
|
+
lines << " Swagger: http://localhost:#{port}/swagger" if swagger_enabled
|
|
174
|
+
lines << " Dashboard: http://localhost:#{port}/__dev" if dev_admin_enabled
|
|
175
|
+
lines
|
|
176
|
+
end
|
|
177
|
+
|
|
159
178
|
def print_banner(host: "0.0.0.0", port: 7147, server_name: nil)
|
|
160
179
|
# TINA4_SUPPRESS — short-circuit ALL banner output for headless / CI runs.
|
|
161
180
|
return if Tina4::Env.is_truthy(ENV["TINA4_SUPPRESS"])
|
|
@@ -187,8 +206,12 @@ module Tina4
|
|
|
187
206
|
puts " Simple. Fast. Human. | Built for AI. Built for you."
|
|
188
207
|
puts ""
|
|
189
208
|
puts " Server: http://#{display}:#{port} (#{server_name})"
|
|
190
|
-
|
|
191
|
-
|
|
209
|
+
# Only advertise a surface that is actually reachable (issue #99).
|
|
210
|
+
banner_surface_lines(
|
|
211
|
+
port,
|
|
212
|
+
swagger_enabled: Tina4::Swagger.enabled?,
|
|
213
|
+
dev_admin_enabled: is_debug
|
|
214
|
+
).each { |line| puts line }
|
|
192
215
|
puts " Debug: #{is_debug ? 'ON' : 'OFF'} (Log level: #{log_level})"
|
|
193
216
|
puts ""
|
|
194
217
|
rescue
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.84
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -136,6 +136,34 @@ dependencies:
|
|
|
136
136
|
- - "~>"
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
138
|
version: '1.8'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: logger
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '1.6'
|
|
146
|
+
type: :runtime
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '1.6'
|
|
153
|
+
- !ruby/object:Gem::Dependency
|
|
154
|
+
name: base64
|
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - "~>"
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0.2'
|
|
160
|
+
type: :runtime
|
|
161
|
+
prerelease: false
|
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - "~>"
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '0.2'
|
|
139
167
|
- !ruby/object:Gem::Dependency
|
|
140
168
|
name: sqlite3
|
|
141
169
|
requirement: !ruby/object:Gem::Requirement
|