tina4ruby 3.11.13 → 3.11.15
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 +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +3 -3
data/lib/tina4/webserver.rb
CHANGED
|
@@ -1,312 +1,312 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
class WebServer
|
|
5
|
-
def initialize(app, host: "0.0.0.0", port: 7147)
|
|
6
|
-
@app = app
|
|
7
|
-
@host = host
|
|
8
|
-
@port = port
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
# Kill whatever process is listening on *port*.
|
|
12
|
-
# Uses lsof on macOS/Linux and netstat + taskkill on Windows.
|
|
13
|
-
# Raises RuntimeError if the port cannot be freed.
|
|
14
|
-
def free_port(port)
|
|
15
|
-
puts " Port #{port} in use — killing existing process..."
|
|
16
|
-
|
|
17
|
-
if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
|
18
|
-
output = `netstat -ano 2>&1`
|
|
19
|
-
pid = nil
|
|
20
|
-
output.each_line do |line|
|
|
21
|
-
if line.include?(":#{port}") && (line.include?("LISTENING") || line.include?("ESTABLISHED"))
|
|
22
|
-
parts = line.strip.split(/\s+/)
|
|
23
|
-
candidate = parts.last
|
|
24
|
-
if candidate =~ /^\d+$/
|
|
25
|
-
pid = candidate
|
|
26
|
-
break
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
if pid
|
|
31
|
-
system("taskkill /PID #{pid} /F")
|
|
32
|
-
else
|
|
33
|
-
raise "Could not free port #{port}: no PID found"
|
|
34
|
-
end
|
|
35
|
-
else
|
|
36
|
-
pids = `lsof -ti :#{port} 2>/dev/null`.strip.split("\n")
|
|
37
|
-
if pids.empty?
|
|
38
|
-
return # Nothing found — port may have freed itself
|
|
39
|
-
end
|
|
40
|
-
pids.each do |pid|
|
|
41
|
-
pid = pid.strip
|
|
42
|
-
next unless pid =~ /^\d+$/
|
|
43
|
-
begin
|
|
44
|
-
Process.kill("TERM", pid.to_i)
|
|
45
|
-
rescue Errno::ESRCH
|
|
46
|
-
# Process already gone
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Give the OS a moment to reclaim the port
|
|
52
|
-
sleep(0.5)
|
|
53
|
-
puts " Port #{port} freed"
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def start
|
|
57
|
-
is_managed = ARGV.include?('--managed')
|
|
58
|
-
unless is_managed || ENV['TINA4_OVERRIDE_CLIENT'] == 'true'
|
|
59
|
-
puts
|
|
60
|
-
puts '=' * 60
|
|
61
|
-
puts
|
|
62
|
-
puts ' Tina4 must be started with the tina4 CLI:'
|
|
63
|
-
puts
|
|
64
|
-
puts ' tina4 serve (development)'
|
|
65
|
-
puts ' tina4 serve --production (production)'
|
|
66
|
-
puts
|
|
67
|
-
puts ' Install: cargo install tina4'
|
|
68
|
-
puts ' Docs: https://tina4.com'
|
|
69
|
-
puts
|
|
70
|
-
puts ' To run directly, add to .env:'
|
|
71
|
-
puts ' TINA4_OVERRIDE_CLIENT=true'
|
|
72
|
-
puts
|
|
73
|
-
puts '=' * 60
|
|
74
|
-
puts
|
|
75
|
-
exit 1
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
require "webrick"
|
|
79
|
-
require "stringio"
|
|
80
|
-
require "socket"
|
|
81
|
-
|
|
82
|
-
# Ensure the main port is available — kill whatever is on it if needed
|
|
83
|
-
begin
|
|
84
|
-
test = TCPServer.new("0.0.0.0", @port)
|
|
85
|
-
test.close
|
|
86
|
-
rescue Errno::EADDRINUSE
|
|
87
|
-
free_port(@port)
|
|
88
|
-
# Verify the port is now free; raise if still occupied
|
|
89
|
-
begin
|
|
90
|
-
test = TCPServer.new("0.0.0.0", @port)
|
|
91
|
-
test.close
|
|
92
|
-
rescue Errno::EADDRINUSE
|
|
93
|
-
raise "Could not free port #{@port}"
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
Tina4.print_banner(host: @host, port: @port)
|
|
98
|
-
Tina4::Log.info("Starting Tina4 WEBrick server on http://#{@host}:#{@port}")
|
|
99
|
-
@server = WEBrick::HTTPServer.new(
|
|
100
|
-
BindAddress: @host,
|
|
101
|
-
Port: @port,
|
|
102
|
-
Logger: WEBrick::Log.new(File::NULL),
|
|
103
|
-
AccessLog: []
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Setup graceful shutdown with WEBrick server reference
|
|
107
|
-
Tina4::Shutdown.setup(server: @server)
|
|
108
|
-
|
|
109
|
-
# Use a custom servlet that passes ALL methods (including OPTIONS) to Rack
|
|
110
|
-
rack_app = @app
|
|
111
|
-
servlet = Class.new(WEBrick::HTTPServlet::AbstractServlet) do
|
|
112
|
-
define_method(:initialize) do |server, app|
|
|
113
|
-
super(server)
|
|
114
|
-
@app = app
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
%w[GET POST PUT DELETE PATCH HEAD OPTIONS].each do |http_method|
|
|
118
|
-
define_method("do_#{http_method}") do |webrick_req, webrick_res|
|
|
119
|
-
handle_request(webrick_req, webrick_res)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
define_method(:handle_request) do |webrick_req, webrick_res|
|
|
124
|
-
# Reject new requests during shutdown
|
|
125
|
-
if Tina4::Shutdown.shutting_down?
|
|
126
|
-
webrick_res.status = 503
|
|
127
|
-
webrick_res.body = '{"error":"Service shutting down"}'
|
|
128
|
-
webrick_res["content-type"] = "application/json"
|
|
129
|
-
return
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
Tina4::Shutdown.track_request do
|
|
133
|
-
env = build_rack_env(webrick_req)
|
|
134
|
-
status, headers, body = @app.call(env)
|
|
135
|
-
|
|
136
|
-
webrick_res.status = status
|
|
137
|
-
headers.each do |key, value|
|
|
138
|
-
if key.downcase == "set-cookie"
|
|
139
|
-
Array(value.split("\n")).each { |c| webrick_res.cookies << WEBrick::Cookie.parse_set_cookie(c) }
|
|
140
|
-
else
|
|
141
|
-
webrick_res[key] = value
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
response_body = ""
|
|
146
|
-
body.each { |chunk| response_body += chunk }
|
|
147
|
-
webrick_res.body = response_body
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
define_method(:build_rack_env) do |req|
|
|
152
|
-
input = StringIO.new(req.body || "")
|
|
153
|
-
env = {
|
|
154
|
-
"REQUEST_METHOD" => req.request_method,
|
|
155
|
-
"PATH_INFO" => req.path,
|
|
156
|
-
"QUERY_STRING" => req.query_string || "",
|
|
157
|
-
"SERVER_NAME" => webrick_req_host,
|
|
158
|
-
"SERVER_PORT" => webrick_req_port,
|
|
159
|
-
"CONTENT_TYPE" => req.content_type || "",
|
|
160
|
-
"CONTENT_LENGTH" => (req.content_length rescue 0).to_s,
|
|
161
|
-
"REMOTE_ADDR" => req.peeraddr&.last || "127.0.0.1",
|
|
162
|
-
"rack.input" => input,
|
|
163
|
-
"rack.errors" => $stderr,
|
|
164
|
-
"rack.url_scheme" => "http",
|
|
165
|
-
"rack.version" => [1, 3],
|
|
166
|
-
"rack.multithread" => true,
|
|
167
|
-
"rack.multiprocess" => false,
|
|
168
|
-
"rack.run_once" => false
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
req.header.each do |key, values|
|
|
172
|
-
env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
|
|
173
|
-
env[env_key] = values.join(", ")
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
env
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Store host/port for the servlet's build_rack_env
|
|
181
|
-
host = @host
|
|
182
|
-
port = @port.to_s
|
|
183
|
-
servlet.define_method(:webrick_req_host) { host }
|
|
184
|
-
servlet.define_method(:webrick_req_port) { port }
|
|
185
|
-
|
|
186
|
-
@server.mount("/", servlet, rack_app)
|
|
187
|
-
|
|
188
|
-
# Test port (port + 1000) — stable, no-browser
|
|
189
|
-
@ai_server = nil
|
|
190
|
-
@ai_thread = nil
|
|
191
|
-
no_ai_port = %w[true 1 yes].include?(ENV.fetch("TINA4_NO_AI_PORT", "").downcase)
|
|
192
|
-
is_debug = %w[true 1 yes].include?(ENV.fetch("TINA4_DEBUG", "").downcase)
|
|
193
|
-
|
|
194
|
-
if is_debug && !no_ai_port
|
|
195
|
-
ai_port = @port + 1000
|
|
196
|
-
begin
|
|
197
|
-
test = TCPServer.new("0.0.0.0", ai_port)
|
|
198
|
-
test.close
|
|
199
|
-
|
|
200
|
-
@ai_server = WEBrick::HTTPServer.new(
|
|
201
|
-
BindAddress: @host,
|
|
202
|
-
Port: ai_port,
|
|
203
|
-
Logger: WEBrick::Log.new(File::NULL),
|
|
204
|
-
AccessLog: []
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
# Wrap the rack app so AI-port requests are tagged
|
|
208
|
-
ai_rack_app = Tina4::AiPortRackApp.new(@app)
|
|
209
|
-
|
|
210
|
-
# Build a servlet identical to the main one but bound to the AI port host/port
|
|
211
|
-
ai_host = @host
|
|
212
|
-
ai_port_str = ai_port.to_s
|
|
213
|
-
ai_servlet = Class.new(WEBrick::HTTPServlet::AbstractServlet) do
|
|
214
|
-
define_method(:initialize) do |server, app|
|
|
215
|
-
super(server)
|
|
216
|
-
@app = app
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
%w[GET POST PUT DELETE PATCH HEAD OPTIONS].each do |http_method|
|
|
220
|
-
define_method("do_#{http_method}") do |webrick_req, webrick_res|
|
|
221
|
-
handle_request(webrick_req, webrick_res)
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
define_method(:handle_request) do |webrick_req, webrick_res|
|
|
226
|
-
if Tina4::Shutdown.shutting_down?
|
|
227
|
-
webrick_res.status = 503
|
|
228
|
-
webrick_res.body = '{"error":"Service shutting down"}'
|
|
229
|
-
webrick_res["content-type"] = "application/json"
|
|
230
|
-
return
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
Tina4::Shutdown.track_request do
|
|
234
|
-
env = build_rack_env(webrick_req)
|
|
235
|
-
status, headers, body = @app.call(env)
|
|
236
|
-
|
|
237
|
-
webrick_res.status = status
|
|
238
|
-
headers.each do |key, value|
|
|
239
|
-
if key.downcase == "set-cookie"
|
|
240
|
-
Array(value.split("\n")).each { |c| webrick_res.cookies << WEBrick::Cookie.parse_set_cookie(c) }
|
|
241
|
-
else
|
|
242
|
-
webrick_res[key] = value
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
response_body = ""
|
|
247
|
-
body.each { |chunk| response_body += chunk }
|
|
248
|
-
webrick_res.body = response_body
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
define_method(:build_rack_env) do |req|
|
|
253
|
-
input = StringIO.new(req.body || "")
|
|
254
|
-
env = {
|
|
255
|
-
"REQUEST_METHOD" => req.request_method,
|
|
256
|
-
"PATH_INFO" => req.path,
|
|
257
|
-
"QUERY_STRING" => req.query_string || "",
|
|
258
|
-
"SERVER_NAME" => webrick_req_host,
|
|
259
|
-
"SERVER_PORT" => webrick_req_port,
|
|
260
|
-
"CONTENT_TYPE" => req.content_type || "",
|
|
261
|
-
"CONTENT_LENGTH" => (req.content_length rescue 0).to_s,
|
|
262
|
-
"REMOTE_ADDR" => req.peeraddr&.last || "127.0.0.1",
|
|
263
|
-
"rack.input" => input,
|
|
264
|
-
"rack.errors" => $stderr,
|
|
265
|
-
"rack.url_scheme" => "http",
|
|
266
|
-
"rack.version" => [1, 3],
|
|
267
|
-
"rack.multithread" => true,
|
|
268
|
-
"rack.multiprocess" => false,
|
|
269
|
-
"rack.run_once" => false
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
req.header.each do |key, values|
|
|
273
|
-
env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
|
|
274
|
-
env[env_key] = values.join(", ")
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
env
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
ai_servlet.define_method(:webrick_req_host) { ai_host }
|
|
282
|
-
ai_servlet.define_method(:webrick_req_port) { ai_port_str }
|
|
283
|
-
|
|
284
|
-
@ai_server.mount("/", ai_servlet, ai_rack_app)
|
|
285
|
-
@ai_thread = Thread.new { @ai_server.start }
|
|
286
|
-
puts " Test Port: http://localhost:#{ai_port} (stable — no hot-reload)"
|
|
287
|
-
rescue Errno::EADDRINUSE
|
|
288
|
-
puts " Test Port: SKIPPED (port #{ai_port} in use)"
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
@server.start
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def stop
|
|
296
|
-
@ai_server&.shutdown
|
|
297
|
-
@ai_thread&.join(5)
|
|
298
|
-
@server&.shutdown
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
# Dispatch a Rack-style env through the Tina4 app and return [status, headers, body].
|
|
302
|
-
#
|
|
303
|
-
# Useful for testing and embedding — does not require a running server.
|
|
304
|
-
# Cross-framework parity with Python and Node.js.
|
|
305
|
-
#
|
|
306
|
-
# @param env [Hash] A Rack environment hash
|
|
307
|
-
# @return [Array] Rack-style response triple [status, headers, body]
|
|
308
|
-
def handle(env)
|
|
309
|
-
@app.call(env)
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
class WebServer
|
|
5
|
+
def initialize(app, host: "0.0.0.0", port: 7147)
|
|
6
|
+
@app = app
|
|
7
|
+
@host = host
|
|
8
|
+
@port = port
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Kill whatever process is listening on *port*.
|
|
12
|
+
# Uses lsof on macOS/Linux and netstat + taskkill on Windows.
|
|
13
|
+
# Raises RuntimeError if the port cannot be freed.
|
|
14
|
+
def free_port(port)
|
|
15
|
+
puts " Port #{port} in use — killing existing process..."
|
|
16
|
+
|
|
17
|
+
if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
|
18
|
+
output = `netstat -ano 2>&1`
|
|
19
|
+
pid = nil
|
|
20
|
+
output.each_line do |line|
|
|
21
|
+
if line.include?(":#{port}") && (line.include?("LISTENING") || line.include?("ESTABLISHED"))
|
|
22
|
+
parts = line.strip.split(/\s+/)
|
|
23
|
+
candidate = parts.last
|
|
24
|
+
if candidate =~ /^\d+$/
|
|
25
|
+
pid = candidate
|
|
26
|
+
break
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
if pid
|
|
31
|
+
system("taskkill /PID #{pid} /F")
|
|
32
|
+
else
|
|
33
|
+
raise "Could not free port #{port}: no PID found"
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
pids = `lsof -ti :#{port} 2>/dev/null`.strip.split("\n")
|
|
37
|
+
if pids.empty?
|
|
38
|
+
return # Nothing found — port may have freed itself
|
|
39
|
+
end
|
|
40
|
+
pids.each do |pid|
|
|
41
|
+
pid = pid.strip
|
|
42
|
+
next unless pid =~ /^\d+$/
|
|
43
|
+
begin
|
|
44
|
+
Process.kill("TERM", pid.to_i)
|
|
45
|
+
rescue Errno::ESRCH
|
|
46
|
+
# Process already gone
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Give the OS a moment to reclaim the port
|
|
52
|
+
sleep(0.5)
|
|
53
|
+
puts " Port #{port} freed"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def start
|
|
57
|
+
is_managed = ARGV.include?('--managed')
|
|
58
|
+
unless is_managed || ENV['TINA4_OVERRIDE_CLIENT'] == 'true'
|
|
59
|
+
puts
|
|
60
|
+
puts '=' * 60
|
|
61
|
+
puts
|
|
62
|
+
puts ' Tina4 must be started with the tina4 CLI:'
|
|
63
|
+
puts
|
|
64
|
+
puts ' tina4 serve (development)'
|
|
65
|
+
puts ' tina4 serve --production (production)'
|
|
66
|
+
puts
|
|
67
|
+
puts ' Install: cargo install tina4'
|
|
68
|
+
puts ' Docs: https://tina4.com'
|
|
69
|
+
puts
|
|
70
|
+
puts ' To run directly, add to .env:'
|
|
71
|
+
puts ' TINA4_OVERRIDE_CLIENT=true'
|
|
72
|
+
puts
|
|
73
|
+
puts '=' * 60
|
|
74
|
+
puts
|
|
75
|
+
exit 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
require "webrick"
|
|
79
|
+
require "stringio"
|
|
80
|
+
require "socket"
|
|
81
|
+
|
|
82
|
+
# Ensure the main port is available — kill whatever is on it if needed
|
|
83
|
+
begin
|
|
84
|
+
test = TCPServer.new("0.0.0.0", @port)
|
|
85
|
+
test.close
|
|
86
|
+
rescue Errno::EADDRINUSE
|
|
87
|
+
free_port(@port)
|
|
88
|
+
# Verify the port is now free; raise if still occupied
|
|
89
|
+
begin
|
|
90
|
+
test = TCPServer.new("0.0.0.0", @port)
|
|
91
|
+
test.close
|
|
92
|
+
rescue Errno::EADDRINUSE
|
|
93
|
+
raise "Could not free port #{@port}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
Tina4.print_banner(host: @host, port: @port)
|
|
98
|
+
Tina4::Log.info("Starting Tina4 WEBrick server on http://#{@host}:#{@port}")
|
|
99
|
+
@server = WEBrick::HTTPServer.new(
|
|
100
|
+
BindAddress: @host,
|
|
101
|
+
Port: @port,
|
|
102
|
+
Logger: WEBrick::Log.new(File::NULL),
|
|
103
|
+
AccessLog: []
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Setup graceful shutdown with WEBrick server reference
|
|
107
|
+
Tina4::Shutdown.setup(server: @server)
|
|
108
|
+
|
|
109
|
+
# Use a custom servlet that passes ALL methods (including OPTIONS) to Rack
|
|
110
|
+
rack_app = @app
|
|
111
|
+
servlet = Class.new(WEBrick::HTTPServlet::AbstractServlet) do
|
|
112
|
+
define_method(:initialize) do |server, app|
|
|
113
|
+
super(server)
|
|
114
|
+
@app = app
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
%w[GET POST PUT DELETE PATCH HEAD OPTIONS].each do |http_method|
|
|
118
|
+
define_method("do_#{http_method}") do |webrick_req, webrick_res|
|
|
119
|
+
handle_request(webrick_req, webrick_res)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
define_method(:handle_request) do |webrick_req, webrick_res|
|
|
124
|
+
# Reject new requests during shutdown
|
|
125
|
+
if Tina4::Shutdown.shutting_down?
|
|
126
|
+
webrick_res.status = 503
|
|
127
|
+
webrick_res.body = '{"error":"Service shutting down"}'
|
|
128
|
+
webrick_res["content-type"] = "application/json"
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
Tina4::Shutdown.track_request do
|
|
133
|
+
env = build_rack_env(webrick_req)
|
|
134
|
+
status, headers, body = @app.call(env)
|
|
135
|
+
|
|
136
|
+
webrick_res.status = status
|
|
137
|
+
headers.each do |key, value|
|
|
138
|
+
if key.downcase == "set-cookie"
|
|
139
|
+
Array(value.split("\n")).each { |c| webrick_res.cookies << WEBrick::Cookie.parse_set_cookie(c) }
|
|
140
|
+
else
|
|
141
|
+
webrick_res[key] = value
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
response_body = ""
|
|
146
|
+
body.each { |chunk| response_body += chunk }
|
|
147
|
+
webrick_res.body = response_body
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
define_method(:build_rack_env) do |req|
|
|
152
|
+
input = StringIO.new(req.body || "")
|
|
153
|
+
env = {
|
|
154
|
+
"REQUEST_METHOD" => req.request_method,
|
|
155
|
+
"PATH_INFO" => req.path,
|
|
156
|
+
"QUERY_STRING" => req.query_string || "",
|
|
157
|
+
"SERVER_NAME" => webrick_req_host,
|
|
158
|
+
"SERVER_PORT" => webrick_req_port,
|
|
159
|
+
"CONTENT_TYPE" => req.content_type || "",
|
|
160
|
+
"CONTENT_LENGTH" => (req.content_length rescue 0).to_s,
|
|
161
|
+
"REMOTE_ADDR" => req.peeraddr&.last || "127.0.0.1",
|
|
162
|
+
"rack.input" => input,
|
|
163
|
+
"rack.errors" => $stderr,
|
|
164
|
+
"rack.url_scheme" => "http",
|
|
165
|
+
"rack.version" => [1, 3],
|
|
166
|
+
"rack.multithread" => true,
|
|
167
|
+
"rack.multiprocess" => false,
|
|
168
|
+
"rack.run_once" => false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
req.header.each do |key, values|
|
|
172
|
+
env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
|
|
173
|
+
env[env_key] = values.join(", ")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
env
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Store host/port for the servlet's build_rack_env
|
|
181
|
+
host = @host
|
|
182
|
+
port = @port.to_s
|
|
183
|
+
servlet.define_method(:webrick_req_host) { host }
|
|
184
|
+
servlet.define_method(:webrick_req_port) { port }
|
|
185
|
+
|
|
186
|
+
@server.mount("/", servlet, rack_app)
|
|
187
|
+
|
|
188
|
+
# Test port (port + 1000) — stable, no-browser
|
|
189
|
+
@ai_server = nil
|
|
190
|
+
@ai_thread = nil
|
|
191
|
+
no_ai_port = %w[true 1 yes].include?(ENV.fetch("TINA4_NO_AI_PORT", "").downcase)
|
|
192
|
+
is_debug = %w[true 1 yes].include?(ENV.fetch("TINA4_DEBUG", "").downcase)
|
|
193
|
+
|
|
194
|
+
if is_debug && !no_ai_port
|
|
195
|
+
ai_port = @port + 1000
|
|
196
|
+
begin
|
|
197
|
+
test = TCPServer.new("0.0.0.0", ai_port)
|
|
198
|
+
test.close
|
|
199
|
+
|
|
200
|
+
@ai_server = WEBrick::HTTPServer.new(
|
|
201
|
+
BindAddress: @host,
|
|
202
|
+
Port: ai_port,
|
|
203
|
+
Logger: WEBrick::Log.new(File::NULL),
|
|
204
|
+
AccessLog: []
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Wrap the rack app so AI-port requests are tagged
|
|
208
|
+
ai_rack_app = Tina4::AiPortRackApp.new(@app)
|
|
209
|
+
|
|
210
|
+
# Build a servlet identical to the main one but bound to the AI port host/port
|
|
211
|
+
ai_host = @host
|
|
212
|
+
ai_port_str = ai_port.to_s
|
|
213
|
+
ai_servlet = Class.new(WEBrick::HTTPServlet::AbstractServlet) do
|
|
214
|
+
define_method(:initialize) do |server, app|
|
|
215
|
+
super(server)
|
|
216
|
+
@app = app
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
%w[GET POST PUT DELETE PATCH HEAD OPTIONS].each do |http_method|
|
|
220
|
+
define_method("do_#{http_method}") do |webrick_req, webrick_res|
|
|
221
|
+
handle_request(webrick_req, webrick_res)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
define_method(:handle_request) do |webrick_req, webrick_res|
|
|
226
|
+
if Tina4::Shutdown.shutting_down?
|
|
227
|
+
webrick_res.status = 503
|
|
228
|
+
webrick_res.body = '{"error":"Service shutting down"}'
|
|
229
|
+
webrick_res["content-type"] = "application/json"
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
Tina4::Shutdown.track_request do
|
|
234
|
+
env = build_rack_env(webrick_req)
|
|
235
|
+
status, headers, body = @app.call(env)
|
|
236
|
+
|
|
237
|
+
webrick_res.status = status
|
|
238
|
+
headers.each do |key, value|
|
|
239
|
+
if key.downcase == "set-cookie"
|
|
240
|
+
Array(value.split("\n")).each { |c| webrick_res.cookies << WEBrick::Cookie.parse_set_cookie(c) }
|
|
241
|
+
else
|
|
242
|
+
webrick_res[key] = value
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
response_body = ""
|
|
247
|
+
body.each { |chunk| response_body += chunk }
|
|
248
|
+
webrick_res.body = response_body
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
define_method(:build_rack_env) do |req|
|
|
253
|
+
input = StringIO.new(req.body || "")
|
|
254
|
+
env = {
|
|
255
|
+
"REQUEST_METHOD" => req.request_method,
|
|
256
|
+
"PATH_INFO" => req.path,
|
|
257
|
+
"QUERY_STRING" => req.query_string || "",
|
|
258
|
+
"SERVER_NAME" => webrick_req_host,
|
|
259
|
+
"SERVER_PORT" => webrick_req_port,
|
|
260
|
+
"CONTENT_TYPE" => req.content_type || "",
|
|
261
|
+
"CONTENT_LENGTH" => (req.content_length rescue 0).to_s,
|
|
262
|
+
"REMOTE_ADDR" => req.peeraddr&.last || "127.0.0.1",
|
|
263
|
+
"rack.input" => input,
|
|
264
|
+
"rack.errors" => $stderr,
|
|
265
|
+
"rack.url_scheme" => "http",
|
|
266
|
+
"rack.version" => [1, 3],
|
|
267
|
+
"rack.multithread" => true,
|
|
268
|
+
"rack.multiprocess" => false,
|
|
269
|
+
"rack.run_once" => false
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
req.header.each do |key, values|
|
|
273
|
+
env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
|
|
274
|
+
env[env_key] = values.join(", ")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
env
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
ai_servlet.define_method(:webrick_req_host) { ai_host }
|
|
282
|
+
ai_servlet.define_method(:webrick_req_port) { ai_port_str }
|
|
283
|
+
|
|
284
|
+
@ai_server.mount("/", ai_servlet, ai_rack_app)
|
|
285
|
+
@ai_thread = Thread.new { @ai_server.start }
|
|
286
|
+
puts " Test Port: http://localhost:#{ai_port} (stable — no hot-reload)"
|
|
287
|
+
rescue Errno::EADDRINUSE
|
|
288
|
+
puts " Test Port: SKIPPED (port #{ai_port} in use)"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
@server.start
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def stop
|
|
296
|
+
@ai_server&.shutdown
|
|
297
|
+
@ai_thread&.join(5)
|
|
298
|
+
@server&.shutdown
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Dispatch a Rack-style env through the Tina4 app and return [status, headers, body].
|
|
302
|
+
#
|
|
303
|
+
# Useful for testing and embedding — does not require a running server.
|
|
304
|
+
# Cross-framework parity with Python and Node.js.
|
|
305
|
+
#
|
|
306
|
+
# @param env [Hash] A Rack environment hash
|
|
307
|
+
# @return [Array] Rack-style response triple [status, headers, body]
|
|
308
|
+
def handle(env)
|
|
309
|
+
@app.call(env)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|