tep 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/* tep_sqlite.c - thin libsqlite3 wrapper for spinel-AOT'd tep apps.
|
|
2
|
+
*
|
|
3
|
+
* Pattern mirrors sphttp.c: spinel can't load CRuby C extensions
|
|
4
|
+
* (the `sqlite3` gem ships native code via Ruby's MRI ABI), so we
|
|
5
|
+
* write a small C shim with stable, str/int-typed entry points and
|
|
6
|
+
* expose them via spinel's `ffi_func` DSL.
|
|
7
|
+
*
|
|
8
|
+
* Scope of v1
|
|
9
|
+
* -----------
|
|
10
|
+
* - `open(path)` / `close(h)` -- multiple DB
|
|
11
|
+
* handles (up to TEP_SQLITE_MAX_HANDLES) tracked in a static
|
|
12
|
+
* slot table so handles fit in spinel's :int FFI return.
|
|
13
|
+
* - `exec(h, sql)` -- run a
|
|
14
|
+
* statement that returns no rows (CREATE / INSERT / UPDATE /
|
|
15
|
+
* DELETE / PRAGMA). No bind in this form; for parameterised
|
|
16
|
+
* writes use prepare + bind + step + finalize.
|
|
17
|
+
* - `prepare(h, sql)` / `bind_{str,int}(idx, v)` / `step()` /
|
|
18
|
+
* `col_{str,int}(idx)` / `col_count()` / `finalize()`
|
|
19
|
+
* -- a single
|
|
20
|
+
* global cursor (one in-flight statement per process). Spinel
|
|
21
|
+
* workers are single-threaded, and the framework never holds
|
|
22
|
+
* two cursors at once -- handlers run serially per worker.
|
|
23
|
+
* - `last_insert_rowid(h)` -- for INSERT
|
|
24
|
+
* ... RETURNING-less workflows.
|
|
25
|
+
*
|
|
26
|
+
* Out of scope (v1)
|
|
27
|
+
* -----------------
|
|
28
|
+
* - Multi-cursor: nested queries (open one cursor, iterate, run
|
|
29
|
+
* another query inside). Document and revisit.
|
|
30
|
+
* - Blob / NULL columns: col_str returns "" for NULL, callers
|
|
31
|
+
* can't distinguish empty-string from NULL.
|
|
32
|
+
* - Transactions: works via `exec("BEGIN")` / `exec("COMMIT")`.
|
|
33
|
+
*
|
|
34
|
+
* Errors
|
|
35
|
+
* ------
|
|
36
|
+
* Most functions return 0 on success and -1 on error. Detailed
|
|
37
|
+
* errmsg surfacing isn't wired through (spinel's :str return
|
|
38
|
+
* lifetime would make it awkward); the C shim could expose
|
|
39
|
+
* `tep_sqlite_errmsg()` as a static-buffer copy if needed.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
#include <stdlib.h>
|
|
43
|
+
#include <string.h>
|
|
44
|
+
#include <sqlite3.h>
|
|
45
|
+
|
|
46
|
+
#define TEP_SQLITE_MAX_HANDLES 16
|
|
47
|
+
#define TEP_SQLITE_COL_BUFSIZE 65536
|
|
48
|
+
#define TEP_SQLITE_COL_BUF_SLOTS 16
|
|
49
|
+
#define TEP_SQLITE_CACHE_SLOTS 64 /* prepare-statement cache */
|
|
50
|
+
#define TEP_SQLITE_CACHE_SQL_MAX 512 /* longest cached SQL */
|
|
51
|
+
|
|
52
|
+
static sqlite3 *tep_sqlite_handles[TEP_SQLITE_MAX_HANDLES] = {0};
|
|
53
|
+
static sqlite3_stmt *tep_sqlite_stmt = NULL;
|
|
54
|
+
/* When the current cursor came from the prepare-statement cache, we
|
|
55
|
+
* must NOT sqlite3_finalize it on tep_sqlite_finalize -- the slot
|
|
56
|
+
* stays alive for reuse, and finalize becomes reset+clear_bindings.
|
|
57
|
+
* Same applies to tep_sqlite_prepare's stmt-swap path. */
|
|
58
|
+
static int tep_sqlite_stmt_cached = 0;
|
|
59
|
+
|
|
60
|
+
/* prepare-statement cache (chunk per #75). Linear scan by (h, sql);
|
|
61
|
+
* n is small so O(n) is fine. Bounded by TEP_SQLITE_CACHE_SLOTS;
|
|
62
|
+
* if full, prepare_cached falls through to an uncached prepare. */
|
|
63
|
+
typedef struct {
|
|
64
|
+
int h; /* db handle index (1-based), 0 = empty */
|
|
65
|
+
sqlite3_stmt *stmt;
|
|
66
|
+
char sql[TEP_SQLITE_CACHE_SQL_MAX];
|
|
67
|
+
} tep_sqlite_cache_entry;
|
|
68
|
+
static tep_sqlite_cache_entry tep_sqlite_cache[TEP_SQLITE_CACHE_SLOTS] = {0};
|
|
69
|
+
/* Rotating return buffers for col_str. spinel's `:str` return type
|
|
70
|
+
* wants a pointer that stays valid until "the caller is done with
|
|
71
|
+
* it", but in practice callers stash multiple col_str results into
|
|
72
|
+
* variables / hashes / array entries before the buffer would
|
|
73
|
+
* otherwise rotate. A single static buf would alias all those
|
|
74
|
+
* entries to whatever the most recent call wrote.
|
|
75
|
+
*
|
|
76
|
+
* We rotate across SLOTS buffers; each call lands in the next
|
|
77
|
+
* slot. With 16 slots a typical handler doing
|
|
78
|
+
*
|
|
79
|
+
* a = first_str(...); b = first_str(...); c = first_str(...)
|
|
80
|
+
*
|
|
81
|
+
* sees three independent strings.
|
|
82
|
+
*
|
|
83
|
+
* The aliasing window only collapses for callers who hold > 16
|
|
84
|
+
* live col_str references concurrently -- e.g. iterating a query
|
|
85
|
+
* and pushing every row into an array without copying. Document
|
|
86
|
+
* that lifetime in `Tep::SQLite#col_str`. */
|
|
87
|
+
static char tep_sqlite_col_buf[TEP_SQLITE_COL_BUF_SLOTS][TEP_SQLITE_COL_BUFSIZE];
|
|
88
|
+
static int tep_sqlite_col_slot = 0;
|
|
89
|
+
|
|
90
|
+
int tep_sqlite_open(const char *path) {
|
|
91
|
+
int i;
|
|
92
|
+
for (i = 0; i < TEP_SQLITE_MAX_HANDLES; i++) {
|
|
93
|
+
if (tep_sqlite_handles[i] == NULL) {
|
|
94
|
+
sqlite3 *db = NULL;
|
|
95
|
+
if (sqlite3_open(path, &db) != SQLITE_OK) {
|
|
96
|
+
if (db) sqlite3_close(db);
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
tep_sqlite_handles[i] = db;
|
|
100
|
+
return i + 1; /* 1-indexed: 0 reserved for "uninitialised" */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return -1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
int tep_sqlite_close(int h) {
|
|
107
|
+
if (h < 1 || h > TEP_SQLITE_MAX_HANDLES) return -1;
|
|
108
|
+
sqlite3 *db = tep_sqlite_handles[h - 1];
|
|
109
|
+
if (db == NULL) return -1;
|
|
110
|
+
/* Finalize any cursor that was on this handle to avoid
|
|
111
|
+
* sqlite_close returning SQLITE_BUSY. */
|
|
112
|
+
if (tep_sqlite_stmt && sqlite3_db_handle(tep_sqlite_stmt) == db) {
|
|
113
|
+
if (!tep_sqlite_stmt_cached) {
|
|
114
|
+
sqlite3_finalize(tep_sqlite_stmt);
|
|
115
|
+
}
|
|
116
|
+
/* Cached stmts get finalized below by the cache walk. */
|
|
117
|
+
tep_sqlite_stmt = NULL;
|
|
118
|
+
tep_sqlite_stmt_cached = 0;
|
|
119
|
+
}
|
|
120
|
+
/* Finalize every cached statement that belongs to this handle
|
|
121
|
+
* before closing the db. Cache slots are owner-keyed by `h`. */
|
|
122
|
+
int i;
|
|
123
|
+
for (i = 0; i < TEP_SQLITE_CACHE_SLOTS; i++) {
|
|
124
|
+
if (tep_sqlite_cache[i].h == h && tep_sqlite_cache[i].stmt) {
|
|
125
|
+
sqlite3_finalize(tep_sqlite_cache[i].stmt);
|
|
126
|
+
tep_sqlite_cache[i].stmt = NULL;
|
|
127
|
+
tep_sqlite_cache[i].h = 0;
|
|
128
|
+
tep_sqlite_cache[i].sql[0] = '\0';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
sqlite3_close(db);
|
|
132
|
+
tep_sqlite_handles[h - 1] = NULL;
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
int tep_sqlite_exec(int h, const char *sql) {
|
|
137
|
+
if (h < 1 || h > TEP_SQLITE_MAX_HANDLES) return -1;
|
|
138
|
+
sqlite3 *db = tep_sqlite_handles[h - 1];
|
|
139
|
+
if (db == NULL) return -1;
|
|
140
|
+
char *err = NULL;
|
|
141
|
+
int rc = sqlite3_exec(db, sql, NULL, NULL, &err);
|
|
142
|
+
if (err) sqlite3_free(err);
|
|
143
|
+
return rc == SQLITE_OK ? 0 : -1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Drop or reset whatever's currently on the singleton cursor before
|
|
147
|
+
* we install a new one. Cached stmts get reset+clear_bindings (so
|
|
148
|
+
* the cached slot stays valid); uncached stmts get finalized. */
|
|
149
|
+
static void tep_sqlite_release_current(void) {
|
|
150
|
+
if (!tep_sqlite_stmt) return;
|
|
151
|
+
if (tep_sqlite_stmt_cached) {
|
|
152
|
+
sqlite3_reset(tep_sqlite_stmt);
|
|
153
|
+
sqlite3_clear_bindings(tep_sqlite_stmt);
|
|
154
|
+
} else {
|
|
155
|
+
sqlite3_finalize(tep_sqlite_stmt);
|
|
156
|
+
}
|
|
157
|
+
tep_sqlite_stmt = NULL;
|
|
158
|
+
tep_sqlite_stmt_cached = 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
int tep_sqlite_prepare(int h, const char *sql) {
|
|
162
|
+
if (h < 1 || h > TEP_SQLITE_MAX_HANDLES) return -1;
|
|
163
|
+
sqlite3 *db = tep_sqlite_handles[h - 1];
|
|
164
|
+
if (db == NULL) return -1;
|
|
165
|
+
tep_sqlite_release_current();
|
|
166
|
+
if (sqlite3_prepare_v2(db, sql, -1, &tep_sqlite_stmt, NULL) != SQLITE_OK) {
|
|
167
|
+
tep_sqlite_stmt = NULL;
|
|
168
|
+
return -1;
|
|
169
|
+
}
|
|
170
|
+
/* uncached */
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Cached variant: looks up `sql` for db handle `h` in the cache;
|
|
175
|
+
* on hit reuses the prepared statement (with reset + clear_bindings);
|
|
176
|
+
* on miss prepares + stashes in the first free slot; if the cache is
|
|
177
|
+
* full, falls back to an uncached prepare so the caller still works.
|
|
178
|
+
*
|
|
179
|
+
* SQL is matched literally (no normalization). Apps that generate
|
|
180
|
+
* SQL with varying whitespace miss the cache; format consistently. */
|
|
181
|
+
int tep_sqlite_prepare_cached(int h, const char *sql) {
|
|
182
|
+
if (h < 1 || h > TEP_SQLITE_MAX_HANDLES) return -1;
|
|
183
|
+
sqlite3 *db = tep_sqlite_handles[h - 1];
|
|
184
|
+
if (db == NULL) return -1;
|
|
185
|
+
/* SQL longer than the cache's per-slot buffer: just do an
|
|
186
|
+
* uncached prepare. The caller still gets correct behavior. */
|
|
187
|
+
size_t sql_len = strlen(sql);
|
|
188
|
+
if (sql_len >= TEP_SQLITE_CACHE_SQL_MAX) {
|
|
189
|
+
return tep_sqlite_prepare(h, sql);
|
|
190
|
+
}
|
|
191
|
+
tep_sqlite_release_current();
|
|
192
|
+
/* Cache lookup. */
|
|
193
|
+
int i;
|
|
194
|
+
for (i = 0; i < TEP_SQLITE_CACHE_SLOTS; i++) {
|
|
195
|
+
if (tep_sqlite_cache[i].h == h &&
|
|
196
|
+
tep_sqlite_cache[i].stmt &&
|
|
197
|
+
strcmp(tep_sqlite_cache[i].sql, sql) == 0) {
|
|
198
|
+
tep_sqlite_stmt = tep_sqlite_cache[i].stmt;
|
|
199
|
+
sqlite3_reset(tep_sqlite_stmt);
|
|
200
|
+
sqlite3_clear_bindings(tep_sqlite_stmt);
|
|
201
|
+
tep_sqlite_stmt_cached = 1;
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/* Cache miss -- find an empty slot. */
|
|
206
|
+
int empty = -1;
|
|
207
|
+
for (i = 0; i < TEP_SQLITE_CACHE_SLOTS; i++) {
|
|
208
|
+
if (tep_sqlite_cache[i].h == 0) { empty = i; break; }
|
|
209
|
+
}
|
|
210
|
+
if (empty < 0) {
|
|
211
|
+
/* Cache full: prepare uncached so the caller works. The
|
|
212
|
+
* existing tep_sqlite_finalize path will sqlite3_finalize
|
|
213
|
+
* this one as today. */
|
|
214
|
+
if (sqlite3_prepare_v2(db, sql, -1, &tep_sqlite_stmt, NULL) != SQLITE_OK) {
|
|
215
|
+
tep_sqlite_stmt = NULL;
|
|
216
|
+
return -1;
|
|
217
|
+
}
|
|
218
|
+
tep_sqlite_stmt_cached = 0;
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
/* Prepare + stash. */
|
|
222
|
+
sqlite3_stmt *new_stmt = NULL;
|
|
223
|
+
if (sqlite3_prepare_v2(db, sql, -1, &new_stmt, NULL) != SQLITE_OK) {
|
|
224
|
+
tep_sqlite_stmt = NULL;
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
tep_sqlite_cache[empty].h = h;
|
|
228
|
+
tep_sqlite_cache[empty].stmt = new_stmt;
|
|
229
|
+
memcpy(tep_sqlite_cache[empty].sql, sql, sql_len);
|
|
230
|
+
tep_sqlite_cache[empty].sql[sql_len] = '\0';
|
|
231
|
+
tep_sqlite_stmt = new_stmt;
|
|
232
|
+
tep_sqlite_stmt_cached = 1;
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
int tep_sqlite_bind_str(int idx, const char *value) {
|
|
237
|
+
if (!tep_sqlite_stmt) return -1;
|
|
238
|
+
/* SQLITE_TRANSIENT -> sqlite copies the string before returning,
|
|
239
|
+
* so the caller's pointer doesn't need to outlive the bind. */
|
|
240
|
+
return sqlite3_bind_text(tep_sqlite_stmt, idx, value, -1, SQLITE_TRANSIENT)
|
|
241
|
+
== SQLITE_OK ? 0 : -1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* 64-bit bind. `long` is the FFI `:long` type (64-bit on the LP64
|
|
245
|
+
* targets Spinel compiles for); routed through sqlite3_bind_int64 so a
|
|
246
|
+
* value > 2^31 isn't truncated on the way in (issue #171). */
|
|
247
|
+
int tep_sqlite_bind_int(int idx, long value) {
|
|
248
|
+
if (!tep_sqlite_stmt) return -1;
|
|
249
|
+
return sqlite3_bind_int64(tep_sqlite_stmt, idx, (sqlite3_int64)value) == SQLITE_OK ? 0 : -1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* 1 -> row available, 0 -> done (no more rows), -1 -> error */
|
|
253
|
+
int tep_sqlite_step(void) {
|
|
254
|
+
if (!tep_sqlite_stmt) return -1;
|
|
255
|
+
int rc = sqlite3_step(tep_sqlite_stmt);
|
|
256
|
+
if (rc == SQLITE_ROW) return 1;
|
|
257
|
+
if (rc == SQLITE_DONE) return 0;
|
|
258
|
+
return -1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const char *tep_sqlite_col_str(int idx) {
|
|
262
|
+
if (!tep_sqlite_stmt) return "";
|
|
263
|
+
const unsigned char *t = sqlite3_column_text(tep_sqlite_stmt, idx);
|
|
264
|
+
if (!t) return "";
|
|
265
|
+
size_t n = strlen((const char *)t);
|
|
266
|
+
if (n >= TEP_SQLITE_COL_BUFSIZE) n = TEP_SQLITE_COL_BUFSIZE - 1;
|
|
267
|
+
char *buf = tep_sqlite_col_buf[tep_sqlite_col_slot];
|
|
268
|
+
tep_sqlite_col_slot = (tep_sqlite_col_slot + 1) % TEP_SQLITE_COL_BUF_SLOTS;
|
|
269
|
+
memcpy(buf, t, n);
|
|
270
|
+
buf[n] = '\0';
|
|
271
|
+
return buf;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* 64-bit column read. sqlite3_column_int (32-bit) silently wrapped a
|
|
275
|
+
* value > 2^31 negative -- e.g. a 3.4e9 download count read back as
|
|
276
|
+
* -867422609 (issue #171). sqlite3_column_int64 + a `long` (FFI
|
|
277
|
+
* `:long`, 64-bit) return path preserves the full range; mrb_int is
|
|
278
|
+
* pointer-width so the Ruby side holds it losslessly. */
|
|
279
|
+
long tep_sqlite_col_int(int idx) {
|
|
280
|
+
if (!tep_sqlite_stmt) return 0;
|
|
281
|
+
return (long)sqlite3_column_int64(tep_sqlite_stmt, idx);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
int tep_sqlite_col_count(void) {
|
|
285
|
+
if (!tep_sqlite_stmt) return 0;
|
|
286
|
+
return sqlite3_column_count(tep_sqlite_stmt);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Finalize semantics depend on whether the current stmt came from
|
|
290
|
+
* the prepare-statement cache. Cached stmts get reset + clear_bindings
|
|
291
|
+
* and stay alive in their slot (the whole point of the cache); only
|
|
292
|
+
* uncached stmts actually sqlite3_finalize. The Ruby-side API
|
|
293
|
+
* (`db.finalize`) is unchanged either way. */
|
|
294
|
+
int tep_sqlite_finalize(void) {
|
|
295
|
+
if (!tep_sqlite_stmt) return 0;
|
|
296
|
+
if (tep_sqlite_stmt_cached) {
|
|
297
|
+
sqlite3_reset(tep_sqlite_stmt);
|
|
298
|
+
sqlite3_clear_bindings(tep_sqlite_stmt);
|
|
299
|
+
} else {
|
|
300
|
+
sqlite3_finalize(tep_sqlite_stmt);
|
|
301
|
+
}
|
|
302
|
+
tep_sqlite_stmt = NULL;
|
|
303
|
+
tep_sqlite_stmt_cached = 0;
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
int tep_sqlite_last_insert_rowid(int h) {
|
|
308
|
+
if (h < 1 || h > TEP_SQLITE_MAX_HANDLES) return -1;
|
|
309
|
+
sqlite3 *db = tep_sqlite_handles[h - 1];
|
|
310
|
+
if (db == NULL) return -1;
|
|
311
|
+
return (int)sqlite3_last_insert_rowid(db);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* Reset the current statement so it can be re-stepped (e.g. inside
|
|
315
|
+
* a loop where bound params change between iterations). Returns 0
|
|
316
|
+
* on success. */
|
|
317
|
+
int tep_sqlite_reset(void) {
|
|
318
|
+
if (!tep_sqlite_stmt) return -1;
|
|
319
|
+
return sqlite3_reset(tep_sqlite_stmt) == SQLITE_OK ? 0 : -1;
|
|
320
|
+
}
|
data/lib/tep/url.rb
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Percent-decoding + form-urlencoded query parser.
|
|
2
|
+
module Tep
|
|
3
|
+
class Url
|
|
4
|
+
# "%41+b" -> "A b"
|
|
5
|
+
def self.unescape(s)
|
|
6
|
+
out = ""
|
|
7
|
+
i = 0
|
|
8
|
+
n = s.length
|
|
9
|
+
while i < n
|
|
10
|
+
c = s[i]
|
|
11
|
+
if c == "+"
|
|
12
|
+
out = out + " "
|
|
13
|
+
i += 1
|
|
14
|
+
elsif c == "%" && i + 2 < n
|
|
15
|
+
hi = Url.hex_nibble(s[i + 1])
|
|
16
|
+
lo = Url.hex_nibble(s[i + 2])
|
|
17
|
+
if hi >= 0 && lo >= 0
|
|
18
|
+
out = out + ((hi * 16 + lo).chr)
|
|
19
|
+
i += 3
|
|
20
|
+
else
|
|
21
|
+
out = out + c
|
|
22
|
+
i += 1
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
out = out + c
|
|
26
|
+
i += 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
out
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Percent-encode the bytes that are unsafe in cookie values, query
|
|
33
|
+
# strings, and similar contexts. RFC 3986 unreserved set:
|
|
34
|
+
# ALPHA / DIGIT / `-._~`. Everything else gets `%XX` (uppercase hex).
|
|
35
|
+
def self.escape(s)
|
|
36
|
+
out = ""
|
|
37
|
+
i = 0
|
|
38
|
+
while i < s.length
|
|
39
|
+
c = s[i]
|
|
40
|
+
if (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") ||
|
|
41
|
+
(c >= "0" && c <= "9") || c == "-" || c == "." ||
|
|
42
|
+
c == "_" || c == "~"
|
|
43
|
+
out = out + c
|
|
44
|
+
else
|
|
45
|
+
b = c.getbyte(0)
|
|
46
|
+
hi = b / 16
|
|
47
|
+
lo = b % 16
|
|
48
|
+
out = out + "%" + Url.hex_char(hi) + Url.hex_char(lo)
|
|
49
|
+
end
|
|
50
|
+
i += 1
|
|
51
|
+
end
|
|
52
|
+
out
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.hex_char(n)
|
|
56
|
+
if n < 10
|
|
57
|
+
return ("0".getbyte(0) + n).chr
|
|
58
|
+
end
|
|
59
|
+
("A".getbyte(0) + n - 10).chr
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.hex_nibble(c)
|
|
63
|
+
if c >= "0" && c <= "9"
|
|
64
|
+
return c.getbyte(0) - "0".getbyte(0)
|
|
65
|
+
end
|
|
66
|
+
if c >= "a" && c <= "f"
|
|
67
|
+
return c.getbyte(0) - "a".getbyte(0) + 10
|
|
68
|
+
end
|
|
69
|
+
if c >= "A" && c <= "F"
|
|
70
|
+
return c.getbyte(0) - "A".getbyte(0) + 10
|
|
71
|
+
end
|
|
72
|
+
-1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Split a URL into a Hash with str=>str entries:
|
|
76
|
+
# "scheme" "host" "port" "path" "query"
|
|
77
|
+
#
|
|
78
|
+
# Recognises `http://host[:port]/path?query` and the same shape
|
|
79
|
+
# with `https://`. Without a scheme, the input is treated as a
|
|
80
|
+
# path (host stays empty); useful for routing relative paths
|
|
81
|
+
# through the same parser. Default ports follow the scheme:
|
|
82
|
+
# 80 for http, 443 for https. Path defaults to "/". `query` is
|
|
83
|
+
# the raw substring after `?`, no further decoding.
|
|
84
|
+
#
|
|
85
|
+
# Inlined as one method on purpose: spinel's analyzer widens
|
|
86
|
+
# Hash-typed parameters when a helper mutates them and the
|
|
87
|
+
# caller then keeps reading; sticking to a single body keeps
|
|
88
|
+
# `out` narrowed to StrStrHash throughout.
|
|
89
|
+
def self.split_url(u)
|
|
90
|
+
out = Tep.str_hash
|
|
91
|
+
out["scheme"] = ""
|
|
92
|
+
out["host"] = ""
|
|
93
|
+
out["port"] = ""
|
|
94
|
+
out["path"] = "/"
|
|
95
|
+
out["query"] = ""
|
|
96
|
+
|
|
97
|
+
rest = u
|
|
98
|
+
if rest.length >= 7 && rest[0, 7] == "http://"
|
|
99
|
+
out["scheme"] = "http"
|
|
100
|
+
out["port"] = "80"
|
|
101
|
+
rest = rest[7, rest.length - 7]
|
|
102
|
+
elsif rest.length >= 8 && rest[0, 8] == "https://"
|
|
103
|
+
out["scheme"] = "https"
|
|
104
|
+
out["port"] = "443"
|
|
105
|
+
rest = rest[8, rest.length - 8]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if out["scheme"].length > 0
|
|
109
|
+
slash = Tep.str_find(rest, "/", 0)
|
|
110
|
+
hostport = rest
|
|
111
|
+
tail = "/"
|
|
112
|
+
if slash >= 0
|
|
113
|
+
hostport = rest[0, slash]
|
|
114
|
+
tail = rest[slash, rest.length - slash]
|
|
115
|
+
end
|
|
116
|
+
colon = Tep.str_find(hostport, ":", 0)
|
|
117
|
+
if colon >= 0
|
|
118
|
+
out["host"] = hostport[0, colon]
|
|
119
|
+
out["port"] = hostport[colon + 1, hostport.length - colon - 1]
|
|
120
|
+
else
|
|
121
|
+
out["host"] = hostport
|
|
122
|
+
end
|
|
123
|
+
rest = tail
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
qi = Tep.str_find(rest, "?", 0)
|
|
127
|
+
if qi >= 0
|
|
128
|
+
out["path"] = rest[0, qi]
|
|
129
|
+
out["query"] = rest[qi + 1, rest.length - qi - 1]
|
|
130
|
+
else
|
|
131
|
+
out["path"] = rest
|
|
132
|
+
end
|
|
133
|
+
if out["path"].length == 0
|
|
134
|
+
out["path"] = "/"
|
|
135
|
+
end
|
|
136
|
+
out
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# "a=1&b=2&c" -> Hash {"a"=>"1","b"=>"2","c"=>""}
|
|
140
|
+
def self.parse_query(s)
|
|
141
|
+
h = Tep.str_hash
|
|
142
|
+
if s.length == 0
|
|
143
|
+
return h
|
|
144
|
+
end
|
|
145
|
+
pairs = s.split("&")
|
|
146
|
+
pairs.each do |pair|
|
|
147
|
+
if pair.length > 0
|
|
148
|
+
eq = Tep.str_find(pair, "=", 0)
|
|
149
|
+
if eq < 0
|
|
150
|
+
h[Url.unescape(pair)] = ""
|
|
151
|
+
else
|
|
152
|
+
k = pair[0, eq]
|
|
153
|
+
v = pair[eq + 1, pair.length - eq - 1]
|
|
154
|
+
h[Url.unescape(k)] = Url.unescape(v)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
h
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
data/lib/tep/version.rb
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Tep::WebSocket::Connection -- per-connection recv loop.
|
|
2
|
+
#
|
|
3
|
+
# Designed to run inside a Tep::Scheduler-managed fiber spawned by
|
|
4
|
+
# the upgrade route after the 101 response is written. The fiber:
|
|
5
|
+
# 1. Parks on Tep::Scheduler.io_wait(fd, READ, timeout) for bytes.
|
|
6
|
+
# 2. Reads via Sock.sphttp_recv_into_frame into the binary frame buf.
|
|
7
|
+
# 3. Walks the accumulated buffer with Frame.parse_from_buf,
|
|
8
|
+
# dispatching events to the Driver's handlers.
|
|
9
|
+
# 4. On close (sent OR received), exits cleanly + closes the fd.
|
|
10
|
+
#
|
|
11
|
+
# The recv buffer (sphttp_frame_buf, 64 KiB) is the per-fork static
|
|
12
|
+
# from Phase 0.5; cross-fiber sharing within one worker process is
|
|
13
|
+
# bounded by the worker's cooperative scheduling -- only one fiber
|
|
14
|
+
# parses at a time. A future Phase 2.1 (or whenever multi-fiber WS
|
|
15
|
+
# concurrency-per-worker becomes a goal) replaces this with
|
|
16
|
+
# per-fiber buffers via Fiber.storage (matz/spinel#578).
|
|
17
|
+
module Tep
|
|
18
|
+
module WebSocket
|
|
19
|
+
class Connection
|
|
20
|
+
attr_accessor :driver, :fd, :idle_timeout_seconds
|
|
21
|
+
|
|
22
|
+
def initialize(driver)
|
|
23
|
+
@driver = driver
|
|
24
|
+
@fd = driver.fd
|
|
25
|
+
@idle_timeout_seconds = 300
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def set_idle_timeout(seconds)
|
|
29
|
+
@idle_timeout_seconds = seconds
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Drive the recv loop. Returns 0 on clean close, -1 on error.
|
|
33
|
+
# Idempotent across multiple frames per recv: a single
|
|
34
|
+
# sphttp_recv_into_frame fill may contain several complete
|
|
35
|
+
# frames; Connection consumes them all before parking again.
|
|
36
|
+
#
|
|
37
|
+
# The caller (Tep::Server::Scheduled.write_response) owns the
|
|
38
|
+
# fd lifecycle -- run() never calls sphttp_close. On clean
|
|
39
|
+
# close OR error the server's handle_connection closes the fd
|
|
40
|
+
# via its usual exit path.
|
|
41
|
+
def run
|
|
42
|
+
# Synthetic open event before the first recv -- handlers
|
|
43
|
+
# often want to send a welcome message.
|
|
44
|
+
Connection.dispatch_open(@driver)
|
|
45
|
+
|
|
46
|
+
while true
|
|
47
|
+
ready = Tep::Scheduler.io_wait(@fd, Tep::Scheduler::READ, @idle_timeout_seconds)
|
|
48
|
+
if ready == 0
|
|
49
|
+
# Timeout: close 1001 going-away.
|
|
50
|
+
@driver.close(Tep::WebSocket::CLOSE_GOING_AWAY, "idle timeout")
|
|
51
|
+
return 0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
n = Sock.sphttp_recv_into_frame(@fd)
|
|
55
|
+
if n <= 0
|
|
56
|
+
# EOF or error: dispatch close without sending one back
|
|
57
|
+
# (peer already gone) and exit.
|
|
58
|
+
Connection.dispatch_close(@driver, Tep::WebSocket::CLOSE_GOING_AWAY, "")
|
|
59
|
+
if n == 0
|
|
60
|
+
return 0
|
|
61
|
+
end
|
|
62
|
+
return -1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Parse + dispatch as many complete frames as possible
|
|
66
|
+
# from this recv.
|
|
67
|
+
state = Tep::WebSocket::ConnectionState.new
|
|
68
|
+
state.start = 0
|
|
69
|
+
state.avail = n
|
|
70
|
+
while true
|
|
71
|
+
r = Tep::WebSocket::Frame.parse_from_buf(state.start, state.avail)
|
|
72
|
+
if r.outcome == "need"
|
|
73
|
+
break
|
|
74
|
+
end
|
|
75
|
+
if r.outcome == "close"
|
|
76
|
+
@driver.close(r.close_code, "protocol error")
|
|
77
|
+
return 0
|
|
78
|
+
end
|
|
79
|
+
Connection.dispatch_frame(@driver, r.frame)
|
|
80
|
+
state.start = state.start + r.consumed
|
|
81
|
+
if state.start >= state.avail
|
|
82
|
+
break
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Route a parsed frame to the right handler.
|
|
90
|
+
def self.dispatch_frame(driver, frame)
|
|
91
|
+
op = frame.opcode
|
|
92
|
+
if op == Tep::WebSocket::OPCODE_TEXT
|
|
93
|
+
Connection.dispatch_message(driver, frame.payload, true)
|
|
94
|
+
elsif op == Tep::WebSocket::OPCODE_BINARY
|
|
95
|
+
Connection.dispatch_message(driver, frame.payload, false)
|
|
96
|
+
elsif op == Tep::WebSocket::OPCODE_PING
|
|
97
|
+
# Auto-pong with the ping's payload (§5.5.3).
|
|
98
|
+
driver.pong(frame.payload)
|
|
99
|
+
Connection.dispatch_ping(driver, frame.payload)
|
|
100
|
+
elsif op == Tep::WebSocket::OPCODE_PONG
|
|
101
|
+
Connection.dispatch_pong(driver, frame.payload)
|
|
102
|
+
elsif op == Tep::WebSocket::OPCODE_CLOSE
|
|
103
|
+
code = 0
|
|
104
|
+
reason = ""
|
|
105
|
+
if frame.payload.length >= 2
|
|
106
|
+
code = (frame.payload[0].ord << 8) | frame.payload[1].ord
|
|
107
|
+
if frame.payload.length > 2
|
|
108
|
+
reason = frame.payload[2, frame.payload.length - 2]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
# Echo the close back (§5.5.1) then dispatch.
|
|
112
|
+
driver.close(code == 0 ? Tep::WebSocket::CLOSE_NORMAL : code, reason)
|
|
113
|
+
Connection.dispatch_close(driver, code, reason)
|
|
114
|
+
end
|
|
115
|
+
0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.dispatch_open(driver)
|
|
119
|
+
evt = Tep::WebSocket::Event.new
|
|
120
|
+
driver.h_open.handle_event(evt)
|
|
121
|
+
0
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.dispatch_message(driver, data, text)
|
|
125
|
+
evt = Tep::WebSocket::Event.new
|
|
126
|
+
evt.data = data
|
|
127
|
+
driver.h_message.handle_event(evt)
|
|
128
|
+
0
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.dispatch_ping(driver, data)
|
|
132
|
+
evt = Tep::WebSocket::Event.new
|
|
133
|
+
evt.data = data
|
|
134
|
+
driver.h_ping.handle_event(evt)
|
|
135
|
+
0
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.dispatch_pong(driver, data)
|
|
139
|
+
evt = Tep::WebSocket::Event.new
|
|
140
|
+
evt.data = data
|
|
141
|
+
driver.h_pong.handle_event(evt)
|
|
142
|
+
0
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.dispatch_close(driver, code, reason)
|
|
146
|
+
evt = Tep::WebSocket::Event.new
|
|
147
|
+
evt.code = code
|
|
148
|
+
evt.reason = reason
|
|
149
|
+
driver.h_close.handle_event(evt)
|
|
150
|
+
# Auto-cleanup: any Broadcast subscription or Presence row
|
|
151
|
+
# keyed on this connection's fd gets dropped. Both calls
|
|
152
|
+
# are no-op-safe when nothing was tracked (zero matches).
|
|
153
|
+
# Apps that still call unsubscribe_fd / untrack_by_fd
|
|
154
|
+
# explicitly stay correct -- the second call finds 0 matches.
|
|
155
|
+
Tep::Broadcast.unsubscribe_fd(driver.fd)
|
|
156
|
+
Tep::Presence.untrack_by_fd(driver.fd)
|
|
157
|
+
0
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Per-recv-loop iteration state. Avoids tuple-returns from
|
|
162
|
+
# parse_from_buf calls (spinel multi-return support is uneven).
|
|
163
|
+
class ConnectionState
|
|
164
|
+
attr_accessor :start, :avail
|
|
165
|
+
def initialize
|
|
166
|
+
@start = 0
|
|
167
|
+
@avail = 0
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|