JetCore engine (Tag.cpp/.h, Global.cpp/.h, __*.cpp tag classes): - Numerous __mysql.cpp connection-pooling correctness fixes (stale result sets, double-indirection sessionid bugs, field caching, pool keying) and a session-registration-before-connection-confirmed ordering fix (dangling pointer on a failed connect) - Tag.cpp: large body of fixes to the tag-dispatch/container-scanning logic (bare-name fallback, database aliasing, single-quote handling, @-prefix session variable resolution, TAGDEF caching, nested split-tag scanning for if/while/until/ifrow so an inner one doesn't corrupt an outer one's own boundary scan) plus the process-first rendering order fix (a <process> now runs before <checksession>/page resolution, fixing a real "blank page after successful login" bug) - Global.h/.cpp: added imapSessions (parallel to the existing __mysql* session map) for the new <imap> tag family - __whiledir.cpp/__include.cpp: caching and directory-name-aliasing fixes - __call.cpp/.h: fixed a dangling-pointer bug - __write.cpp: fixed an O_WRONLY-only open that couldn't create a new file - __ifrow.cpp/__sql.cpp: fixed the same sessionid double-indirection bug __mysql.cpp had (resolveKeyword's result was being used as a map key lookup instead of the resolved value itself) jetc compiler (jetc/JetParser.cpp/.h, JetCodegen.cpp/.h, JetAST.h, jetc.cpp, build-shared.sh, build-compiled.sh): - Parser fixes: inline <include>, nested-quote handling, void-tag recognition - Codegen fixes and expanded AST support - build-shared.sh: atomic rename into place for a freshly-built .jtc (a live server dlopen()ing the same path mid-rebuild was corrupting its own already-mapped view of the old file) and a staleness check so a cached JetCore .o doesn't silently keep serving yesterday's fixes forever - Added -lssl to both build scripts' link lines (needed once the new <imap> tag's client-side TLS code existed - previously nothing in this codebase used OpenSSL's SSL/TLS client API, only libcrypto) Site content (jetc/barant-site/): SPA (fetch-and-swap navigation) rollout across essentially every .page/.jet/.section/.tag/.proc/.method file - dynamic-content fixes, fragment-mode stripping, native $[@name] session variables replacing older mechanisms, and this session's own checkout.page redesign, login start-param redirect fix, and per-page side-menu (menu.bar/$[PAGE].menu) refresh support. Completed the earlier main.css/control.css move from assets/ to assets/images/ (git still showed the old paths as tracked-but-deleted until now). Deliberately left out of this commit: dozens of .pre-<description>- <date> manual backup files (this session's and earlier ones' own before/after snapshots, not meant for permanent history), compiled .jtc build outputs, ad hoc test binaries and scripts under jetc/ (login_asan, wcwc_test, optest2_bin, etc.), and jetc/__pycache__/ - none of these are meant to be tracked; a .gitignore covering them would be worth adding separately.
261 lines
13 KiB
C++
261 lines
13 KiB
C++
#include "__mysql.h"
|
|
#include "Exception.h"
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
|
|
namespace jet {
|
|
|
|
namespace {
|
|
struct PooledConnection {
|
|
MYSQL *handle;
|
|
std::string host, user, database;
|
|
};
|
|
// Persistent, per-worker-thread connection cache - reused across
|
|
// requests instead of a fresh mysql_real_connect() every time.
|
|
// Safe with no locking: the apiserver's EPoll worker pool is a
|
|
// fixed set of OS threads for the process's whole life (see
|
|
// MysqlThreadScope's own doc comment - mysql_thread_init() runs
|
|
// once per worker thread at thread start), so a thread_local
|
|
// instance here means a given MYSQL* handle is only ever touched
|
|
// by the one thread that created it, never concurrently accessed.
|
|
// Keyed by sessionid, which is how this codebase's own <database>/
|
|
// <sql>/<ifrow> tags already address a specific connection
|
|
// (Global::sessions is keyed the same way) - the cached host/user/
|
|
// database are kept alongside purely as a defensive check that a
|
|
// reused entry really matches what THIS invocation asked for,
|
|
// not a real expectation that sessionid is ever reused across
|
|
// genuinely different backing databases in practice.
|
|
thread_local std::unordered_map<std::string, PooledConnection> connectionPool;
|
|
}
|
|
|
|
__mysql::__mysql(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this) {
|
|
|
|
if(!keywordDefined("host"))
|
|
throw coreutils::Exception("host must be specified for mysql tag.");
|
|
// jet-1.4's <database> spells the same attribute "name" instead of
|
|
// "database" (e.g. SESSIONCTL.tag's own <sessionctl> definition) -
|
|
// accept either rather than requiring that real content to change.
|
|
coreutils::ZString databaseKeyword = keywordDefined("database") ? "database" : "name";
|
|
if(!keywordDefined(databaseKeyword))
|
|
throw coreutils::Exception("database must be specified for mysql tag.");
|
|
if(!keywordDefined("user"))
|
|
throw coreutils::Exception("user must be specified for mysql tag.");
|
|
if(!keywordDefined("password"))
|
|
throw coreutils::Exception("password must be specified for mysql tag.");
|
|
|
|
// Same double-indirection bug as the host/user/password/database
|
|
// fix above - resolveKeyword("sessionid") already returns the
|
|
// resolved VALUE (e.g. "_SESSIONCTL"), so wrapping it in keywords[...]
|
|
// used that value as a map KEY instead, auto-vivifying an empty
|
|
// string (no attribute is ever literally named "_SESSIONCTL").
|
|
// sessionId was therefore always empty regardless of what
|
|
// sessionid= actually specified - confirmed live via gdb: two
|
|
// real <mysql>/<database> tags with genuinely different sessionid
|
|
// values ("" and "_SESSIONCTL") both registered under "", the
|
|
// second colliding with "sessionid already exists.".
|
|
sessionId = resolveKeyword("sessionid");
|
|
|
|
// mysql_real_connect() returns NULL on failure but does NOT itself
|
|
// free the handle mysql_init() allocated - the caller is still
|
|
// expected to mysql_close() that original handle to avoid leaking
|
|
// it (MySQL C API docs). The previous code overwrote `mysql` with
|
|
// the connect() return value directly, so on failure the only
|
|
// pointer to the real handle was lost before ever being freed - a
|
|
// real leak on every failed connection attempt (this member's
|
|
// eventual value, NULL, is what a normal ~__mysql() would try to
|
|
// mysql_close() anyway, which is a safe no-op, but that never even
|
|
// runs here since the constructor throws below - see the
|
|
// addSession() comment for why that matters more).
|
|
// resolveKeyword() already returns the fully-resolved VALUE - the
|
|
// previous keywords[resolveKeyword("host")] wrapped that resolved
|
|
// VALUE ("localhost") back into a keywords[] lookup, i.e. used it
|
|
// as a MAP KEY instead of the value itself. Since no attribute is
|
|
// ever literally named "localhost"/the real username/etc.,
|
|
// std::map::operator[] silently auto-vivified an empty MString for
|
|
// every one of the four parameters - mysql_real_connect() was
|
|
// therefore always called with host/user/password/database all
|
|
// empty, regardless of what the tag actually specified. Confirmed
|
|
// live: MySQL's own error ("Access denied for user 'barant'@'localhost'
|
|
// (using password: NO)") showed the password was genuinely empty,
|
|
// not merely wrong.
|
|
coreutils::MString hostVal = resolveKeyword("host");
|
|
coreutils::MString userVal = resolveKeyword("user");
|
|
coreutils::MString passwordVal = resolveKeyword("password");
|
|
coreutils::MString databaseVal = resolveKeyword(databaseKeyword);
|
|
|
|
// Reuse a live connection this same worker thread already holds
|
|
// for this sessionid, rather than paying mysql_real_connect()'s
|
|
// full handshake cost on every single request. mysql_ping() is
|
|
// what makes this safe - it's a genuine round trip to the server,
|
|
// so a connection MySQL itself already dropped (its own
|
|
// wait_timeout, or an admin-initiated restart like this project
|
|
// already lived through once) is detected and discarded here
|
|
// rather than handed to processContainer() to fail on later.
|
|
std::string poolKey = sessionId.str();
|
|
auto pooled = connectionPool.find(poolKey);
|
|
if(pooled != connectionPool.end() &&
|
|
pooled->second.host == hostVal.str() &&
|
|
pooled->second.user == userVal.str() &&
|
|
pooled->second.database == databaseVal.str()) {
|
|
if(mysql_ping(pooled->second.handle) == 0) {
|
|
mysql = pooled->second.handle;
|
|
} else {
|
|
mysql_close(pooled->second.handle);
|
|
connectionPool.erase(pooled);
|
|
}
|
|
}
|
|
|
|
if(!mysql) {
|
|
MYSQL *handle = mysql_init(NULL);
|
|
mysql = mysql_real_connect(handle,
|
|
hostVal.c_str(),
|
|
userVal.c_str(),
|
|
passwordVal.c_str(),
|
|
databaseVal.c_str(), 0, NULL, 0);
|
|
|
|
if(!mysql) {
|
|
std::string err = mysql_error(handle);
|
|
mysql_close(handle);
|
|
throw coreutils::Exception("database connection failed: " + err);
|
|
}
|
|
|
|
connectionPool[poolKey] = PooledConnection{mysql, hostVal.str(), userVal.str(), databaseVal.str()};
|
|
}
|
|
|
|
// Registered only once the connection is confirmed live - a real,
|
|
// reproducible crash (heap corruption - "free(): invalid pointer"
|
|
// seen live, then two independent server-hang repros) traced back
|
|
// to this: addSession() used to run BEFORE the connect attempt, so
|
|
// a FAILED connection (which throws, right above) left a dangling
|
|
// `this` pointer registered under sessionId in Global's session
|
|
// map forever - ~__mysql() (which would normally call
|
|
// removeSession()) never runs for a constructor that itself threw,
|
|
// per ordinary C++ rules (only already-completed base-class
|
|
// subobjects get destroyed during exception unwinding, not the
|
|
// derived object whose constructor is still executing). The very
|
|
// next <mysql> tag construction using the same sessionId (e.g. two
|
|
// product pages with an empty sessionid="" back to back, both
|
|
// failing the same way) then collided with that stale entry.
|
|
// Confirmed via a real, deterministic two-request repro before this
|
|
// fix, and confirmed gone after it.
|
|
global.addSession(sessionId, this);
|
|
|
|
processContainer(container);
|
|
|
|
}
|
|
|
|
__mysql::~__mysql() {
|
|
global.removeSession(sessionId);
|
|
mysql_free_result(result);
|
|
// mysql itself is deliberately NOT closed here anymore - see the
|
|
// connectionPool doc comment above the constructor. It's owned by
|
|
// this worker thread's persistent pool now and gets reused by
|
|
// whatever request this thread handles next, instead of being
|
|
// torn down and fully reconnected from scratch every time.
|
|
}
|
|
|
|
void __mysql::query(coreutils::MString query) {
|
|
int rc = mysql_real_query(mysql, query.getData(), query.getLength());
|
|
result = mysql_store_result(mysql);
|
|
if(result) {
|
|
row = mysql_fetch_row(result);
|
|
fieldLength = mysql_fetch_lengths(result);
|
|
qFields = mysql_num_fields(result);
|
|
// Built once per query, not once per column LOOKUP - see the
|
|
// matching comment on getColumnValue() and on fieldIndex's own
|
|
// declaration in __mysql.h. mysql_fetch_fields() returns the
|
|
// whole field array in one call (mysql_fetch_field_direct()
|
|
// looping ix=0..qFields-1, as getColumnValue() used to do on
|
|
// every single named access, does the exact same work over and
|
|
// over for a result set whose schema never changes after this
|
|
// point).
|
|
fieldIndex.clear();
|
|
MYSQL_FIELD *fields = mysql_fetch_fields(result);
|
|
for(unsigned int ix = 0; ix < qFields; ++ix)
|
|
fieldIndex[std::string(fields[ix].name)] = (int)ix;
|
|
} else {
|
|
// A failed/errored query (real content builds one from an unset
|
|
// $[...] parameter, e.g. "where tasks.id=" with no value - genuine
|
|
// MySQL syntax error) previously left row/qFields holding whatever
|
|
// a PRIOR successful query on this same session object had set,
|
|
// since only the success branch above ever touched them. A later
|
|
// getColumnValue() call then looped 0..qFields (stale, nonzero)
|
|
// calling mysql_fetch_field_direct() against this NULL `result` -
|
|
// segfaulting (confirmed via gdb: SIGSEGV in
|
|
// mysql_fetch_field_direct, called from getColumnValue) instead of
|
|
// reaching the "column does not exist in session result." the
|
|
// caller already throws/catches for a genuinely missing column.
|
|
// Resetting both here makes a failed query present as "no row, no
|
|
// columns" - clean and consistent regardless of what ran before it
|
|
// on this session.
|
|
row = NULL;
|
|
qFields = 0;
|
|
fieldIndex.clear();
|
|
}
|
|
}
|
|
|
|
void __mysql::nextRow() {
|
|
row = mysql_fetch_row(result);
|
|
fieldLength = mysql_fetch_lengths(result);
|
|
}
|
|
|
|
bool __mysql::hasRow() {
|
|
return row != NULL;
|
|
}
|
|
|
|
coreutils::ZString __mysql::getColumnValue(coreutils::ZString column) {
|
|
MYSQL_FIELD *field;
|
|
if(column == "?#") {
|
|
nbrOfColumns = (int)qFields;
|
|
return nbrOfColumns;
|
|
} else if(column.ifNext("#") || column.ifNext("?")) {
|
|
// "#N"/"?N" (column name at 1-based position N) are the same
|
|
// jet-1.4 syntax under two different prefixes - real, unmodified
|
|
// 1.4 content uses both (barant.tag's own <showtable> definition
|
|
// writes $[?$[index]], never $[#$[index]]) - confirmed via a real
|
|
// repro: admin.page's own Users/Products tables rendered every
|
|
// column header and cell completely blank, since "?N" fell all
|
|
// the way through to the ordinary column-VALUE lookup below
|
|
// (nothing named "?3" exists in the row) and read as empty rather
|
|
// than as "the name of column 3", per the jet-1.4-fallback #2
|
|
// empty-string-on-missing-column behavior elsewhere in this
|
|
// codebase (see Tag.cpp).
|
|
if(column.eod()) {
|
|
nbrOfRows = (int)mysql_num_rows(result);
|
|
return nbrOfRows;
|
|
} else {
|
|
int index = column.asInteger();
|
|
field = mysql_fetch_field_direct(result, index - 1);
|
|
return coreutils::ZString(field->name);
|
|
}
|
|
}
|
|
|
|
// A query that ran successfully but matched zero rows (a real,
|
|
// valid query - e.g. cart.page's own aggregate/JOIN query against
|
|
// an empty cart session - not a failed one, see query()'s own
|
|
// row=NULL/qFields=0 reset above for that separate case) leaves
|
|
// qFields correctly reflecting the real schema but `row` NULL.
|
|
// The loop below finds a genuine matching column name and then
|
|
// dereferences row[ix] - segfaulting (confirmed via gdb: SIGSEGV
|
|
// right here) instead of reaching a catchable error. Reusing the
|
|
// same "column does not exist" exception the loop's own fallthrough
|
|
// already throws - no current row means no column value is
|
|
// obtainable either way, and callers already expect and catch
|
|
// this exact exception.
|
|
if(!row)
|
|
throw coreutils::Exception("column does not exist in session result.");
|
|
|
|
// O(1) against the map query() already built for this result set,
|
|
// instead of re-deriving field metadata (mysql_fetch_field_direct,
|
|
// a real MySQL C API call) and string-comparing column names one
|
|
// by one on every single named access - see fieldIndex's own doc
|
|
// comment in __mysql.h for the full story.
|
|
auto it = fieldIndex.find(column.str());
|
|
if(it != fieldIndex.end())
|
|
return coreutils::ZString(row[it->second], fieldLength[it->second]);
|
|
throw coreutils::Exception("column does not exist in session result.");
|
|
}
|
|
|
|
}
|