fresco 0.0.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 +7 -0
- data/exe/fresco +3 -0
- data/lib/fresco/application.rb +12 -0
- data/lib/fresco/cli/build.rb +682 -0
- data/lib/fresco/cli/dev.rb +17 -0
- data/lib/fresco/cli/dev_loop.rb +815 -0
- data/lib/fresco/cli/new.rb +120 -0
- data/lib/fresco/cli/release.rb +76 -0
- data/lib/fresco/cli.rb +56 -0
- data/lib/fresco/database_config.rb +34 -0
- data/lib/fresco/generators/app/Gemfile.tt +18 -0
- data/lib/fresco/generators/app/README.md.tt +32 -0
- data/lib/fresco/generators/app/app/action.rb.tt +20 -0
- data/lib/fresco/generators/app/app/actions/root_path.rb.tt +5 -0
- data/lib/fresco/generators/app/app/views/layouts/application.html.erb +29 -0
- data/lib/fresco/generators/app/app/views/root_path.html.erb +8 -0
- data/lib/fresco/generators/app/app.rb.tt +15 -0
- data/lib/fresco/generators/app/bin/build +2 -0
- data/lib/fresco/generators/app/bin/dev +2 -0
- data/lib/fresco/generators/app/bin/release +2 -0
- data/lib/fresco/generators/app/config/app.rb.tt +26 -0
- data/lib/fresco/generators/app/config/database.rb +17 -0
- data/lib/fresco/generators/app/config/routes.rb +11 -0
- data/lib/fresco/generators/app/db/schema.rb +14 -0
- data/lib/fresco/generators/app/public/404.html +87 -0
- data/lib/fresco/generators/app/public/500.html +84 -0
- data/lib/fresco/migration_builder.rb +55 -0
- data/lib/fresco/model_builder.rb +54 -0
- data/lib/fresco/paths.rb +20 -0
- data/lib/fresco/router.rb +67 -0
- data/lib/fresco/runtime/boot.rb +34 -0
- data/lib/fresco/runtime/db_postgres.rb +403 -0
- data/lib/fresco/runtime/db_sqlite.rb +495 -0
- data/lib/fresco/runtime/http.c +456 -0
- data/lib/fresco/runtime/postgres.c +339 -0
- data/lib/fresco/runtime/runtime.rb +1810 -0
- data/lib/fresco/runtime/sqlite.c +220 -0
- data/lib/fresco/runtime/welcome.rb +152 -0
- data/lib/fresco/schema_builder.rb +71 -0
- data/lib/fresco/templates/dispatch.rb.erb +32 -0
- data/lib/fresco/templates/layout_dispatch.rb.erb +16 -0
- data/lib/fresco/templates/manifest.rb.erb +5 -0
- data/lib/fresco/templates/migrations.rb.erb +152 -0
- data/lib/fresco/templates/model.rb.erb +223 -0
- data/lib/fresco/templates/view.rb.erb +5 -0
- data/lib/fresco/version.rb +3 -0
- data/lib/fresco.rb +61 -0
- metadata +115 -0
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
# Fresco: dev loop. CRuby only — no Spinel rebuild.
|
|
2
|
+
#
|
|
3
|
+
# 1. Regenerate generated/* via `fresco build`.
|
|
4
|
+
# 2. Watch config/routes.rb + app/actions/ + app/views/ for changes;
|
|
5
|
+
# rebuild + reload on edit.
|
|
6
|
+
# 3. Listen on -p PORT (default 3030) with a tiny TCPServer-backed
|
|
7
|
+
# HTTP loop that calls the same `dispatch_request` the prod
|
|
8
|
+
# binary uses. ERB / action edits round-trip without a Spinel
|
|
9
|
+
# recompile; the C shim only matters in release.
|
|
10
|
+
#
|
|
11
|
+
# Loaded by Fresco::CLI::Dev#run into top-level scope so methods
|
|
12
|
+
# defined here share scope with the methods generated/runtime.rb
|
|
13
|
+
# defines on load.
|
|
14
|
+
|
|
15
|
+
require "socket"
|
|
16
|
+
|
|
17
|
+
ROOT = Dir.pwd
|
|
18
|
+
# Defensive: dev's helpers all assume relative paths from the user
|
|
19
|
+
# app's cwd; bind it explicitly so `fresco dev` invoked from a
|
|
20
|
+
# subdirectory still walks up to the project root cleanly when
|
|
21
|
+
# we add that capability later.
|
|
22
|
+
Dir.chdir(ROOT)
|
|
23
|
+
|
|
24
|
+
WATCH_GLOBS = [
|
|
25
|
+
"config/routes.rb",
|
|
26
|
+
"config/app.rb",
|
|
27
|
+
"config/database.rb",
|
|
28
|
+
"db/schema.rb",
|
|
29
|
+
"app/action.rb",
|
|
30
|
+
"app/actions/**/*.rb",
|
|
31
|
+
"app/models/**/*.rb",
|
|
32
|
+
"app/views/**/*.erb",
|
|
33
|
+
].freeze
|
|
34
|
+
RELOAD_LOCK = Mutex.new
|
|
35
|
+
|
|
36
|
+
# Spinel's FFI DSL (`ffi_func`, `ffi_cflags`, …) is analyzer-only — the
|
|
37
|
+
# AOT pipeline rewrites it before runtime. Under CRuby those names
|
|
38
|
+
# don't exist, which would crash any `load` of a runtime that declares
|
|
39
|
+
# FFI modules. Stub them as no-ops on `Module` so we can load
|
|
40
|
+
# generated/runtime.rb in dev.
|
|
41
|
+
class Module
|
|
42
|
+
def ffi_lib(*); end
|
|
43
|
+
def ffi_cflags(*); end
|
|
44
|
+
def ffi_func(*); end
|
|
45
|
+
def ffi_buffer(*); end
|
|
46
|
+
def ffi_read_u32(*); end
|
|
47
|
+
def ffi_read_i32(*); end
|
|
48
|
+
def ffi_read_ptr(*); end
|
|
49
|
+
def ffi_const(name, value)
|
|
50
|
+
const_set(name, value) unless const_defined?(name)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# `load`-ing the generated runtime + actions on every reload re-runs
|
|
55
|
+
# their toplevel `FOO = ...` assignments, which Ruby warns about. The
|
|
56
|
+
# redefinitions are intentional under the dev reloader, so filter just
|
|
57
|
+
# those two warning shapes — everything else (uninitialized ivars,
|
|
58
|
+
# method redefinitions, etc.) still flows through.
|
|
59
|
+
module SilenceReloadConstWarnings
|
|
60
|
+
IGNORE = /already initialized constant|previous definition of/
|
|
61
|
+
def warn(msg, category: nil)
|
|
62
|
+
return if msg.is_a?(String) && msg.match?(IGNORE)
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
Warning.singleton_class.prepend(SilenceReloadConstWarnings)
|
|
67
|
+
|
|
68
|
+
def build!
|
|
69
|
+
# Subprocess (vs. in-process call) keeps Fresco's build-time DSL
|
|
70
|
+
# accumulators (Fresco.models, Fresco.app.routes.entries, etc.) from
|
|
71
|
+
# leaking across rebuilds. The cost is one fork + ruby startup per
|
|
72
|
+
# reload, which is fine for development.
|
|
73
|
+
unless system("bundle", "exec", "fresco", "build")
|
|
74
|
+
warn "[dev] fresco build failed — keeping previous dispatcher"
|
|
75
|
+
return false
|
|
76
|
+
end
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# `load` (vs require) re-evaluates on each call so reloads pick up
|
|
81
|
+
# edits. Paths are relative to ROOT (we chdir'd above).
|
|
82
|
+
def reload!
|
|
83
|
+
RELOAD_LOCK.synchronize do
|
|
84
|
+
load "generated/runtime.rb"
|
|
85
|
+
Dir.glob("generated/views/*.rb").sort.each { |f| load f }
|
|
86
|
+
load "generated/layout_dispatch.rb"
|
|
87
|
+
# db_adapter mirrors boot.rb's order: between layout_dispatch and
|
|
88
|
+
# config/app so the FFI module + Fresco::Db::Active are defined
|
|
89
|
+
# before user code can reference them. `fresco build` always emits
|
|
90
|
+
# this file (an empty stub when no adapter is configured), so the
|
|
91
|
+
# load never misses.
|
|
92
|
+
load "generated/db_adapter.rb"
|
|
93
|
+
# Migrations runner — defines Fresco::DbMigrations. Used by
|
|
94
|
+
# `./app db:migrate` and `fresco dev db:migrate` alike, so it has
|
|
95
|
+
# to be on the dev load path too. Boot.rb requires it between
|
|
96
|
+
# db_adapter and config/app; we mirror that order here.
|
|
97
|
+
load "generated/migrations.rb"
|
|
98
|
+
load "config/app.rb"
|
|
99
|
+
# config/database.rb populates Fresco.app.database_config from the
|
|
100
|
+
# running process's environment (ENV.fetch lives inside the file).
|
|
101
|
+
# `fresco build` also synthesises a placeholder when the user
|
|
102
|
+
# didn't supply one, so the load is unconditional.
|
|
103
|
+
load "config/database.rb" if File.exist?("config/database.rb")
|
|
104
|
+
# Generated models load before user action.rb so action classes
|
|
105
|
+
# can reference them. Models live under generated/models/ (one .rb
|
|
106
|
+
# per declared model in app/models/) — same file location dev and
|
|
107
|
+
# release use.
|
|
108
|
+
Dir.glob("generated/models/*.rb").sort.each { |f| load f }
|
|
109
|
+
load "app/action.rb"
|
|
110
|
+
Dir.glob("app/actions/**/*.rb").sort.each { |f| load f }
|
|
111
|
+
# Fresco-supplied default welcome action — only present when
|
|
112
|
+
# config/routes.rb has no `root to:` and `fresco build` emitted
|
|
113
|
+
# the fallback. Dispatch.rb references Fresco::Welcome, so load
|
|
114
|
+
# it before the dispatcher.
|
|
115
|
+
load "generated/welcome.rb" if File.exist?("generated/welcome.rb")
|
|
116
|
+
load "generated/dispatch.rb"
|
|
117
|
+
|
|
118
|
+
# The C shim isn't linked under CRuby — `ffi_func` was a no-op
|
|
119
|
+
# above, so `Sock` exists but has no methods. Provide CRuby
|
|
120
|
+
# stand-ins for the handful of Sock entries the runtime calls
|
|
121
|
+
# from non-server code paths (i.e. inside actions like Files::Show).
|
|
122
|
+
# The server-loop entries (sphttp_listen/accept/read/etc.) aren't
|
|
123
|
+
# needed because serve_dev replaces that loop entirely.
|
|
124
|
+
Sock.define_singleton_method(:sphttp_file_size) do |path|
|
|
125
|
+
File.exist?(path) ? File.size(path) : -1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# HMAC stand-ins. Under Spinel these are the public-domain
|
|
129
|
+
# SHA-256/HMAC code in generated/runtime/http.c; under CRuby we
|
|
130
|
+
# route through OpenSSL::HMAC so the Session class round-trips
|
|
131
|
+
# identically.
|
|
132
|
+
require "openssl"
|
|
133
|
+
require "base64"
|
|
134
|
+
Sock.define_singleton_method(:sphttp_hmac_sha256_hex) do |key, msg|
|
|
135
|
+
OpenSSL::HMAC.hexdigest("SHA256", key, msg)
|
|
136
|
+
end
|
|
137
|
+
Sock.define_singleton_method(:sphttp_hmac_sha256_b64url) do |key, msg|
|
|
138
|
+
Base64.urlsafe_encode64(OpenSSL::HMAC.digest("SHA256", key, msg), padding: false)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Monotonic-clock stand-ins for the request-timing primitives the
|
|
142
|
+
# production handle_connection uses. Actions can reach for these
|
|
143
|
+
# (e.g. an around filter that times its work) and `fresco dev`
|
|
144
|
+
# needs to honor the same API surface, even though serve_dev's own
|
|
145
|
+
# loop doesn't rely on them.
|
|
146
|
+
Sock.instance_variable_set(:@dev_mark, 0)
|
|
147
|
+
Sock.define_singleton_method(:sphttp_mark_now) do
|
|
148
|
+
Sock.instance_variable_set(:@dev_mark, Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond))
|
|
149
|
+
0
|
|
150
|
+
end
|
|
151
|
+
Sock.define_singleton_method(:sphttp_elapsed_micros) do
|
|
152
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) -
|
|
153
|
+
Sock.instance_variable_get(:@dev_mark)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# DB adapter stand-ins. config/database.rb (loaded above) sets
|
|
157
|
+
# Fresco.app.database_config; we install the matching FFI stubs so
|
|
158
|
+
# actions hitting Fresco::Db::Active work under CRuby. Production
|
|
159
|
+
# routes through the linked C shim instead; the Ruby surface is
|
|
160
|
+
# identical so dev and prod behave the same from action code.
|
|
161
|
+
db_adapter = Fresco.database_adapter || :none
|
|
162
|
+
|
|
163
|
+
if db_adapter == :sqlite
|
|
164
|
+
require "sqlite3"
|
|
165
|
+
Sock.instance_variable_set(:@dev_sqlite_handles, [nil] * 16)
|
|
166
|
+
Sock.instance_variable_set(:@dev_sqlite_stmts, [nil] * 64)
|
|
167
|
+
Sqlite.define_singleton_method(:fresco_sqlite_open) do |path|
|
|
168
|
+
handles = Sock.instance_variable_get(:@dev_sqlite_handles)
|
|
169
|
+
slot = handles.index(nil)
|
|
170
|
+
return -1 if slot.nil?
|
|
171
|
+
begin
|
|
172
|
+
handles[slot] = ::SQLite3::Database.new(path)
|
|
173
|
+
slot + 1
|
|
174
|
+
rescue ::SQLite3::Exception
|
|
175
|
+
-1
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
Sqlite.define_singleton_method(:fresco_sqlite_close) do |h|
|
|
179
|
+
handles = Sock.instance_variable_get(:@dev_sqlite_handles)
|
|
180
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
181
|
+
return -1 if h < 1 || h > handles.length
|
|
182
|
+
db = handles[h - 1]
|
|
183
|
+
return -1 if db.nil?
|
|
184
|
+
stmts.each_with_index do |s, i|
|
|
185
|
+
next if s.nil?
|
|
186
|
+
if s[0] == db
|
|
187
|
+
s[1].close unless s[1].closed?
|
|
188
|
+
stmts[i] = nil
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
db.close
|
|
192
|
+
handles[h - 1] = nil
|
|
193
|
+
0
|
|
194
|
+
end
|
|
195
|
+
Sqlite.define_singleton_method(:fresco_sqlite_exec) do |h, sql|
|
|
196
|
+
handles = Sock.instance_variable_get(:@dev_sqlite_handles)
|
|
197
|
+
return -1 if h < 1 || h > handles.length
|
|
198
|
+
db = handles[h - 1]
|
|
199
|
+
return -1 if db.nil?
|
|
200
|
+
begin
|
|
201
|
+
db.execute_batch(sql)
|
|
202
|
+
0
|
|
203
|
+
rescue ::SQLite3::Exception
|
|
204
|
+
-1
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
Sqlite.define_singleton_method(:fresco_sqlite_prepare) do |h, sql|
|
|
208
|
+
handles = Sock.instance_variable_get(:@dev_sqlite_handles)
|
|
209
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
210
|
+
return -1 if h < 1 || h > handles.length
|
|
211
|
+
db = handles[h - 1]
|
|
212
|
+
return -1 if db.nil?
|
|
213
|
+
slot = stmts.index(nil)
|
|
214
|
+
return -1 if slot.nil?
|
|
215
|
+
begin
|
|
216
|
+
stmt = db.prepare(sql)
|
|
217
|
+
stmts[slot] = [db, stmt, nil] # [db, stmt, last_step_row_or_nil]
|
|
218
|
+
slot + 1
|
|
219
|
+
rescue ::SQLite3::Exception
|
|
220
|
+
-1
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
Sqlite.define_singleton_method(:fresco_sqlite_bind_str) do |cid, idx, value|
|
|
224
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
225
|
+
return -1 if cid < 1 || cid > stmts.length
|
|
226
|
+
entry = stmts[cid - 1]
|
|
227
|
+
return -1 if entry.nil?
|
|
228
|
+
begin
|
|
229
|
+
entry[1].bind_param(idx, value)
|
|
230
|
+
0
|
|
231
|
+
rescue ::SQLite3::Exception
|
|
232
|
+
-1
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
Sqlite.define_singleton_method(:fresco_sqlite_bind_int) do |cid, idx, value|
|
|
236
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
237
|
+
return -1 if cid < 1 || cid > stmts.length
|
|
238
|
+
entry = stmts[cid - 1]
|
|
239
|
+
return -1 if entry.nil?
|
|
240
|
+
begin
|
|
241
|
+
entry[1].bind_param(idx, value)
|
|
242
|
+
0
|
|
243
|
+
rescue ::SQLite3::Exception
|
|
244
|
+
-1
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
Sqlite.define_singleton_method(:fresco_sqlite_step) do |cid|
|
|
248
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
249
|
+
return -1 if cid < 1 || cid > stmts.length
|
|
250
|
+
entry = stmts[cid - 1]
|
|
251
|
+
return -1 if entry.nil?
|
|
252
|
+
# The gem's stmt acts as an iterator; capture the next row (or
|
|
253
|
+
# nil at end-of-result) so col_* lookups read off the same row.
|
|
254
|
+
entry[2] = entry[1].step
|
|
255
|
+
return 0 if entry[2].nil?
|
|
256
|
+
1
|
|
257
|
+
end
|
|
258
|
+
Sqlite.define_singleton_method(:fresco_sqlite_col_str) do |cid, idx|
|
|
259
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
260
|
+
return "" if cid < 1 || cid > stmts.length
|
|
261
|
+
entry = stmts[cid - 1]
|
|
262
|
+
return "" if entry.nil? || entry[2].nil?
|
|
263
|
+
v = entry[2][idx]
|
|
264
|
+
v.nil? ? "" : v.to_s
|
|
265
|
+
end
|
|
266
|
+
Sqlite.define_singleton_method(:fresco_sqlite_col_int) do |cid, idx|
|
|
267
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
268
|
+
return 0 if cid < 1 || cid > stmts.length
|
|
269
|
+
entry = stmts[cid - 1]
|
|
270
|
+
return 0 if entry.nil? || entry[2].nil?
|
|
271
|
+
v = entry[2][idx]
|
|
272
|
+
v.nil? ? 0 : v.to_i
|
|
273
|
+
end
|
|
274
|
+
Sqlite.define_singleton_method(:fresco_sqlite_col_count) do |cid|
|
|
275
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
276
|
+
return 0 if cid < 1 || cid > stmts.length
|
|
277
|
+
entry = stmts[cid - 1]
|
|
278
|
+
return 0 if entry.nil?
|
|
279
|
+
entry[1].columns.length
|
|
280
|
+
end
|
|
281
|
+
Sqlite.define_singleton_method(:fresco_sqlite_finalize) do |cid|
|
|
282
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
283
|
+
return 0 if cid < 1 || cid > stmts.length
|
|
284
|
+
entry = stmts[cid - 1]
|
|
285
|
+
return 0 if entry.nil?
|
|
286
|
+
entry[1].close unless entry[1].closed?
|
|
287
|
+
stmts[cid - 1] = nil
|
|
288
|
+
0
|
|
289
|
+
end
|
|
290
|
+
Sqlite.define_singleton_method(:fresco_sqlite_reset) do |cid|
|
|
291
|
+
stmts = Sock.instance_variable_get(:@dev_sqlite_stmts)
|
|
292
|
+
return -1 if cid < 1 || cid > stmts.length
|
|
293
|
+
entry = stmts[cid - 1]
|
|
294
|
+
return -1 if entry.nil?
|
|
295
|
+
entry[1].reset!
|
|
296
|
+
entry[2] = nil
|
|
297
|
+
0
|
|
298
|
+
end
|
|
299
|
+
Sqlite.define_singleton_method(:fresco_sqlite_last_insert_rowid) do |h|
|
|
300
|
+
handles = Sock.instance_variable_get(:@dev_sqlite_handles)
|
|
301
|
+
return -1 if h < 1 || h > handles.length
|
|
302
|
+
db = handles[h - 1]
|
|
303
|
+
return -1 if db.nil?
|
|
304
|
+
db.last_insert_row_id.to_i
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
if db_adapter == :postgres
|
|
309
|
+
# Postgres stand-ins. The `pg` gem is loaded lazily so apps on
|
|
310
|
+
# the SQLite adapter (or with no DB) don't need it installed.
|
|
311
|
+
# Mirrors the production generated/runtime/postgres.c shim:
|
|
312
|
+
# per-cursor state holding {conn, sql, accumulated binds,
|
|
313
|
+
# PGresult, row index}; bind_*/step lazy-execute via PQexecParams
|
|
314
|
+
# on first step.
|
|
315
|
+
begin
|
|
316
|
+
require "pg"
|
|
317
|
+
rescue LoadError
|
|
318
|
+
abort "[dev] config/database.rb declares :postgres but the `pg` gem " \
|
|
319
|
+
"is not installed. Add `gem \"pg\"` to the :development group " \
|
|
320
|
+
"in your Gemfile and bundle."
|
|
321
|
+
end
|
|
322
|
+
Sock.instance_variable_set(:@dev_pg_handles, [nil] * 16)
|
|
323
|
+
Sock.instance_variable_set(:@dev_pg_stmts, [nil] * 64)
|
|
324
|
+
Pg.define_singleton_method(:fresco_pg_open) do |url|
|
|
325
|
+
handles = Sock.instance_variable_get(:@dev_pg_handles)
|
|
326
|
+
slot = handles.index(nil)
|
|
327
|
+
return -1 if slot.nil?
|
|
328
|
+
begin
|
|
329
|
+
handles[slot] = ::PG.connect(url)
|
|
330
|
+
slot + 1
|
|
331
|
+
rescue ::PG::Error
|
|
332
|
+
-1
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
Pg.define_singleton_method(:fresco_pg_close) do |h|
|
|
336
|
+
handles = Sock.instance_variable_get(:@dev_pg_handles)
|
|
337
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
338
|
+
return -1 if h < 1 || h > handles.length
|
|
339
|
+
conn = handles[h - 1]
|
|
340
|
+
return -1 if conn.nil?
|
|
341
|
+
stmts.each_with_index do |s, i|
|
|
342
|
+
next if s.nil?
|
|
343
|
+
if s[:conn] == conn
|
|
344
|
+
stmts[i] = nil
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
conn.close
|
|
348
|
+
handles[h - 1] = nil
|
|
349
|
+
0
|
|
350
|
+
end
|
|
351
|
+
Pg.define_singleton_method(:fresco_pg_exec) do |h, sql|
|
|
352
|
+
handles = Sock.instance_variable_get(:@dev_pg_handles)
|
|
353
|
+
return -1 if h < 1 || h > handles.length
|
|
354
|
+
conn = handles[h - 1]
|
|
355
|
+
return -1 if conn.nil?
|
|
356
|
+
begin
|
|
357
|
+
conn.exec(sql)
|
|
358
|
+
0
|
|
359
|
+
rescue ::PG::Error
|
|
360
|
+
-1
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
Pg.define_singleton_method(:fresco_pg_prepare) do |h, sql|
|
|
364
|
+
handles = Sock.instance_variable_get(:@dev_pg_handles)
|
|
365
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
366
|
+
return -1 if h < 1 || h > handles.length
|
|
367
|
+
conn = handles[h - 1]
|
|
368
|
+
return -1 if conn.nil?
|
|
369
|
+
slot = stmts.index(nil)
|
|
370
|
+
return -1 if slot.nil?
|
|
371
|
+
# Defer execution until the first step, like the C shim. Binds
|
|
372
|
+
# accumulate into :params (1-indexed, sparse), then PQexecParams
|
|
373
|
+
# equivalent fires on first step.
|
|
374
|
+
stmts[slot] = { conn: conn, sql: sql, params: [], result: nil, row: -1, ntuples: 0 }
|
|
375
|
+
slot + 1
|
|
376
|
+
end
|
|
377
|
+
Pg.define_singleton_method(:fresco_pg_bind_str) do |cid, idx, value|
|
|
378
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
379
|
+
return -1 if cid < 1 || cid > stmts.length
|
|
380
|
+
entry = stmts[cid - 1]
|
|
381
|
+
return -1 if entry.nil?
|
|
382
|
+
entry[:params][idx - 1] = value.to_s
|
|
383
|
+
0
|
|
384
|
+
end
|
|
385
|
+
Pg.define_singleton_method(:fresco_pg_bind_int) do |cid, idx, value|
|
|
386
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
387
|
+
return -1 if cid < 1 || cid > stmts.length
|
|
388
|
+
entry = stmts[cid - 1]
|
|
389
|
+
return -1 if entry.nil?
|
|
390
|
+
entry[:params][idx - 1] = value.to_s
|
|
391
|
+
0
|
|
392
|
+
end
|
|
393
|
+
Pg.define_singleton_method(:fresco_pg_step) do |cid|
|
|
394
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
395
|
+
return -1 if cid < 1 || cid > stmts.length
|
|
396
|
+
entry = stmts[cid - 1]
|
|
397
|
+
return -1 if entry.nil?
|
|
398
|
+
if entry[:result].nil?
|
|
399
|
+
# First step: fire query with accumulated binds. Replace nils
|
|
400
|
+
# with empty strings so libpq's text coercion has something
|
|
401
|
+
# to chew on.
|
|
402
|
+
values = entry[:params].map { |v| v.nil? ? "" : v }
|
|
403
|
+
begin
|
|
404
|
+
entry[:result] = entry[:conn].exec_params(entry[:sql], values)
|
|
405
|
+
entry[:ntuples] = entry[:result].ntuples
|
|
406
|
+
entry[:row] = -1
|
|
407
|
+
rescue ::PG::Error
|
|
408
|
+
entry[:result] = nil
|
|
409
|
+
return -1
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
entry[:row] += 1
|
|
413
|
+
entry[:row] < entry[:ntuples] ? 1 : 0
|
|
414
|
+
end
|
|
415
|
+
Pg.define_singleton_method(:fresco_pg_col_str) do |cid, idx|
|
|
416
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
417
|
+
return "" if cid < 1 || cid > stmts.length
|
|
418
|
+
entry = stmts[cid - 1]
|
|
419
|
+
return "" if entry.nil? || entry[:result].nil?
|
|
420
|
+
return "" if entry[:row] < 0 || entry[:row] >= entry[:ntuples]
|
|
421
|
+
v = entry[:result].getvalue(entry[:row], idx)
|
|
422
|
+
v.nil? ? "" : v.to_s
|
|
423
|
+
end
|
|
424
|
+
Pg.define_singleton_method(:fresco_pg_col_int) do |cid, idx|
|
|
425
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
426
|
+
return 0 if cid < 1 || cid > stmts.length
|
|
427
|
+
entry = stmts[cid - 1]
|
|
428
|
+
return 0 if entry.nil? || entry[:result].nil?
|
|
429
|
+
return 0 if entry[:row] < 0 || entry[:row] >= entry[:ntuples]
|
|
430
|
+
v = entry[:result].getvalue(entry[:row], idx)
|
|
431
|
+
v.nil? ? 0 : v.to_i
|
|
432
|
+
end
|
|
433
|
+
Pg.define_singleton_method(:fresco_pg_col_count) do |cid|
|
|
434
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
435
|
+
return 0 if cid < 1 || cid > stmts.length
|
|
436
|
+
entry = stmts[cid - 1]
|
|
437
|
+
return 0 if entry.nil? || entry[:result].nil?
|
|
438
|
+
entry[:result].nfields
|
|
439
|
+
end
|
|
440
|
+
Pg.define_singleton_method(:fresco_pg_finalize) do |cid|
|
|
441
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
442
|
+
return 0 if cid < 1 || cid > stmts.length
|
|
443
|
+
stmts[cid - 1] = nil
|
|
444
|
+
0
|
|
445
|
+
end
|
|
446
|
+
Pg.define_singleton_method(:fresco_pg_reset) do |cid|
|
|
447
|
+
stmts = Sock.instance_variable_get(:@dev_pg_stmts)
|
|
448
|
+
return -1 if cid < 1 || cid > stmts.length
|
|
449
|
+
entry = stmts[cid - 1]
|
|
450
|
+
return -1 if entry.nil?
|
|
451
|
+
entry[:result] = nil
|
|
452
|
+
entry[:row] = -1
|
|
453
|
+
entry[:ntuples] = 0
|
|
454
|
+
0
|
|
455
|
+
end
|
|
456
|
+
Pg.define_singleton_method(:fresco_pg_last_insert_rowid) do |h|
|
|
457
|
+
handles = Sock.instance_variable_get(:@dev_pg_handles)
|
|
458
|
+
return -1 if h < 1 || h > handles.length
|
|
459
|
+
conn = handles[h - 1]
|
|
460
|
+
return -1 if conn.nil?
|
|
461
|
+
begin
|
|
462
|
+
res = conn.exec("SELECT lastval()")
|
|
463
|
+
res.ntuples > 0 ? res.getvalue(0, 0).to_i : -1
|
|
464
|
+
rescue ::PG::Error
|
|
465
|
+
-1
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def snapshot_mtimes
|
|
473
|
+
files = WATCH_GLOBS.flat_map { |g| Dir.glob(g) }.sort
|
|
474
|
+
files.each_with_object({}) { |f, h| h[f] = File.mtime(f).to_f }
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def start_watcher
|
|
478
|
+
last = snapshot_mtimes
|
|
479
|
+
Thread.new do
|
|
480
|
+
Thread.current.abort_on_exception = false
|
|
481
|
+
loop do
|
|
482
|
+
sleep 0.2
|
|
483
|
+
cur = snapshot_mtimes
|
|
484
|
+
next if cur == last
|
|
485
|
+
changed = (cur.keys | last.keys).select { |k| cur[k] != last[k] }
|
|
486
|
+
last = cur
|
|
487
|
+
puts "#{Color.yellow('[reload]')} #{changed.join(', ')}"
|
|
488
|
+
reload! if build!
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# --- Dev HTTP listener ------------------------------------------
|
|
494
|
+
#
|
|
495
|
+
# Single-threaded TCPServer accept loop. Each request: parse the
|
|
496
|
+
# request line + headers, read a Content-Length-bounded body, build a
|
|
497
|
+
# Request, dispatch under RELOAD_LOCK (so file-watcher reloads don't
|
|
498
|
+
# race with in-flight requests), write the response. No keep-alive —
|
|
499
|
+
# every dev response carries `Connection: close`. Keep-alive lives in
|
|
500
|
+
# the prefork worker on the prod path.
|
|
501
|
+
|
|
502
|
+
def parse_dev_request(sock)
|
|
503
|
+
request_line = sock.gets("\r\n")
|
|
504
|
+
return nil if request_line.nil?
|
|
505
|
+
method, raw_path, _version = request_line.strip.split(" ", 3)
|
|
506
|
+
return nil if method.nil? || raw_path.nil?
|
|
507
|
+
|
|
508
|
+
qmark = raw_path.index("?")
|
|
509
|
+
if qmark
|
|
510
|
+
path = raw_path[0...qmark]
|
|
511
|
+
qstr = raw_path[(qmark + 1)..]
|
|
512
|
+
else
|
|
513
|
+
path = raw_path
|
|
514
|
+
qstr = ""
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
headers = {}
|
|
518
|
+
while (line = sock.gets("\r\n"))
|
|
519
|
+
line = line.chomp
|
|
520
|
+
break if line.empty?
|
|
521
|
+
colon = line.index(":")
|
|
522
|
+
next unless colon
|
|
523
|
+
name = line[0...colon].downcase
|
|
524
|
+
value = line[(colon + 1)..].lstrip
|
|
525
|
+
headers[name] = value
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
body = ""
|
|
529
|
+
cl = headers["content-length"]
|
|
530
|
+
if cl && cl.to_i > 0
|
|
531
|
+
body = sock.read(cl.to_i) || ""
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
req = Request.new(method, path, body)
|
|
535
|
+
parse_query_string!(qstr, req.params) if qstr.length > 0
|
|
536
|
+
headers.each { |k, v| req.headers[k] = v }
|
|
537
|
+
ctype = headers["content-type"]
|
|
538
|
+
if ctype && ctype == "application/x-www-form-urlencoded"
|
|
539
|
+
parse_form_body!(body, req.params)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Mirror runtime's `_method` override (see runtime/runtime.rb).
|
|
543
|
+
if req.verb == "POST"
|
|
544
|
+
override = req.params.str(:_method)
|
|
545
|
+
req.verb = override.upcase if override.length > 0
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Parse Cookie header into req.cookies. We can't reuse runtime's
|
|
549
|
+
# `parse_cookie_header!` because it relies on Spinel's
|
|
550
|
+
# `String#index`-returns-`-1` semantics (CRuby returns nil). The
|
|
551
|
+
# idiomatic split-and-loop below is equivalent.
|
|
552
|
+
cookie_hdr = headers["cookie"]
|
|
553
|
+
if cookie_hdr && !cookie_hdr.empty?
|
|
554
|
+
cookie_hdr.split(";").each do |pair|
|
|
555
|
+
pair = pair.strip
|
|
556
|
+
next if pair.empty?
|
|
557
|
+
eq = pair.index("=")
|
|
558
|
+
if eq.nil?
|
|
559
|
+
req.cookies[pair] = ""
|
|
560
|
+
else
|
|
561
|
+
req.cookies[pair[0...eq]] = url_decode(pair[(eq + 1)..])
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
maybe_load_session!(req)
|
|
566
|
+
|
|
567
|
+
req
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Write an HTTP/1.1 response. Body responses go out as one write;
|
|
571
|
+
# file responses (file_path set on the Response) are loaded from
|
|
572
|
+
# disk and written inline — production uses sendfile(2), but dev
|
|
573
|
+
# is correctness-over-throughput.
|
|
574
|
+
def write_dev_response(sock, res)
|
|
575
|
+
if res.file_path.length > 0
|
|
576
|
+
if File.exist?(res.file_path)
|
|
577
|
+
status = res.status
|
|
578
|
+
ctype = res.content_type
|
|
579
|
+
body = File.binread(res.file_path)
|
|
580
|
+
else
|
|
581
|
+
status = 404
|
|
582
|
+
ctype = "text/plain; charset=utf-8"
|
|
583
|
+
body = "Not Found"
|
|
584
|
+
end
|
|
585
|
+
else
|
|
586
|
+
status = res.status
|
|
587
|
+
ctype = res.content_type
|
|
588
|
+
body = res.body
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
reason = reason_for(status)
|
|
592
|
+
sock.write("HTTP/1.1 #{status} #{reason}\r\n")
|
|
593
|
+
sock.write("Content-Type: #{ctype}\r\n")
|
|
594
|
+
sock.write("Content-Length: #{body.bytesize}\r\n")
|
|
595
|
+
sock.write("Location: #{res.location}\r\n") if res.location.length > 0
|
|
596
|
+
res.set_cookies.each { |line| sock.write("Set-Cookie: #{line}\r\n") }
|
|
597
|
+
sock.write("Connection: close\r\n\r\n")
|
|
598
|
+
sock.write(body)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def serve_dev(port)
|
|
602
|
+
server = TCPServer.new("0.0.0.0", port)
|
|
603
|
+
puts "#{Color.cyan('[dev]')} listening on http://0.0.0.0:#{port}"
|
|
604
|
+
loop do
|
|
605
|
+
client = server.accept
|
|
606
|
+
handle_dev_connection(client)
|
|
607
|
+
end
|
|
608
|
+
rescue Interrupt
|
|
609
|
+
puts "\n#{Color.cyan('[dev]')} stopping"
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Dev-only crash page. Renders the exception class, message, and
|
|
613
|
+
# backtrace inline so /crash (and any other action raise) shows up
|
|
614
|
+
# in the browser instead of being a static "Internal Server Error"
|
|
615
|
+
# page. NEVER call this in production — leaks paths, gem versions,
|
|
616
|
+
# and whatever string the exception happened to carry.
|
|
617
|
+
def render_dev_error(err, req)
|
|
618
|
+
esc = ->(s) { Spinel::View.h(s.to_s) }
|
|
619
|
+
bt = (err.backtrace || []).map { |line| "<li>#{esc.call(line)}</li>" }.join
|
|
620
|
+
|
|
621
|
+
method = req ? req.verb : "?"
|
|
622
|
+
path = req ? req.path : "?"
|
|
623
|
+
params = req ? format_hash(req.params.raw) : "(unavailable)"
|
|
624
|
+
query = req ? format_hash(req.query) : "(unavailable)"
|
|
625
|
+
headers = req ? format_hash(req.headers) : "(unavailable)"
|
|
626
|
+
|
|
627
|
+
body = <<~HTML
|
|
628
|
+
<!doctype html>
|
|
629
|
+
<html>
|
|
630
|
+
<head>
|
|
631
|
+
<meta charset="utf-8">
|
|
632
|
+
<title>500 — #{esc.call(err.class.name)}</title>
|
|
633
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
634
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
635
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
636
|
+
<script>
|
|
637
|
+
(function () {
|
|
638
|
+
var t = localStorage.getItem("theme");
|
|
639
|
+
if (!t) {
|
|
640
|
+
t = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light";
|
|
641
|
+
localStorage.setItem("theme", t);
|
|
642
|
+
}
|
|
643
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
644
|
+
})();
|
|
645
|
+
</script>
|
|
646
|
+
<style>
|
|
647
|
+
:root {
|
|
648
|
+
--bg: #fff; --fg: #222; --muted: #888;
|
|
649
|
+
--border: #eee; --code-bg: #f7f7f7;
|
|
650
|
+
--accent: #0066cc; --status: #b00020;
|
|
651
|
+
}
|
|
652
|
+
:root[data-theme="dark"] {
|
|
653
|
+
--bg: #141414; --fg: #e8e8e8; --muted: #9a9a9a;
|
|
654
|
+
--border: #2a2a2a; --code-bg: #1f1f1f;
|
|
655
|
+
--accent: #6ab0ff; --status: #ff6680;
|
|
656
|
+
}
|
|
657
|
+
body {
|
|
658
|
+
font-family: 'IBM Plex Mono', ui-monospace, monospace;
|
|
659
|
+
max-width: 60rem; margin: 2rem auto; padding: 0 1.5rem;
|
|
660
|
+
background: var(--bg); color: var(--fg);
|
|
661
|
+
transition: background 0.15s, color 0.15s;
|
|
662
|
+
}
|
|
663
|
+
.status { color: var(--status); font-size: 0.8rem; letter-spacing: 0.05em; text-transform: uppercase; }
|
|
664
|
+
h1 { font-size: 1.25rem; font-weight: 700; margin: 0.25rem 0 0; word-break: break-word; }
|
|
665
|
+
.msg { color: var(--muted); margin: 0.5rem 0 1.5rem; white-space: pre-wrap; }
|
|
666
|
+
h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-top: 2rem; }
|
|
667
|
+
ol.bt { background: var(--code-bg); border: 1px solid var(--border); border-radius: 4px; padding: 1rem 1rem 1rem 3rem; font-size: 0.85rem; line-height: 1.5; color: var(--fg); }
|
|
668
|
+
ol.bt li { word-break: break-all; }
|
|
669
|
+
dl { display: grid; grid-template-columns: 6rem 1fr; gap: 0.4rem 1rem; font-size: 0.85rem; }
|
|
670
|
+
dt { color: var(--muted); }
|
|
671
|
+
dd { margin: 0; word-break: break-word; }
|
|
672
|
+
footer { color: var(--muted); font-size: 0.8rem; margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1rem; }
|
|
673
|
+
#theme-toggle {
|
|
674
|
+
position: fixed; top: 1rem; right: 1rem;
|
|
675
|
+
background: transparent; border: 1px solid var(--border);
|
|
676
|
+
color: var(--fg); border-radius: 6px;
|
|
677
|
+
padding: 0.4rem; cursor: pointer;
|
|
678
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
679
|
+
}
|
|
680
|
+
#theme-toggle:hover { background: var(--code-bg); }
|
|
681
|
+
#theme-toggle svg { width: 18px; height: 18px; display: none; }
|
|
682
|
+
:root[data-theme="light"] #theme-toggle .icon-moon { display: inline-block; }
|
|
683
|
+
:root[data-theme="dark"] #theme-toggle .icon-sun { display: inline-block; }
|
|
684
|
+
</style>
|
|
685
|
+
</head>
|
|
686
|
+
<body>
|
|
687
|
+
<button id="theme-toggle" aria-label="Toggle color theme" title="Toggle color theme">
|
|
688
|
+
<svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
|
|
689
|
+
<svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
690
|
+
</button>
|
|
691
|
+
|
|
692
|
+
<div class="status">500 · fresco dev exception</div>
|
|
693
|
+
<h1>#{esc.call(err.class.name)}</h1>
|
|
694
|
+
<p class="msg">#{esc.call(err.message)}</p>
|
|
695
|
+
|
|
696
|
+
<h2>Backtrace</h2>
|
|
697
|
+
<ol class="bt">#{bt}</ol>
|
|
698
|
+
|
|
699
|
+
<h2>Request</h2>
|
|
700
|
+
<dl>
|
|
701
|
+
<dt>Method</dt><dd>#{esc.call(method)}</dd>
|
|
702
|
+
<dt>Path</dt><dd>#{esc.call(path)}</dd>
|
|
703
|
+
<dt>Params</dt><dd>#{esc.call(params)}</dd>
|
|
704
|
+
<dt>Query</dt><dd>#{esc.call(query)}</dd>
|
|
705
|
+
<dt>Headers</dt><dd>#{esc.call(headers)}</dd>
|
|
706
|
+
</dl>
|
|
707
|
+
|
|
708
|
+
<footer>Rendered by `fresco dev`. Production serves public/500.html with no error details.</footer>
|
|
709
|
+
|
|
710
|
+
<script>
|
|
711
|
+
(function () {
|
|
712
|
+
document.getElementById("theme-toggle").addEventListener("click", function () {
|
|
713
|
+
var cur = document.documentElement.getAttribute("data-theme");
|
|
714
|
+
var nxt = cur === "dark" ? "light" : "dark";
|
|
715
|
+
document.documentElement.setAttribute("data-theme", nxt);
|
|
716
|
+
localStorage.setItem("theme", nxt);
|
|
717
|
+
});
|
|
718
|
+
})();
|
|
719
|
+
</script>
|
|
720
|
+
</body>
|
|
721
|
+
</html>
|
|
722
|
+
HTML
|
|
723
|
+
|
|
724
|
+
Response.html(500, body)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def handle_dev_connection(client)
|
|
728
|
+
req = parse_dev_request(client)
|
|
729
|
+
if req.nil?
|
|
730
|
+
write_dev_response(client, error_response(400, "Bad Request\n"))
|
|
731
|
+
puts "#{Color.dim('[request]')} ? ? #{color_status(400)}"
|
|
732
|
+
return
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# Mirror runtime.rb's request bracket so the SQL-log buffer flushes
|
|
736
|
+
# under the [request] line. The Fresco::Db no-op stub is loaded even
|
|
737
|
+
# when no adapter is configured, so this call is safe regardless.
|
|
738
|
+
Fresco::Db.begin_request!
|
|
739
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
740
|
+
res = RELOAD_LOCK.synchronize { dispatch_request(req.verb, req.path, req) }
|
|
741
|
+
attach_session_cookie!(req, res)
|
|
742
|
+
micros = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1_000_000).to_i
|
|
743
|
+
write_dev_response(client, res)
|
|
744
|
+
log_request_full(req, res.status, micros)
|
|
745
|
+
Fresco::Db.flush_log!
|
|
746
|
+
rescue => e
|
|
747
|
+
puts "#{Color.red('[handler]')} #{e.class}: #{e.message}"
|
|
748
|
+
(e.backtrace || []).first(5).each { |b| puts Color.dim(" #{b}") }
|
|
749
|
+
Fresco::Db.flush_log!
|
|
750
|
+
begin
|
|
751
|
+
write_dev_response(client, render_dev_error(e, req))
|
|
752
|
+
rescue StandardError
|
|
753
|
+
# client already gone — nothing to do
|
|
754
|
+
end
|
|
755
|
+
ensure
|
|
756
|
+
client.close rescue nil
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# --- main --------------------------------------------------------
|
|
760
|
+
|
|
761
|
+
# `fresco dev db:*` short-circuits the watcher+server and just runs
|
|
762
|
+
# the requested subcommand against the dev-mode CRuby load. Useful for
|
|
763
|
+
# applying migrations from your editor without a release build, and
|
|
764
|
+
# for `db:console` — drops into IRB with every generated model
|
|
765
|
+
# loaded and the FFI stand-ins wired so model methods actually hit
|
|
766
|
+
# the DB.
|
|
767
|
+
if ARGV[0] && ARGV[0].start_with?("db:")
|
|
768
|
+
build! || abort("[dev] build failed; fix and retry")
|
|
769
|
+
reload!
|
|
770
|
+
cmd = ARGV[0]
|
|
771
|
+
case cmd
|
|
772
|
+
when "db:migrate"
|
|
773
|
+
Fresco::DbMigrations.migrate!
|
|
774
|
+
when "db:rollback"
|
|
775
|
+
Fresco::DbMigrations.rollback!
|
|
776
|
+
when "db:reset"
|
|
777
|
+
# Roll back to a clean slate, then re-apply. Loops because
|
|
778
|
+
# rollback! peels one migration per call. For a small migration
|
|
779
|
+
# set this is fine; M6+ could add a single-call "rollback all".
|
|
780
|
+
loop_count = 0
|
|
781
|
+
while loop_count < Fresco.migrations.length + 1
|
|
782
|
+
Fresco::DbMigrations.rollback!
|
|
783
|
+
loop_count += 1
|
|
784
|
+
end
|
|
785
|
+
Fresco::DbMigrations.migrate!
|
|
786
|
+
when "db:console"
|
|
787
|
+
require "irb"
|
|
788
|
+
puts "[dev] Fresco IRB console — models and Fresco::Db.* are loaded."
|
|
789
|
+
puts "[dev] adapter=#{Fresco.database_adapter.inspect} dsn=#{Fresco.database_url.empty? ? Fresco.database_path : Fresco.database_url}"
|
|
790
|
+
# IRB.start scans ARGV for a script path; left as-is it tries to
|
|
791
|
+
# open `db:console` as a file. Clear ARGV so IRB drops into a
|
|
792
|
+
# bare interactive session.
|
|
793
|
+
ARGV.clear
|
|
794
|
+
IRB.start
|
|
795
|
+
else
|
|
796
|
+
abort "[dev] unknown subcommand: #{cmd} (try db:migrate, db:rollback, db:reset, db:console)"
|
|
797
|
+
end
|
|
798
|
+
exit 0
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
port = 3030
|
|
802
|
+
i = 0
|
|
803
|
+
while i < ARGV.length
|
|
804
|
+
if ARGV[i] == "-p" && ARGV[i + 1]
|
|
805
|
+
port = ARGV[i + 1].to_i
|
|
806
|
+
i += 2
|
|
807
|
+
else
|
|
808
|
+
i += 1
|
|
809
|
+
end
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
build! || abort("[dev] initial build failed; fix and retry")
|
|
813
|
+
reload!
|
|
814
|
+
start_watcher
|
|
815
|
+
serve_dev(port)
|