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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c8f9a8a50b4bb94a8b19b81774ce30e7a9d64fca932c2eb6a49016dc0352171
4
- data.tar.gz: 90066d82b0085d019c3118fe57d225eb78206fe44f55c235dd3e7d67d8451382
3
+ metadata.gz: a44456ebacba7cc7b8a9b8ffe8a973411c13f8055e3297a9bfb1cbb370d4397b
4
+ data.tar.gz: 1fce4c668f1439fd7c057a4b94e804670d7ce4672a0641f0a325e343e44bba3b
5
5
  SHA512:
6
- metadata.gz: 042b4dbfa4faf371dde9eb25e3e4406170c46086c29f1ff67afecad0fb61c482605f4f912469251b8640545142eeb295dd7eb1797081137575d941ebccf3f9be
7
- data.tar.gz: e5e303934c51d2fc83692817eef9f26382d5bbd5c1b735f6a0b3bcd65a13a7a902574b070cd86a1b60cf0e17a34788444507c5b8cd62c9d8a60aea626abff80f
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
- pids = result.split("\n")
348
- pids.each do |pid|
349
- Process.kill("TERM", pid.to_i)
350
- rescue Errno::ESRCH, Errno::EPERM
351
- # Process already gone or no permission
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: #{pids.join(', ')})"
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.82"
4
+ VERSION = "3.13.84"
5
5
  end
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
- puts " Swagger: http://localhost:#{port}/swagger"
191
- puts " Dashboard: http://localhost:#{port}/__dev"
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.82
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-23 00:00:00.000000000 Z
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