diff --git a/README.md b/README.md index 06bd3c2..43dcd59 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ UCE pages now use explicit request handlers instead of implicit globals: Useful related runtime patterns: -- `render_file(String file_name)` or `render_file(String file_name, Request& context)` to invoke another page +- `unit_render(String file_name)` or `unit_render(String file_name, Request& context)` to invoke another page - `context.cfg` for request-local structured configuration - `context.call` for invocation or message-local structured input - `context.connection` for broker-owned per-WebSocket-connection state shared across `WS(Request& context)` calls @@ -101,7 +101,7 @@ Use `` by default for user-visible text. Use `` only for tru UCE includes a native component layer built on top of ordinary `.uce` files: - `component(name[, props[, context]])` -- `render_component(name[, props[, context]])` +- `component_render(name[, props[, context]])` - `component_exists(name)` - `component_resolve(name)` @@ -121,11 +121,11 @@ When you want returned component markup inside a literal block, prefer: ``` -because `` HTML-escapes the returned markup. For direct output from C++ code, use `render_component(...)`. +because `` HTML-escapes the returned markup. For direct output from C++ code, use `component_render(...)`. Components expose `COMPONENT(Request& context)` as their default entrypoint and may expose additional named handlers with `COMPONENT:NAME(Request& context)`. -The component helpers call only `COMPONENT...` handlers. A file meant purely for component use can define `COMPONENT()` without defining `RENDER()`, which keeps direct page entry and component entry cleanly separated. Inside a component file, `component(":NAME", props, context)` and `render_component(":NAME", props, context)` target another named component handler in that same file. +The component helpers call only `COMPONENT...` handlers. A file meant purely for component use can define `COMPONENT()` without defining `RENDER()`, which keeps direct page entry and component entry cleanly separated. Inside a component file, `component(":NAME", props, context)` and `component_render(":NAME", props, context)` target another named component handler in that same file. ## WebSockets @@ -254,6 +254,10 @@ SESSION_PATH=/tmp/uce/sessions FCGI_SOCKET_PATH=/run/uce.sock FCGI_PORT=9993 +PRECOMPILE_FILES_IN= +SITE_DIRECTORY=site +PROACTIVE_COMPILE_CHECK_INTERVAL=60 + WORKER_COUNT=4 MAX_MEMORY=16777216 SESSION_TIME=2592000 @@ -273,6 +277,14 @@ If you want WebSocket support through nginx, also make sure the built-in HTTP li HTTP_PORT=8080 ``` +Proactive compilation settings: + +- `SITE_DIRECTORY=site` tells the runtime which tree to scan on startup for `.uce` files when `PRECOMPILE_FILES_IN` is left empty. +- `PRECOMPILE_FILES_IN=` can override that startup scan root with a different absolute or runtime-relative directory. +- `PROACTIVE_COMPILE_CHECK_INTERVAL=60` controls how often the low-priority background compiler rechecks known `.uce` files for stale or missing shared objects. + +The runtime keeps a shared known-file registry under `BIN_DIRECTORY` and updates it as request handling discovers new `.uce` files, so proactive recompiles are not limited to the initial startup scan. + Recommended deployment notes: - keep `HTTP_PORT` bound to localhost only at the firewall or by network policy; nginx should be the public entry point diff --git a/etc/uce/settings.cfg b/etc/uce/settings.cfg index 4422746..12c3cb6 100644 --- a/etc/uce/settings.cfg +++ b/etc/uce/settings.cfg @@ -7,9 +7,16 @@ SESSION_PATH=/tmp/uce/sessions FCGI_SOCKET_PATH=/run/uce.sock FCGI_PORT=9993 -# PRECOMPILE .uce FILES +# OPTIONAL PROACTIVE COMPILE ROOT +# Leave empty to scan SITE_DIRECTORY relative to the runtime root. PRECOMPILE_FILES_IN= +# PUBLIC SITE DIRECTORY USED FOR STARTUP SCAN WHEN PRECOMPILE_FILES_IN IS EMPTY +SITE_DIRECTORY=site + +# PERIODIC KNOWN-.uce RECHECK INTERVAL IN SECONDS +PROACTIVE_COMPILE_CHECK_INTERVAL=60 + # SPAWN WORKERS WORKER_COUNT=4 diff --git a/site/doc/areas/ob.txt b/site/doc/areas/ob.txt index a394486..8d691d5 100644 --- a/site/doc/areas/ob.txt +++ b/site/doc/areas/ob.txt @@ -1,7 +1,7 @@ Output / Invocation Functions 1_RENDER -call_file +unit_call component component_exists component_resolve @@ -11,5 +11,6 @@ ob_get ob_get_close ob_start print -render_component -render_file +component_render +unit_load +unit_render diff --git a/site/doc/areas/runtime.txt b/site/doc/areas/runtime.txt new file mode 100644 index 0000000..8aa69b9 --- /dev/null +++ b/site/doc/areas/runtime.txt @@ -0,0 +1,4 @@ +Runtime +unit_info +units_list +unit_compile diff --git a/site/doc/areas/session.txt b/site/doc/areas/session.txt index f19efc8..a226ecd 100644 --- a/site/doc/areas/session.txt +++ b/site/doc/areas/session.txt @@ -1,5 +1,5 @@ Sessions -make_session_id +session_id_create session_destroy session_start diff --git a/site/doc/areas/sys.txt b/site/doc/areas/sys.txt index 0313987..3e7ca52 100644 --- a/site/doc/areas/sys.txt +++ b/site/doc/areas/sys.txt @@ -8,11 +8,11 @@ file_exists file_get_contents file_mtime file_put_contents -get_cwd +cwd_get ls mkdir path_join -set_cwd +cwd_set shell_escape shell_exec -unlink +file_unlink diff --git a/site/doc/areas/time.txt b/site/doc/areas/time.txt index 432117d..b085287 100644 --- a/site/doc/areas/time.txt +++ b/site/doc/areas/time.txt @@ -1,7 +1,7 @@ Time and Date Functions -microtime +time_precise time -date -gmdate -parse_time +time_format_local +time_format_utc +time_parse diff --git a/site/doc/areas/uri.txt b/site/doc/areas/uri.txt index adebcc9..cb9154e 100644 --- a/site/doc/areas/uri.txt +++ b/site/doc/areas/uri.txt @@ -1,7 +1,7 @@ URI Functions encode_query -make_session_id +session_id_create parse_query uri_decode uri_encode diff --git a/site/doc/index.uce b/site/doc/index.uce index 3ff7c8a..c7c9871 100644 --- a/site/doc/index.uce +++ b/site/doc/index.uce @@ -7,15 +7,15 @@ void render_see_section(String name) { if(idx == 0) { - <>

} @@ -26,20 +26,39 @@ RENDER(Request& context) <> +

- UCE Docs: - + UCE Docs /

All API Functions

+ +
+
+ +
+

All API Functions

+
-
: struct
+ -
: directive
+ - +
+ +
+
+ } else { + ?>

Parameters

Related

Related

()
()
0) + { + ?>
diff --git a/site/doc/pages/0_context.txt b/site/doc/pages/0_context.txt index d10b143..920e185 100644 --- a/site/doc/pages/0_context.txt +++ b/site/doc/pages/0_context.txt @@ -64,9 +64,8 @@ Whether the request should be logged f64 stats.time_start f64 stats.time_end -:render_file(String file_name, [Request& context]) +:unit_render(String file_name, [Request& context]) Invokes another UCE file using the current or supplied request context :see >types - diff --git a/site/doc/pages/1_COMPONENT.txt b/site/doc/pages/1_COMPONENT.txt index 39b3832..b447480 100644 --- a/site/doc/pages/1_COMPONENT.txt +++ b/site/doc/pages/1_COMPONENT.txt @@ -4,7 +4,7 @@ COMPONENT(Request& context) :desc Defines the default component entrypoint for the current `.uce` file. -`component()` and `render_component()` invoke `COMPONENT(Request& context)` by default. Named component entrypoints use `COMPONENT:NAME(Request& context)`. +`component()` and `component_render()` invoke `COMPONENT(Request& context)` by default. Named component entrypoints use `COMPONENT:NAME(Request& context)`. This keeps page rendering and component rendering separate: @@ -16,7 +16,7 @@ A file intended only for component reuse can define `COMPONENT()` without defini Inside component handlers, props arrive through `context.call`. -When you call `component(":NAME", props, context)` or `render_component(":NAME", props, context)`, the runtime resolves `:NAME` against the current `.uce` file instead of requiring the file name again. +When you call `component(":NAME", props, context)` or `component_render(":NAME", props, context)`, the runtime resolves `:NAME` against the current `.uce` file instead of requiring the file name again. Examples: `COMPONENT(Request& context)` @@ -31,6 +31,6 @@ Examples: :see >component ->render_component +>component_render >1_RENDER >1_WS diff --git a/site/doc/pages/1_RENDER.txt b/site/doc/pages/1_RENDER.txt index 1628652..3fcd182 100644 --- a/site/doc/pages/1_RENDER.txt +++ b/site/doc/pages/1_RENDER.txt @@ -14,7 +14,7 @@ The request environment is passed explicitly through `context`, including params For a normal direct page request, `context.call` starts empty. -If the page is invoked from another UCE file via `render_file(file_name, context)`, the callee receives that same `context`. +If the page is invoked from another UCE file via `unit_render(file_name, context)`, the callee receives that same `context`. Pages intended to serve WebSocket traffic may expose both `RENDER(Request& context)` and `WS(Request& context)`. Files may also define `COMPONENT()` handlers when they intentionally need both page and component behavior in one unit. In that case `RENDER(Request& context)` serves the direct HTTP response, `WS(Request& context)` handles subsequent WebSocket messages, and `COMPONENT()` remains available only through the component helpers. diff --git a/site/doc/pages/1_preprocessor.txt b/site/doc/pages/1_preprocessor.txt index 3116e44..9609554 100644 --- a/site/doc/pages/1_preprocessor.txt +++ b/site/doc/pages/1_preprocessor.txt @@ -74,7 +74,7 @@ The loaded file is resolved relative to the current source file unless the path - `#load` is recognized only when the current line starts with `#load ` at column 1. - `EXPORT` harvesting likewise only triggers when the current line starts with `EXPORT` at column 1 and is followed by whitespace. - Relative `#load` paths are expanded against the including unit's source directory. -- `render_file()` and `call_file()` are runtime APIs; `#load` is a compile-time include/composition feature. +- `unit_render()` and `unit_call()` are runtime APIs; `#load` is a compile-time include/composition feature. :Limitations - This pass is character-wise, not a full parser. @@ -92,7 +92,7 @@ The loaded file is resolved relative to the current source file unless the path :see load -render_file -call_file +unit_render +unit_call 0_context 1_COMPONENT diff --git a/site/doc/pages/DTree.txt b/site/doc/pages/DTree.txt index 985d286..10787b8 100644 --- a/site/doc/pages/DTree.txt +++ b/site/doc/pages/DTree.txt @@ -12,7 +12,7 @@ Common uses include: `json_decode()` / `json_encode()` `context.var` `context.call` -`call_file()` return values +`unit_call()` return values Useful methods include: `to_string()` diff --git a/site/doc/pages/component.txt b/site/doc/pages/component.txt index f0f1199..e95fa75 100644 --- a/site/doc/pages/component.txt +++ b/site/doc/pages/component.txt @@ -8,7 +8,7 @@ Renders another `.uce` file as a component and returns the captured output as a Component props are passed in `context.call`. -Because `` HTML-escapes its value, embed component markup with ``, `print(component(...))`, or use `render_component(...)` for direct output. +Because `` HTML-escapes its value, embed component markup with ``, `print(component(...))`, or use `component_render(...)` for direct output. When `name` contains a colon, such as `components/card:BODY`, the part after the colon selects a named component handler exported from the component file through `COMPONENT:BODY(Request& context)`. diff --git a/site/doc/pages/render_component.txt b/site/doc/pages/component_render.txt similarity index 75% rename from site/doc/pages/render_component.txt rename to site/doc/pages/component_render.txt index 9c6fb8d..fa1a7f4 100644 --- a/site/doc/pages/render_component.txt +++ b/site/doc/pages/component_render.txt @@ -1,5 +1,5 @@ :sig -void render_component(String name, [DTree props], [Request& context]) +void component_render(String name, [DTree props], [Request& context]) :desc Renders another `.uce` file as a component and writes the result directly to the current output buffer. @@ -10,12 +10,12 @@ Component props are passed through `context.call`, and `name:COMPONENTFUNC` may When `name` starts with `:`, the runtime resolves that named handler against the current `.uce` file. -Use `render_component()` when you want to write component output directly from C++ code instead of capturing it as a `String`. +Use `component_render()` when you want to write component output directly from C++ code instead of capturing it as a `String`. Example: `DTree props;` `props["body"] = "Hello";` -`render_component("components/card:BODY", props, context);` +`component_render("components/card:BODY", props, context);` :see >ob diff --git a/site/doc/pages/get_cwd.txt b/site/doc/pages/cwd_get.txt similarity index 87% rename from site/doc/pages/get_cwd.txt rename to site/doc/pages/cwd_get.txt index 3c68e43..94508e2 100644 --- a/site/doc/pages/get_cwd.txt +++ b/site/doc/pages/cwd_get.txt @@ -1,5 +1,5 @@ :sig -String get_cwd() +String cwd_get() :params return value : the current working directory diff --git a/site/doc/pages/set_cwd.txt b/site/doc/pages/cwd_set.txt similarity index 78% rename from site/doc/pages/set_cwd.txt rename to site/doc/pages/cwd_set.txt index 755aca1..c4c856d 100644 --- a/site/doc/pages/set_cwd.txt +++ b/site/doc/pages/cwd_set.txt @@ -1,5 +1,5 @@ :sig -void set_cwd(String path) +void cwd_set(String path) :params path : the new working directory diff --git a/site/doc/pages/file_unlink.txt b/site/doc/pages/file_unlink.txt new file mode 100644 index 0000000..446485a --- /dev/null +++ b/site/doc/pages/file_unlink.txt @@ -0,0 +1,11 @@ +:sig +void file_unlink(String file_name) + +:params +file_name : name of the file + +:desc +Deletes the file identified by `file_name`. + +:see +>sys diff --git a/site/doc/pages/markdown_to_ast.txt b/site/doc/pages/markdown_to_ast.txt index d772b54..b67e27b 100644 --- a/site/doc/pages/markdown_to_ast.txt +++ b/site/doc/pages/markdown_to_ast.txt @@ -72,6 +72,6 @@ The parser preserves directive data needed by those hooks. :see markdown_to_html component -render_component +component_render json_encode DTree diff --git a/site/doc/pages/markdown_to_html.txt b/site/doc/pages/markdown_to_html.txt index 090b80c..04a153f 100644 --- a/site/doc/pages/markdown_to_html.txt +++ b/site/doc/pages/markdown_to_html.txt @@ -95,6 +95,6 @@ This makes directive components a good fit for alerts, callouts, cards, embeds, :see markdown_to_ast component -render_component +component_render json_decode String diff --git a/site/doc/pages/make_session_id.txt b/site/doc/pages/session_id_create.txt similarity index 58% rename from site/doc/pages/make_session_id.txt rename to site/doc/pages/session_id_create.txt index 755a06c..fbb6616 100644 --- a/site/doc/pages/make_session_id.txt +++ b/site/doc/pages/session_id_create.txt @@ -1,11 +1,11 @@ :sig -String make_session_id() +String session_id_create() :params return value : a new session ID :desc -Creates a session ID +Creates a session ID. :see >session diff --git a/site/doc/pages/kill.txt b/site/doc/pages/task_kill.txt similarity index 81% rename from site/doc/pages/kill.txt rename to site/doc/pages/task_kill.txt index 0094a4f..a4ed75b 100644 --- a/site/doc/pages/kill.txt +++ b/site/doc/pages/task_kill.txt @@ -1,5 +1,5 @@ :sig -s64 kill(pid_t pid, s64 sig) +int task_kill(pid_t pid, int sig = 0) :params pid : PID of the process @@ -7,7 +7,7 @@ sig : signal number return value : 0 if signal was sent, -1 otherwise :desc -This is the standard POSIX kill() function, provided here for reference. +Wraps the standard POSIX `kill()` function. Possible signal numbers are: SIGABND, SIGABRT, SIGALRM, SIGBUS, SIGFPE, SIGHUP, SIGILL, SIGINT, SIGKILL, SIGPIPE, SIGPOLL, SIGPROF, SIGQUIT, SIGSEGV, SIGSYS, SIGTERM, SIGTRAP, SIGURG, SIGUSR1, SIGUSR2, SIGVTALRM, SIGXCPU, SIGXFSZ, SIGCHLD, SIGIO, SIGIOERR, SIGWINCH, SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGCONT. diff --git a/site/doc/pages/date.txt b/site/doc/pages/time_format_local.txt similarity index 97% rename from site/doc/pages/date.txt rename to site/doc/pages/time_format_local.txt index 7e0c918..d7f57ed 100644 --- a/site/doc/pages/date.txt +++ b/site/doc/pages/time_format_local.txt @@ -1,5 +1,5 @@ :sig -String date(String format = "", u64 timestamp = 0) +String time_format_local(String format = "", u64 timestamp = 0) :params format : formatting string specifying the date format diff --git a/site/doc/pages/gmdate.txt b/site/doc/pages/time_format_utc.txt similarity index 98% rename from site/doc/pages/gmdate.txt rename to site/doc/pages/time_format_utc.txt index e29f761..7a870e6 100644 --- a/site/doc/pages/gmdate.txt +++ b/site/doc/pages/time_format_utc.txt @@ -1,5 +1,5 @@ :sig -String gmdate(String format = "", u64 timestamp = 0) +String time_format_utc(String format = "", u64 timestamp = 0) :params format : formatting string specifying the date format diff --git a/site/doc/pages/parse_time.txt b/site/doc/pages/time_parse.txt similarity index 86% rename from site/doc/pages/parse_time.txt rename to site/doc/pages/time_parse.txt index a32f7cb..e0c2e06 100644 --- a/site/doc/pages/parse_time.txt +++ b/site/doc/pages/time_parse.txt @@ -1,5 +1,5 @@ :sig -u64 parse_time(String time_string) +u64 time_parse(String time_string) :params time_string : a string containing a date and/or time in text form diff --git a/site/doc/pages/microtime.txt b/site/doc/pages/time_precise.txt similarity index 89% rename from site/doc/pages/microtime.txt rename to site/doc/pages/time_precise.txt index 9c42f29..c640f85 100644 --- a/site/doc/pages/microtime.txt +++ b/site/doc/pages/time_precise.txt @@ -1,5 +1,5 @@ :sig -f64 microtime() +f64 time_precise() :params return value : current Unix timestamp diff --git a/site/doc/pages/call_file.txt b/site/doc/pages/unit_call.txt similarity index 60% rename from site/doc/pages/call_file.txt rename to site/doc/pages/unit_call.txt index 7e83b01..3e60a6f 100644 --- a/site/doc/pages/call_file.txt +++ b/site/doc/pages/unit_call.txt @@ -1,5 +1,5 @@ :sig -DTree* call_file(String file_name, String function_name, DTree* call_param = null) +DTree* unit_call(String file_name, String function_name, DTree* call_param = null) :params file_name : UCE file to load and execute @@ -8,16 +8,17 @@ call_param : optional, call parameter return value : DTree* returned from function :desc -Calls a function inside a UCE file. +Calls an exported function inside a UCE file. :Example // export a function -EXPORT void test_func() +EXPORT DTree* test_func(DTree* call_param) { print("HELLO FROM TEST FUNCTION"); + return(0); } // use that function in another file -call_file("call_file_funcs.uce", "test_func"); +unit_call("call_file_funcs.uce", "test_func"); :see >ob diff --git a/site/doc/pages/unit_compile.txt b/site/doc/pages/unit_compile.txt new file mode 100644 index 0000000..ca1ecd8 --- /dev/null +++ b/site/doc/pages/unit_compile.txt @@ -0,0 +1,15 @@ +:sig +bool unit_compile(String path = "") + +:params +path : optional UCE unit path. If empty, recompiles the current executing unit. +return value : `true` when the unit was compiled and loaded successfully + +:desc +Triggers a manual recompile of a UCE compilation unit. + +If `path` is relative, it is resolved relative to the current executing unit. Successful manual compiles also refresh the in-memory metadata for that unit. + +:see +unit_info +units_list diff --git a/site/doc/pages/unit_info.txt b/site/doc/pages/unit_info.txt new file mode 100644 index 0000000..d200a9e --- /dev/null +++ b/site/doc/pages/unit_info.txt @@ -0,0 +1,27 @@ +:sig +DTree unit_info(String path = "") + +:params +path : optional UCE unit path. If empty, uses the current executing unit. +return value : metadata tree for the resolved unit, or an empty tree if the unit cannot be resolved + +:desc +Returns runtime metadata for a UCE compilation unit. + +The returned tree includes: + +- normalized paths and generated artifact paths +- compile status, compile error status, runtime error status, and a combined `error_status` +- request and invocation counters +- best, worst, last, and average render time +- compile counters plus best, worst, last, and average compile time +- file mtimes, stale status, load status, and exported API declarations + +If `path` is relative, it is resolved relative to the current executing unit. + +Because unit metadata lives in process memory alongside `SharedUnit`, request and timing counters reflect the current runtime process, not an aggregate across every worker process. + +:see +units_list +unit_compile +0_context diff --git a/site/doc/pages/unit_load.txt b/site/doc/pages/unit_load.txt new file mode 100644 index 0000000..4b20426 --- /dev/null +++ b/site/doc/pages/unit_load.txt @@ -0,0 +1,16 @@ +:sig +SharedUnit* unit_load(String file_name) + +:params +file_name : UCE file to load +return value : loaded shared unit, or `null` if the unit could not be loaded + +:desc +Loads a UCE compilation unit and returns its in-memory `SharedUnit` record. + +This is a low-level runtime helper. Most application code should prefer `unit_render()`, `unit_call()`, `component()`, or `component_render()`. + +:see +unit_render +unit_call +load diff --git a/site/doc/pages/render_file.txt b/site/doc/pages/unit_render.txt similarity index 68% rename from site/doc/pages/render_file.txt rename to site/doc/pages/unit_render.txt index 66c5fe7..53c6d3c 100644 --- a/site/doc/pages/render_file.txt +++ b/site/doc/pages/unit_render.txt @@ -1,6 +1,6 @@ :sig -void render_file(String file_name) -void render_file(String file_name, Request& context) +void unit_render(String file_name) +void unit_render(String file_name, Request& context) :params file_name : UCE file to load and execute @@ -13,10 +13,10 @@ If `context` is omitted, the current active request context is used. :Example // call a common page template -render_file("page-template.uce"); +unit_render("page-template.uce"); // explicitly pass a request context -render_file("page-template.uce", context); +unit_render("page-template.uce", context); :see >ob diff --git a/site/doc/pages/units_list.txt b/site/doc/pages/units_list.txt new file mode 100644 index 0000000..560dd5c --- /dev/null +++ b/site/doc/pages/units_list.txt @@ -0,0 +1,11 @@ +:sig +std::vector units_list() + +:desc +Returns the normalized paths of all known `.uce` units. + +This includes the shared known-unit registry plus any units already loaded in the current runtime process. + +:see +unit_info +unit_compile diff --git a/site/doc/pages/unlink.txt b/site/doc/pages/unlink.txt deleted file mode 100644 index 5d80d53..0000000 --- a/site/doc/pages/unlink.txt +++ /dev/null @@ -1,11 +0,0 @@ -:sig -void unlink(String file_name) - -:params -file_name : name of the file - -:desc -Deletes the file identified by 'file_name'. - -:see ->sys diff --git a/site/doc/search.uce b/site/doc/search.uce new file mode 100644 index 0000000..35128df --- /dev/null +++ b/site/doc/search.uce @@ -0,0 +1,67 @@ + +// Returns whether haystack contains needle (case-sensitive, both should be pre-lowercased) +bool str_contains(String haystack, String needle) +{ + return replace(haystack, needle, "\x01") != haystack; +} + +RENDER(Request& context) +{ + String q = to_lower(trim(context.get["q"])); + + if(q.length() < 2) + { + return; + } + + u32 count = 0; + + for(auto file_name : ls("pages/")) + { + String ft = nibble(file_name, "."); + String name = ft; + String badge = ""; + + if(ft.substr(0, 2) == "0_") + { + nibble(name, "_"); + badge = "struct"; + } + else if(ft.substr(0, 2) == "1_") + { + nibble(name, "_"); + badge = "directive"; + } + + String content = file_get_contents("pages/" + ft + ".txt"); + + if(str_contains(to_lower(name), q) || str_contains(to_lower(content), q)) + { + // Find first content line that contains the query for a snippet + String snippet = ""; + for(auto line : split(content, "\n")) + { + String t = trim(line); + if(t.length() > 4 && t.substr(0, 1) != ":" && str_contains(to_lower(t), q)) + { + snippet = t; + if(snippet.length() > 100) + snippet = snippet.substr(0, 100) + "…"; + break; + } + } + + <>
+ +
+
+ + count += 1; + } + } + + if(count == 0) + { + <>

No results for ""

+ } +} diff --git a/site/doc/style.css b/site/doc/style.css index c4cba53..0ad307c 100644 --- a/site/doc/style.css +++ b/site/doc/style.css @@ -1,105 +1,480 @@ +/* UCE Documentation — Theme + Color palette: #139 deep-blue base · white text · yellow accents */ + +:root { + --bg: #113399; + --bg-surface: rgba(255, 255, 255, 0.065); + --bg-surface-hover: rgba(255, 255, 255, 0.11); + --bg-inset: rgba(0, 0, 0, 0.22); + --bg-code: rgba(0, 0, 0, 0.28); + --text: #e8ecf4; + --text-dim: rgba(255, 255, 255, 0.55); + --text-muted: rgba(255, 255, 255, 0.35); + --accent: #ffd644; + --accent-dim: rgba(255, 214, 68, 0.1); + --accent-hover: #ffe680; + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.18); + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace; + --radius: 8px; + --radius-lg: 14px; + --ease: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ── Reset ── */ + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + * { font-family: inherit; font-size: inherit; - box-sizing: inherit; color: inherit; line-height: inherit; } -h1 { - font-family: monospace; - font-size: 200%; - padding-top: 8px; - padding-bottom: 8px; +/* ── Base ── */ + +html { + font-size: 15px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } body { - max-width: 1024px; - margin-left: auto; - margin-right: auto; - padding-left: 16px; - padding-right: 16px; - font-family: Tahoma, Helvetica, Arial; - font-size: 1.2em; - box-sizing: border-box; - background: #139; - color: white; - line-height: 150%; + max-width: 1200px; + margin: 0 auto; + padding: 0 24px 64px; + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.65; + background: var(--bg); + color: var(--text); } -body > * { - background: rgba(255,255,255,0.1); - padding: 32px; - margin: 16px; +/* ── Typography ── */ + +h1 { + font-family: var(--font-mono); + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.02em; + padding: 28px 0 20px; + border-bottom: 1px solid var(--border); + margin-bottom: 28px; } -a { - color: yellow; +h1 a { + text-decoration: none; + color: var(--text); + transition: color 150ms var(--ease); } -form > div, label { - display: block; - padding-top: 8px; - padding-bottom: 8px; +h1 a:hover { + color: var(--accent); } -input, textarea { - background: rgba(0,0,0,0.2); - border: 2px solid rgba(255,255,255,0.2); -} - -input[type=submit], button { - padding: 8px; - cursor: pointer; -} - -input[type=submit]:hover, button:hover { - color: yellow; - background: rgba(0,0,0,0.5); -} - -input[type=text], textarea { - padding: 8px; - width: 100%; -} - -input[type=text]:hover, textarea:hover { - background: rgba(0,0,0,0.25); -} - -textarea { - height: 20%; -} - -pre { - height: 20%; - overflow: auto; - padding: 8px; - border: 2px solid rgba(0,0,0,0.2); - background: rgba(100,100,100,0.15); - font-family: monospace; - white-space: pre-wrap; +h2 { + font-family: var(--font-mono); + font-size: 1.2rem; + font-weight: 600; + letter-spacing: -0.01em; + margin-bottom: 16px; + color: var(--text); } h3 { - margin: 0; - font-family: monospace; - font-size: 1.4em; - margin-bottom: 0.8em; - opacity: 0.7; + font-family: var(--font-mono); + font-size: 0.82rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-dim); + margin: 0 0 12px; +} + +a { + color: var(--accent); + text-decoration: none; + transition: color 150ms var(--ease); +} + +a:hover { + color: var(--accent-hover); + text-decoration: underline; + text-underline-offset: 2px; +} + +/* ── Index Layout ── */ + +.index-layout { + display: grid; + grid-template-columns: 280px 1fr; + gap: 32px; + align-items: start; +} + +@media (max-width: 800px) { + .index-layout { + grid-template-columns: 1fr; + } +} + +.sidebar { + position: sticky; + top: 20px; +} + +.category { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px 18px; + margin-bottom: 10px; + transition: border-color 150ms var(--ease); +} + +.category:hover { + border-color: var(--border-strong); +} + +.category h3 { + font-size: 0.75rem; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); +} + +.category ul { + list-style: none; +} + +.category li { + padding: 1px 0; +} + +.category li a { + font-family: var(--font-mono); + font-size: 0.85rem; + display: block; + padding: 3px 8px; + border-radius: 4px; + transition: background 150ms var(--ease); +} + +.category li a:hover { + background: var(--accent-dim); + text-decoration: none; +} + +.content h2 { + margin-bottom: 20px; +} + +.func-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); + gap: 4px; +} + +.func-item { + font-family: var(--font-mono); + font-size: 0.88rem; +} + +.func-item a { + display: block; + padding: 7px 12px; + border-radius: 6px; + transition: background 150ms var(--ease); +} + +.func-item a:hover { + background: var(--bg-surface-hover); + text-decoration: none; +} + +.badge { + font-size: 0.68rem; + font-weight: 500; + padding: 1px 7px; + border-radius: 4px; + margin-left: 6px; + vertical-align: middle; + background: rgba(255, 255, 255, 0.08); + color: var(--text-dim); +} + +.dim { + opacity: 0.4; +} + +/* ── Detail Page ── */ + +.doc-detail { + max-width: 780px; +} + +.doc-section { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px 24px; + margin-bottom: 16px; +} + +.doc-section h3 { + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); } .sig { - font-family: monospace; - white-space: pre; - font-size: 120%; + font-family: var(--font-mono); + font-size: 1.05rem; + white-space: pre-wrap; + line-height: 1.5; + background: var(--bg-code); + border-color: var(--border-strong); } -.params { +.sig div { + font-family: var(--font-mono); +} +.params div { + padding: 8px 0; + border-bottom: 1px solid var(--border); +} + +.params div:last-child { + border-bottom: none; } .params b { - font-family: monospace; - white-space: pre; + font-family: var(--font-mono); + color: var(--accent); + font-weight: 500; +} + +.see { + background: transparent; + border: none; + padding: 0; +} + +.see a { + font-family: var(--font-mono); + font-size: 0.92rem; +} + +.see .category { + margin-top: 8px; +} + +/* ── Forms ── */ + +form > div, label { + display: block; + padding: 6px 0; +} + +input, textarea, select { + background: var(--bg-inset); + border: 1px solid var(--border-strong); + border-radius: 6px; + color: var(--text); + transition: border-color 150ms var(--ease), background 150ms var(--ease); +} + +input[type=text], textarea { + padding: 10px 14px; + width: 100%; +} + +input[type=text]:hover, textarea:hover, +input[type=text]:focus, textarea:focus { + border-color: var(--accent); + outline: none; + background: rgba(0, 0, 0, 0.28); +} + +input[type=submit], button { + padding: 10px 20px; + font-weight: 600; + cursor: pointer; + background: var(--accent-dim); + border: 1px solid rgba(255, 214, 68, 0.25); + color: var(--accent); + border-radius: 6px; +} + +input[type=submit]:hover, button:hover { + background: rgba(255, 214, 68, 0.2); + border-color: var(--accent); + color: var(--accent-hover); +} + +textarea { + min-height: 120px; + resize: vertical; +} + +/* ── Code ── */ + +pre { + padding: 16px 20px; + overflow: auto; + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: var(--radius); + font-family: var(--font-mono); + font-size: 0.88rem; + line-height: 1.55; + white-space: pre-wrap; +} + +code { + font-family: var(--font-mono); + font-size: 0.9em; + padding: 2px 6px; + background: var(--bg-inset); + border-radius: 4px; +} + +pre code { + padding: 0; + background: none; +} + +/* ── Lists ── */ + +ul { + list-style: none; +} + +li { + padding: 2px 0; +} + +/* ── Details / Summary ── */ + +details { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 16px; +} + +summary { + padding: 14px 20px; + cursor: pointer; + font-weight: 600; + user-select: none; + transition: color 150ms var(--ease); +} + +summary:hover { + color: var(--accent); +} + +details[open] summary { + border-bottom: 1px solid var(--border); +} + +details pre { + border: none; + border-radius: 0 0 var(--radius) var(--radius); + margin: 0; +} + +/* ── Search ── */ + +.search-bar { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 24px; +} + +#doc-search { + flex: 1; + max-width: 480px; + padding: 11px 16px 11px 42px; + background: var(--bg-inset) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.35)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E") no-repeat 14px center; + border: 1px solid var(--border-strong); + border-radius: 8px; + color: var(--text); + font-family: var(--font-sans); + font-size: 0.95rem; + transition: border-color 150ms var(--ease), box-shadow 150ms var(--ease); +} + +#doc-search::placeholder { + color: var(--text-muted); +} + +#doc-search:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(255, 214, 68, 0.12); +} + +.search-count { + font-size: 0.82rem; + color: var(--text-muted); + white-space: nowrap; +} + +#search-results { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); + gap: 4px; +} + +#search-results .no-results { + grid-column: 1 / -1; + color: var(--text-dim); + padding: 12px 0; +} + +.search-hit { + display: flex; + flex-direction: column; + gap: 3px; +} + +.search-snippet { + font-size: 0.75rem; + color: var(--text-dim); + font-family: var(--font-mono); + padding: 0 12px 6px; + line-height: 1.4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ── Scrollbar ── */ + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.25); } diff --git a/site/examples/uce-starter/README.md b/site/examples/uce-starter/README.md index 2c9ab0e..991a46a 100644 --- a/site/examples/uce-starter/README.md +++ b/site/examples/uce-starter/README.md @@ -7,10 +7,18 @@ It keeps the PHP starter around as reference while mirroring the same broad stru - `index.uce` as the front controller - `views/` for routed page content - `components/` for reusable UI building blocks -- `themes/`, `js/`, and `img/` for static assets +- `themes/`, `js/`, and `img/` for theme templates and static assets This port intentionally leans on UCE's component layer rather than treating components as a compatibility shim. The page shell, nav/footer chrome, theme switcher, dashboard blocks, workspace primitives, and marketing sections are all rendered through `component()`. +Theme rendering is split the same way as the PHP starter: + +- `themes/common/page.blank.uce` +- `themes/common/page.json.uce` +- `themes//page.html.uce` + +Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` resolves the routed view, captures the `main` fragment, then hands off once into `themes/common/page.*.uce` or `themes//page.html.uce`, matching the PHP starter's page-layer flow without an extra shell implementation. + The example uses query-string routing in the same style as the PHP starter: - `index.uce` diff --git a/site/examples/uce-starter/components/data/widgets.uce b/site/examples/uce-starter/components/data/widgets.uce index a14928d..fa25082 100644 --- a/site/examples/uce-starter/components/data/widgets.uce +++ b/site/examples/uce-starter/components/data/widgets.uce @@ -139,7 +139,7 @@ COMPONENT:TIMESERIES_CHART(Request& context) yAxisLeftFormat: , yAxisRightFormat: }); - chart.setData(, ); + chart.setData(, ); window[] = chart; }()); @@ -205,7 +205,7 @@ COMPONENT:SORTABLE_TABLE(Request& context) @@ -219,8 +219,13 @@ COMPONENT:DATA_TABLE(Request& context) starter_register_css("js/ag-grid/ag-theme-alpine.css", context); DTree columns = context.call["columns"]; - if((columns.get_type_name() != "array" || columns["0"]["field"].to_string() == "") && - context.call["items"]["0"].get_type_name() == "array") + if(columns.get_type_name() != "array") + columns.set_array(); + bool has_explicit_columns = columns.is_list() && + columns._map.size() > 0 && + columns._map.find("0") != columns._map.end() && + columns["0"]["field"].to_string() != ""; + if(!has_explicit_columns && context.call["items"]["0"].get_type_name() == "array") { context.call["items"]["0"].each([&](DTree value, String key) { DTree col; @@ -232,14 +237,39 @@ COMPONENT:DATA_TABLE(Request& context) columns.push(col); }); } + if(!columns.is_list()) + { + DTree normalized_columns; + normalized_columns.set_array(); + columns.each([&](DTree column, String key) { + if(column.get_type_name() == "array") + normalized_columns.push(column); + }); + columns = normalized_columns; + } + + DTree row_data = context.call["items"]; + if(row_data.get_type_name() != "array") + row_data.set_array(); + if(!row_data.is_list()) + { + DTree normalized_rows; + normalized_rows.set_array(); + row_data.each([&](DTree row, String key) { + if(row.get_type_name() == "array") + normalized_rows.push(row); + }); + row_data = normalized_rows; + } DTree grid_options; grid_options["pagination"].set_bool(true); grid_options["paginationPageSize"] = first(context.call["options"]["paginationPageSize"].to_string(), "20"); + grid_options["paginationPageSizeSelector"].set_bool(false); grid_options["suppressMenuHide"].set_bool(true); grid_options["animateRows"].set_bool(true); grid_options["columnDefs"] = columns; - grid_options["rowData"] = context.call["items"]; + grid_options["rowData"] = row_data; <>
@@ -258,17 +288,19 @@ COMPONENT:DATA_TABLE(Request& context) diff --git a/site/examples/uce-starter/components/gauges/arcgauge.uce b/site/examples/uce-starter/components/gauges/arcgauge.uce new file mode 100644 index 0000000..a9b406a --- /dev/null +++ b/site/examples/uce-starter/components/gauges/arcgauge.uce @@ -0,0 +1,91 @@ +#load "helpers.uce" + +COMPONENT(Request& context) +{ + starter_register_js("components/gauges/common.js", context); + starter_register_css("themes/common/css/gauges.css", context); + + String gauge_id = first(context.call["id"].to_string(), "arcgauge-" + std::to_string((u64)time())); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + String style = context.call["style"].to_string(); + DTree scale = context.call["scale"]; + + <> +
+ +
+

+

+
+ +
+ 0.0 ? gauge_clamp_value((value / max_value) * 100.0, 0.0, 100.0) : 0.0; + f64 arc_length = round((pct / 100.0) * 157.08 * 10.0) / 10.0; + String resolved_color = ""; + if(merged["color"].get_type_name() == "array") + resolved_color = gauge_range_color(merged["color"], value); + else + resolved_color = merged["color"].to_string(); + if(resolved_color == "") + { + if(pct >= 85.0) + resolved_color = "var(--error, #ef4444)"; + else if(pct >= 60.0) + resolved_color = "var(--warning, #f59e0b)"; + else + resolved_color = "var(--success, #10b981)"; + } + String display_value = gauge_format_number(value, precision) + merged["unit"].to_string(); + String watermark_prefix = merged["watermark_prefix"].to_string(); + ?>
+
+ +
+
+
+
+ + + + +} diff --git a/site/examples/uce-starter/components/gauges/gauges.uce b/site/examples/uce-starter/components/gauges/gauges.uce deleted file mode 100644 index 6987abb..0000000 --- a/site/examples/uce-starter/components/gauges/gauges.uce +++ /dev/null @@ -1,231 +0,0 @@ -#load "../../lib/app.uce" - -COMPONENT:PROGRESSBAR(Request& context) -{ - starter_register_js("components/gauges/common.js", context); - starter_register_css("themes/common/css/gauges.css", context); - - String gauge_id = first(context.call["id"].to_string(), "progressbar-" + std::to_string((u64)time())); - String layout = first(context.call["layout"].to_string(), "horizontal"); - String style = context.call["style"].to_string(); - String item_style = context.call["item-style"].to_string(); - String value_style = context.call["value-style"].to_string(); - String bar_style = context.call["bar-style"].to_string(); - String title = context.call["title"].to_string(); - String subtitle = context.call["subtitle"].to_string(); - - <> -
- -
-

-

-
- - -
-
-
-
-
-
-
-
-
-
-
-
- -
px;"> -
-
-
-
-
-
-
-
-
- -
- - - - -} - -COMPONENT:NEEDLEGAUGE(Request& context) -{ - starter_register_css("themes/common/css/gauges.css", context); - String gauge_id = first(context.call["id"].to_string(), "needlegauge-" + std::to_string((u64)time())); - String style = context.call["style"].to_string(); - String title = context.call["title"].to_string(); - String subtitle = context.call["subtitle"].to_string(); - s32 size = (s32)int_val(first(context.call["size"].to_string(), "200")); - - <> -
-

-
-
-
-
- - - - - -
-
-
-
-
-
-
-
- - - - -} - -COMPONENT:ARCGAUGE(Request& context) -{ - starter_register_js("components/gauges/common.js", context); - starter_register_css("themes/common/css/gauges.css", context); - String gauge_id = first(context.call["id"].to_string(), "arcgauge-" + std::to_string((u64)time())); - String title = context.call["title"].to_string(); - String subtitle = context.call["subtitle"].to_string(); - String style = context.call["style"].to_string(); - - <> -
-

-
- 0.0 ? std::max(0.0, std::min(100.0, (value / max_value) * 100.0)) : 0.0; - f64 arc_length = (pct / 100.0) * 157.08; - char buf[64]; - snprintf(buf, sizeof(buf), ("%." + std::to_string(precision) + "f").c_str(), value); - ?>
-
- -
-
-
-
- - - - -} diff --git a/site/examples/uce-starter/components/gauges/helpers.uce b/site/examples/uce-starter/components/gauges/helpers.uce new file mode 100644 index 0000000..5fb6421 --- /dev/null +++ b/site/examples/uce-starter/components/gauges/helpers.uce @@ -0,0 +1,71 @@ +#load "../../lib/app.uce" +#include + +f64 gauge_clamp_value(f64 value, f64 min_value, f64 max_value) +{ + return(std::max(min_value, std::min(max_value, value))); +} + +f64 gauge_pi() +{ + return(3.14159265358979323846); +} + +String gauge_string_attr(String value) +{ + return(html_escape(value)); +} + +String gauge_attr_id(String value) +{ + return(ascii_safe_name(value)); +} + +String gauge_range_color(DTree ranges, f64 value) +{ + String matched = ""; + ranges.each([&](DTree range, String key) { + f64 from_value = float_val(first(range["from"].to_string(), "-999999999")); + f64 to_value = float_val(first(range["to"].to_string(), "999999999")); + if(value >= from_value && value <= to_value && matched == "") + matched = range["color"].to_string(); + }); + return(matched); +} + +String gauge_arc_path(f64 cx, f64 cy, f64 radius, f64 start_angle, f64 end_angle) +{ + f64 start_x = cx + cos(start_angle) * radius; + f64 start_y = cy + sin(start_angle) * radius; + f64 end_x = cx + cos(end_angle) * radius; + f64 end_y = cy + sin(end_angle) * radius; + s32 large_arc = fabs(end_angle - start_angle) > gauge_pi() ? 1 : 0; + s32 sweep = end_angle >= start_angle ? 1 : 0; + return( + "M " + std::to_string(start_x) + " " + std::to_string(start_y) + + " A " + std::to_string(radius) + " " + std::to_string(radius) + + " 0 " + std::to_string(large_arc) + " " + std::to_string(sweep) + + " " + std::to_string(end_x) + " " + std::to_string(end_y) + ); +} + +String gauge_circle_segment_svg(f64 cx, f64 cy, f64 radius, f64 start_angle, f64 end_angle, String stroke, f64 stroke_width, String fill, String style) +{ + String path_d = gauge_arc_path(cx, cy, radius, start_angle, end_angle); + return( + "" + ); +} + +String gauge_format_number(f64 value, s32 precision) +{ + char buf[64]; + snprintf(buf, sizeof(buf), ("%." + std::to_string(std::max(0, precision)) + "f").c_str(), value); + return(String(buf)); +} diff --git a/site/examples/uce-starter/components/gauges/needlegauge.uce b/site/examples/uce-starter/components/gauges/needlegauge.uce new file mode 100644 index 0000000..25ed3f1 --- /dev/null +++ b/site/examples/uce-starter/components/gauges/needlegauge.uce @@ -0,0 +1,144 @@ +#load "helpers.uce" + +COMPONENT(Request& context) +{ + starter_register_js("components/gauges/common.js", context); + starter_register_css("themes/common/css/gauges.css", context); + + String gauge_id = first(context.call["id"].to_string(), "needlegauge-" + std::to_string((u64)time())); + String style = context.call["style"].to_string(); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + f64 size = float_val(first(context.call["size"].to_string(), "200")); + DTree scale = context.call["scale"]; + f64 scale_angle_start = float_val(first(scale["angle_start"].to_string(), std::to_string(-gauge_pi()))); + f64 scale_angle_end = float_val(first(scale["angle_end"].to_string(), "0")); + f64 img_height = float_val(first(context.call["img_height"].to_string(), std::to_string(size * std::max(cos(scale_angle_start), cos(scale_angle_end))))); + + <> +
+ +
+

+

+
+ +
+ "; + } + for(f64 v = min_value; v <= max_value + 0.0001; v += label_interval) + { + f64 angle = angle_start + ((v - min_value) / vrange) * (angle_end - angle_start); + f64 x1 = size / 2.0 + cos(angle) * size * 0.35; + f64 y1 = size / 2.0 + sin(angle) * size * 0.35; + f64 x2 = size / 2.0 + cos(angle) * size * 0.41; + f64 y2 = size / 2.0 + sin(angle) * size * 0.41; + ticks_html += ""; + f64 lx = size / 2.0 + cos(angle) * size * 0.45; + f64 ly = size / 2.0 + sin(angle) * size * 0.45; + String label_value = std::to_string((s64)round(v)); + ticks_html += "" + + html_escape(label_value) + ""; + } + String segments_html = ""; + if(merged["color"].get_type_name() == "array") + { + merged["color"].each([&](DTree range, String key) { + f64 range_from = std::max(float_val(first(range["from"].to_string(), std::to_string(min_value))), min_value); + f64 range_to = std::min(float_val(first(range["to"].to_string(), std::to_string(max_value))), max_value); + f64 angle_from = angle_start + ((range_from - min_value) / vrange) * (angle_end - angle_start); + f64 angle_to = angle_start + ((range_to - min_value) / vrange) * (angle_end - angle_start); + segments_html += gauge_circle_segment_svg(size / 2.0, size / 2.0, size * 0.4, angle_from, angle_to, first(range["color"].to_string(), "rgba(120,120,120,0.5)"), 8.0, "rgba(0,0,0,0)", "opacity:0.25"); + }); + } + else + { + segments_html = gauge_circle_segment_svg(size / 2.0, size / 2.0, size * 0.4, angle_start, angle_end, tick_color, 8.0, "rgba(0,0,0,0)", "opacity:0.25"); + } + ?>
+
+
+ + + + + + +
+
+
+
+
+
+
+
+ + + + +} diff --git a/site/examples/uce-starter/components/gauges/progressbar.uce b/site/examples/uce-starter/components/gauges/progressbar.uce new file mode 100644 index 0000000..1410243 --- /dev/null +++ b/site/examples/uce-starter/components/gauges/progressbar.uce @@ -0,0 +1,168 @@ +#load "helpers.uce" + +COMPONENT(Request& context) +{ + starter_register_js("components/gauges/common.js", context); + starter_register_css("themes/common/css/gauges.css", context); + + String gauge_id = first(context.call["id"].to_string(), "progressbar-" + std::to_string((u64)time())); + String style = context.call["style"].to_string(); + String item_style = context.call["item-style"].to_string(); + String label_style = context.call["label-style"].to_string(); + String value_style = context.call["value-style"].to_string(); + String bar_style = context.call["bar-style"].to_string(); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + String layout = first(context.call["layout"].to_string(), "horizontal"); + String bar_color_default = context.call["bar-color"].to_string(); + DTree scale = context.call["scale"]; + DTree markers = context.call["markers"]; + + StringList default_palette; + default_palette.push_back("var(--success, #10b981)"); + default_palette.push_back("var(--primary, #60a5fa)"); + default_palette.push_back("var(--accent, #22d3ee)"); + default_palette.push_back("var(--warning, #f59e0b)"); + u32 auto_color_counter = 0; + + <> +
+ +
+

+

+
+ + +
+
+ +
+
+
+
+
+
+
" + style="left: %; background: ;">
+
+
+ +
+
+ +
px;"> +
+ +
+
+
+
" + style="bottom: %; background: ;">
+
+
+
+ +
+
+ +
+ + + + +} diff --git a/site/examples/uce-starter/components/theme/account_links.uce b/site/examples/uce-starter/components/theme/account_links.uce index 314e99e..96018c2 100644 --- a/site/examples/uce-starter/components/theme/account_links.uce +++ b/site/examples/uce-starter/components/theme/account_links.uce @@ -7,9 +7,9 @@ COMPONENT(Request& context) DTree user = users.current(); bool signed_in = user["email"].to_string() != ""; String theme_key = context.cfg.get_by_path("theme/key").to_string(); - String wrapper_class = theme_key == "localfirst" ? "admin-account-card" : "nav-account nav-menu"; - String links_class = theme_key == "localfirst" ? "admin-account-links" : ""; - String name_class = theme_key == "localfirst" ? "admin-account-name" : "account-name"; + String wrapper_class = first(context.call["wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-card" : "nav-account nav-menu"); + String links_class = first(context.call["links_wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-links" : ""); + String name_class = first(context.call["name_class"].to_string(), theme_key == "localfirst" ? "admin-account-name" : "account-name"); <>
diff --git a/site/examples/uce-starter/components/theme/footer.uce b/site/examples/uce-starter/components/theme/footer.uce index 7fa17b7..1a81513 100644 --- a/site/examples/uce-starter/components/theme/footer.uce +++ b/site/examples/uce-starter/components/theme/footer.uce @@ -3,18 +3,19 @@ COMPONENT(Request& context) { starter_boot(context); - if(context.var["starter"]["embed_mode"].to_string() != "") + bool embed_mode = starter_page_embed_mode(context); + if(embed_mode) return; String text = first(context.cfg.get_by_path("theme/footer_text").to_string(), context.cfg.get_by_path("site/name").to_string()); - String inner_class = context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : ""; + String inner_class = first(context.call["inner_class"].to_string(), context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : ""); <>

-

+

diff --git a/site/examples/uce-starter/components/theme/global_controls.uce b/site/examples/uce-starter/components/theme/global_controls.uce new file mode 100644 index 0000000..9af2162 --- /dev/null +++ b/site/examples/uce-starter/components/theme/global_controls.uce @@ -0,0 +1,18 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + if(starter_page_embed_mode(context)) + return; + + String cookie_value = context.call["cookie_consent"].to_string(); + bool cookie_consent = !(cookie_value == "0" || cookie_value == "false" || cookie_value == "FALSE" || cookie_value == "no" || cookie_value == "NO"); + + <> + + + + + +} diff --git a/site/examples/uce-starter/components/theme/head.uce b/site/examples/uce-starter/components/theme/head.uce new file mode 100644 index 0000000..77ddaa7 --- /dev/null +++ b/site/examples/uce-starter/components/theme/head.uce @@ -0,0 +1,22 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + String title = first(context.var["starter"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string()); + String description = first(context.cfg.get_by_path("theme/meta_description").to_string(), "UCE starter example"); + String theme_color = first(context.cfg.get_by_path("theme/theme_color").to_string(), "#0f172a"); + String icon = starter_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context); + + <> + + <?= title + " | " + context.cfg.get_by_path("site/name").to_string() ?> + + + + + + + + +} diff --git a/site/examples/uce-starter/components/theme/page_shell.uce b/site/examples/uce-starter/components/theme/page_shell.uce index b521acf..890f2c2 100644 --- a/site/examples/uce-starter/components/theme/page_shell.uce +++ b/site/examples/uce-starter/components/theme/page_shell.uce @@ -3,115 +3,18 @@ COMPONENT(Request& context) { starter_boot(context); - - String page_type = first(context.var["starter"]["page_type"].to_string(), "html"); - if(context.flags.status >= 300 && context.flags.status < 400) - return; - - if(page_type == "blank") - { - print(context.call["main_html"].to_string()); - return; - } - - if(page_type == "json") - { - context.header["Content-Type"] = "application/json"; - context.header["Cache-Control"] = "no-cache, no-store, must-revalidate"; - if(context.var["starter"]["json"].get_type_name() == "array") - { - print(json_encode(context.var["starter"]["json"])); - } - else - { - print(context.call["main_html"].to_string()); - } - return; - } - - String theme_key = context.cfg.get_by_path("theme/key").to_string(); - bool embed_mode = context.var["starter"]["embed_mode"].to_string() == "1"; - String body_class = embed_mode ? "embed-mode" : ""; - if(theme_key == "portal-dark") - body_class = "portal-theme portal-dark-theme dark-theme" + String(embed_mode ? " embed-mode" : ""); - else if(theme_key == "portal-light") - body_class = "portal-theme portal-light-theme" + String(embed_mode ? " embed-mode" : ""); - else if(theme_key == "localfirst") - body_class = "admin-page localfirst-theme dark-theme" + String(embed_mode ? " embed-mode" : ""); - else if(theme_key == "retro-gaming") - body_class = embed_mode ? "embed-mode" : ""; - - <> - - "> - - - - > -
- - - - - - - - -
- -
- -
- -
- -
- -
-
-
- - -
> - -
- - - - - + if(context.call["main_html"].to_string() != "") + context.var["starter"]["fragments"]["main"] = context.call["main_html"]; + if(context.call["json"].get_type_name() == "array") + context.var["starter"]["json"] = context.call["json"]; + if(context.call["page_type"].to_string() != "") + context.var["starter"]["page_type"] = context.call["page_type"]; + if(context.call["embed_mode"].to_string() != "") + context.var["starter"]["embed_mode"] = context.call["embed_mode"]; + starter_render_page(context); } COMPONENT:HEAD(Request& context) { - starter_boot(context); - String title = first(context.var["starter"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string()); - String description = first(context.cfg.get_by_path("theme/meta_description").to_string(), "UCE starter example"); - String theme_color = first(context.cfg.get_by_path("theme/theme_color").to_string(), "#0f172a"); - String icon = starter_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context); - - <> - - <?= title + " | " + context.cfg.get_by_path("site/name").to_string() ?> - - - - - - - - + print(component("head", context.call, context)); } diff --git a/site/examples/uce-starter/components/theme/standard_nav.uce b/site/examples/uce-starter/components/theme/standard_nav.uce index 227f2fe..587548a 100644 --- a/site/examples/uce-starter/components/theme/standard_nav.uce +++ b/site/examples/uce-starter/components/theme/standard_nav.uce @@ -3,9 +3,20 @@ COMPONENT(Request& context) { starter_boot(context); + String nav_class = context.call["nav_class"].to_string(); + if(nav_class == "" && (context.cfg.get_by_path("theme/key").to_string() == "portal-dark" || context.cfg.get_by_path("theme/key").to_string() == "dark" || context.cfg.get_by_path("theme/key").to_string() == "retro-gaming")) + nav_class = "nav-shell"; + + DTree account_props; + if(context.call["account_wrapper_class"].to_string() != "") + account_props["wrapper_class"] = context.call["account_wrapper_class"]; + if(context.call["links_wrapper_class"].to_string() != "") + account_props["links_wrapper_class"] = context.call["links_wrapper_class"]; + if(context.call["name_class"].to_string() != "") + account_props["name_class"] = context.call["name_class"]; <> - > + > - + } diff --git a/site/examples/uce-starter/index.uce b/site/examples/uce-starter/index.uce index 8e018d2..43e15f0 100644 --- a/site/examples/uce-starter/index.uce +++ b/site/examples/uce-starter/index.uce @@ -11,7 +11,7 @@ RENDER(Request& context) { if(resolved["param"].to_string() != "") context.var["starter"]["route"]["param"] = resolved["param"]; - render_file(resolved["file"].to_string(), context); + unit_render(resolved["file"].to_string(), context); } else { @@ -24,8 +24,6 @@ RENDER(Request& context) } String main_html = ob_get_close(); - - DTree shell_props; - shell_props["main_html"] = main_html; - render_component("components/theme/page_shell", shell_props, context); + context.var["starter"]["fragments"]["main"] = main_html; + starter_render_page(context); } diff --git a/site/examples/uce-starter/js/site.js b/site/examples/uce-starter/js/site.js index 2c311c4..b347640 100755 --- a/site/examples/uce-starter/js/site.js +++ b/site/examples/uce-starter/js/site.js @@ -1,9 +1,19 @@ var enable_debug = true; +var starterReady = (typeof $ !== 'undefined' && $.ready) + ? $.ready.bind($) + : function(callback) { + if (document.readyState !== 'loading') { + callback(); + } else { + document.addEventListener('DOMContentLoaded', callback); + } + }; + var UI = { smoothScrollToNamedAnchors: function() { - $('a[href^="#"]').each(anchor => { + document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); @@ -18,7 +28,8 @@ var UI = { }, enablePageTransitions: function() { - $(document.body).append(``); + }`; + document.body.appendChild(style); document.addEventListener('click', e => { const link = e.target.closest('a[href]'); @@ -62,4 +74,4 @@ var UI = { } -$.ready(UI.init); +starterReady(UI.init); diff --git a/site/examples/uce-starter/lib/app.uce b/site/examples/uce-starter/lib/app.uce index ccfb01b..4f3c4bd 100644 --- a/site/examples/uce-starter/lib/app.uce +++ b/site/examples/uce-starter/lib/app.uce @@ -4,7 +4,7 @@ String starter_fs_root(Request& context) { String root = context.var["starter"]["fs_root"].to_string(); if(root == "") - root = get_cwd(); + root = cwd_get(); return(root); } @@ -276,13 +276,89 @@ String starter_html_class(Request& context) return(classes); } +String starter_page_main_html(Request& context) +{ + String main_html = context.call["main_html"].to_string(); + if(main_html == "") + main_html = context.var["starter"]["fragments"]["main"].to_string(); + return(main_html); +} + +bool starter_bool_value(DTree value, bool fallback = false) +{ + String raw = trim(value.to_string()); + if(raw == "") + return(fallback); + return( + raw != "0" && + raw != "false" && + raw != "FALSE" && + raw != "False" && + raw != "(false)" && + raw != "no" && + raw != "NO" && + raw != "No" + ); +} + +bool starter_request_embed_mode(Request& context) +{ + return(starter_bool_value(context.var["starter"]["embed_mode"])); +} + +bool starter_page_embed_mode(Request& context) +{ + String embed_value = context.call["embed_mode"].to_string(); + if(embed_value != "") + return(starter_bool_value(context.call["embed_mode"])); + return(starter_request_embed_mode(context)); +} + +String starter_theme_page_component(Request& context) +{ + String page_type = first(context.var["starter"]["page_type"].to_string(), "html"); + if(page_type == "blank") + return("themes/common/page.blank.uce"); + if(page_type == "json") + return("themes/common/page.json.uce"); + + String theme_path = context.cfg.get_by_path("theme/path").to_string(); + if(theme_path == "") + return(""); + if(theme_path[theme_path.length() - 1] != '/') + theme_path.append(1, '/'); + return(theme_path + "page.html.uce"); +} + +void starter_render_page(Request& context) +{ + if(context.flags.status >= 300 && context.flags.status < 400) + return; + + DTree page_props; + page_props["main_html"] = context.var["starter"]["fragments"]["main"]; + if(context.var["starter"]["json"].get_type_name() == "array") + page_props["json"] = context.var["starter"]["json"]; + + String page_component = starter_theme_page_component(context); + if(page_component != "" && file_exists(page_component)) + { + print(component(page_component, page_props, context)); + return; + } + + context.set_status(500, "Internal Server Error"); + context.header["Content-Type"] = "text/plain; charset=utf-8"; + print("UCE Starter is missing the page template component: " + page_component); +} + void starter_boot(Request& context) { if(context.var["starter"]["booted"].to_string() == "1") return; context.var["starter"]["booted"] = "1"; - context.var["starter"]["fs_root"] = get_cwd(); + context.var["starter"]["fs_root"] = cwd_get(); context.var["starter"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]); String base_url = dirname(context.var["starter"]["script_url"].to_string()); diff --git a/site/examples/uce-starter/lib/user.class.h b/site/examples/uce-starter/lib/user.class.h index 70ee64d..61ff4ab 100644 --- a/site/examples/uce-starter/lib/user.class.h +++ b/site/examples/uce-starter/lib/user.class.h @@ -102,7 +102,7 @@ public: return(result); } - String salt = gen_sha1(std::to_string((u64)time()) + ":" + std::to_string((u64)(microtime() * 1000000.0)) + ":" + make_session_id()).substr(0, 24); + String salt = gen_sha1(std::to_string((u64)time()) + ":" + std::to_string((u64)(time_precise() * 1000000.0)) + ":" + session_id_create()).substr(0, 24); DTree user; user["email"] = email; user["salt"] = salt; diff --git a/site/examples/uce-starter/themes/common/page.blank.php b/site/examples/uce-starter/themes/common/page.blank.php deleted file mode 100755 index 92858e3..0000000 --- a/site/examples/uce-starter/themes/common/page.blank.php +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/site/examples/uce-starter/themes/common/page.blank.uce b/site/examples/uce-starter/themes/common/page.blank.uce new file mode 100644 index 0000000..40d48f7 --- /dev/null +++ b/site/examples/uce-starter/themes/common/page.blank.uce @@ -0,0 +1,7 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + print(starter_page_main_html(context)); +} diff --git a/site/examples/uce-starter/themes/common/page.json.php b/site/examples/uce-starter/themes/common/page.json.php deleted file mode 100755 index 89fe5e2..0000000 --- a/site/examples/uce-starter/themes/common/page.json.php +++ /dev/null @@ -1,12 +0,0 @@ - - - - - -> - - false, - 'account_wrapper_class' => 'nav-account nav-menu', - )); ?> -
> - -
- $embed_mode)); ?> - - - diff --git a/site/examples/uce-starter/themes/dark/page.html.uce b/site/examples/uce-starter/themes/dark/page.html.uce new file mode 100644 index 0000000..8645bb0 --- /dev/null +++ b/site/examples/uce-starter/themes/dark/page.html.uce @@ -0,0 +1,31 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree nav_props; + nav_props["account_wrapper_class"] = "nav-account nav-menu"; + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + > + + +
> + +
+ + + + +} diff --git a/site/examples/uce-starter/themes/light/page.html.php b/site/examples/uce-starter/themes/light/page.html.php deleted file mode 100755 index 1df8f11..0000000 --- a/site/examples/uce-starter/themes/light/page.html.php +++ /dev/null @@ -1,19 +0,0 @@ - - - - - -> - - false)); ?> -
> - -
- $embed_mode)); ?> - - - - diff --git a/site/examples/uce-starter/themes/light/page.html.uce b/site/examples/uce-starter/themes/light/page.html.uce new file mode 100644 index 0000000..495d3e0 --- /dev/null +++ b/site/examples/uce-starter/themes/light/page.html.uce @@ -0,0 +1,28 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + > + + +
> + +
+ + + + +} diff --git a/site/examples/uce-starter/themes/localfirst/page.html.php b/site/examples/uce-starter/themes/localfirst/page.html.php deleted file mode 100644 index 6c9db4d..0000000 --- a/site/examples/uce-starter/themes/localfirst/page.html.php +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - false)); ?> -
- -
-
- 'admin-account-card', - 'links_wrapper_class' => 'admin-account-links', - 'name_class' => 'admin-account-name', - )); ?> -
-
- -
-
-
- - - \ No newline at end of file diff --git a/site/examples/uce-starter/themes/localfirst/page.html.uce b/site/examples/uce-starter/themes/localfirst/page.html.uce new file mode 100644 index 0000000..5a7771c --- /dev/null +++ b/site/examples/uce-starter/themes/localfirst/page.html.uce @@ -0,0 +1,54 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + String current_path = context.var["starter"]["route"]["l_path"].to_string(); + + DTree global_props; + global_props["cookie_consent"].set_bool(false); + + DTree account_props; + account_props["wrapper_class"] = "admin-account-card"; + account_props["links_wrapper_class"] = "admin-account-links"; + account_props["name_class"] = "admin-account-name"; + + <> + + "> + + + + "> + +
+ +
+ +
+ +
+ +
+ +
+
+
+ + + +} diff --git a/site/examples/uce-starter/themes/portal-dark/page.html.php b/site/examples/uce-starter/themes/portal-dark/page.html.php deleted file mode 100644 index ae86edd..0000000 --- a/site/examples/uce-starter/themes/portal-dark/page.html.php +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - 'nav-account nav-menu')); ?> -
> - -
- $embed_mode)); ?> - - - \ No newline at end of file diff --git a/site/examples/uce-starter/themes/portal-dark/page.html.uce b/site/examples/uce-starter/themes/portal-dark/page.html.uce new file mode 100644 index 0000000..3f353a7 --- /dev/null +++ b/site/examples/uce-starter/themes/portal-dark/page.html.uce @@ -0,0 +1,31 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree nav_props; + nav_props["account_wrapper_class"] = "nav-account nav-menu"; + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + "> + + +
> + +
+ + + + +} diff --git a/site/examples/uce-starter/themes/portal-light/page.html.php b/site/examples/uce-starter/themes/portal-light/page.html.php deleted file mode 100644 index 0382b0f..0000000 --- a/site/examples/uce-starter/themes/portal-light/page.html.php +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - -
> - -
- $embed_mode, 'inner_class' => 'footer-inner')); ?> - - - \ No newline at end of file diff --git a/site/examples/uce-starter/themes/portal-light/page.html.uce b/site/examples/uce-starter/themes/portal-light/page.html.uce new file mode 100644 index 0000000..d125df0 --- /dev/null +++ b/site/examples/uce-starter/themes/portal-light/page.html.uce @@ -0,0 +1,29 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + footer_props["inner_class"] = "footer-inner"; + + <> + + "> + + + + "> + + +
> + +
+ + + + +} diff --git a/site/examples/uce-starter/themes/retro-gaming/page.html.php b/site/examples/uce-starter/themes/retro-gaming/page.html.php deleted file mode 100644 index acf2c11..0000000 --- a/site/examples/uce-starter/themes/retro-gaming/page.html.php +++ /dev/null @@ -1,22 +0,0 @@ - - - - - -> -
- - false, - 'account_wrapper_class' => 'nav-account nav-menu', - )); ?> -
> - -
- $embed_mode)); ?> - - - diff --git a/site/examples/uce-starter/themes/retro-gaming/page.html.uce b/site/examples/uce-starter/themes/retro-gaming/page.html.uce new file mode 100644 index 0000000..c0a53da --- /dev/null +++ b/site/examples/uce-starter/themes/retro-gaming/page.html.uce @@ -0,0 +1,32 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree nav_props; + nav_props["account_wrapper_class"] = "nav-account nav-menu"; + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + > +
+ + +
> + +
+ + + + +} diff --git a/site/examples/uce-starter/views/account/profile.uce b/site/examples/uce-starter/views/account/profile.uce index b748397..17fbc14 100644 --- a/site/examples/uce-starter/views/account/profile.uce +++ b/site/examples/uce-starter/views/account/profile.uce @@ -21,7 +21,7 @@ RENDER(Request& context)

Profile

Email:

Roles:

-

Created:

+

Created:

">Logout

} diff --git a/site/examples/uce-starter/views/gauges.uce b/site/examples/uce-starter/views/gauges.uce index 7b7e656..1c6bf73 100644 --- a/site/examples/uce-starter/views/gauges.uce +++ b/site/examples/uce-starter/views/gauges.uce @@ -5,6 +5,7 @@ RENDER(Request& context) starter_boot(context); context.var["starter"]["page_title"] = "Gauges"; starter_register_css("themes/common/css/gauges.css", context); + f64 pi = 3.14159265358979323846; DTree props; DTree item; @@ -20,10 +21,19 @@ RENDER(Request& context) props["layout"] = "horizontal"; props["style"] = "flex:1 1 24rem"; props["listen"].set_bool(true); - item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; item["tooltip"] = "CPU Usage"; props["items"]["cpu"] = item; item.clear(); + props["markers"]["zero"]["value"] = "0"; + props["markers"]["zero"]["label"] = "Zero"; + props["markers"]["zero"]["color"] = "var(--bg-color)"; + props["markers"]["high"]["value"] = "100"; + props["markers"]["high"]["label"] = "High"; + item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; item["tooltip"] = "CPU Usage"; + item["color"]["0"]["from"] = "-50"; item["color"]["0"]["to"] = "20"; item["color"]["0"]["color"] = "var(--text-muted)"; + item["color"]["1"]["from"] = "20"; item["color"]["1"]["to"] = "80"; item["color"]["1"]["color"] = "var(--primary)"; + item["color"]["2"]["from"] = "80"; item["color"]["2"]["to"] = "100"; item["color"]["2"]["color"] = "var(--warning)"; + props["items"]["cpu"] = item; item.clear(); item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "Memory"; item["tooltip"] = "Memory Usage"; props["items"]["memory"] = item; item.clear(); item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "Disk I/O"; item["tooltip"] = "Disk I/O"; props["items"]["disk"] = item; - print(component("../components/gauges/gauges:PROGRESSBAR", props, context)); + print(component("../components/gauges/progressbar", props, context)); props.clear(); props["id"] = "vertical_demo"; @@ -33,10 +43,15 @@ RENDER(Request& context) props["style"] = "flex:1 1 24rem"; props["height"] = "350"; props["listen"].set_bool(true); - item.clear(); item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; props["items"]["cpu"] = item; - item.clear(); item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "RAM"; props["items"]["memory"] = item; - item.clear(); item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "I/O"; props["items"]["disk"] = item; - print(component("../components/gauges/gauges:PROGRESSBAR", props, context)); + props["markers"]["zero"]["value"] = "0"; + props["markers"]["zero"]["label"] = "Zero"; + props["markers"]["zero"]["color"] = "var(--bg-color)"; + props["markers"]["high"]["value"] = "100"; + props["markers"]["high"]["label"] = "High"; + item.clear(); item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; item["tooltip"] = "CPU Usage"; props["items"]["cpu"] = item; + item.clear(); item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "RAM"; item["tooltip"] = "Memory Usage"; props["items"]["memory"] = item; + item.clear(); item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "I/O"; item["tooltip"] = "Disk I/O"; props["items"]["disk"] = item; + print(component("../components/gauges/progressbar", props, context)); <>
@@ -54,13 +69,34 @@ RENDER(Request& context) props.clear(); props["id"] = "needle_demo"; props["title"] = "Needle Gauge"; - props["subtitle"] = "The original analog gauge now uses the same elevated panels and theme-token palette as the arc gauges."; + props["subtitle"] = "The original analog gauge now uses the same elevated panels, typography, and theme-token palette as the arc gauges."; props["style"] = "flex:1 1 24rem"; props["listen"].set_bool(true); - item.clear(); item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; item["tooltip"] = "CPU Usage"; props["items"]["cpu"] = item; - item.clear(); item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "Memory"; item["tooltip"] = "Memory Usage"; props["items"]["memory"] = item; - item.clear(); item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "Disk I/O"; item["tooltip"] = "Disk I/O"; props["items"]["disk"] = item; - print(component("../components/gauges/gauges:NEEDLEGAUGE", props, context)); + props["label"] = "CPU"; + props["tooltip"] = "CPU Usage"; + props["scale"]["angle_start"] = std::to_string(-1.2 * pi); + props["scale"]["angle_end"] = std::to_string(0.2 * pi); + props["scale"]["max"] = "100"; + props["scale"]["unit"] = "%"; + props["scale"]["ticks-every"] = "10"; + props["scale"]["value-labels-every"] = "20"; + props["scale"]["tick-color"] = "var(--text-muted)"; + props["scale"]["color"]["0"]["from"] = "-50"; + props["scale"]["color"]["0"]["to"] = "10"; + props["scale"]["color"]["0"]["color"] = "var(--primary)"; + props["scale"]["color"]["1"]["from"] = "10"; + props["scale"]["color"]["1"]["to"] = "70"; + props["scale"]["color"]["1"]["color"] = "var(--text-muted)"; + props["scale"]["color"]["2"]["from"] = "70"; + props["scale"]["color"]["2"]["to"] = "90"; + props["scale"]["color"]["2"]["color"] = "var(--warning)"; + props["scale"]["color"]["3"]["from"] = "90"; + props["scale"]["color"]["3"]["to"] = "200"; + props["scale"]["color"]["3"]["color"] = "var(--error)"; + item.clear(); item["max"] = "200"; item["value"] = "45"; item["label"] = "CPU"; props["items"]["cpu"] = item; + item.clear(); item["value"] = "92"; item["min"] = "-50"; item["label"] = "Memory"; item["tooltip"] = "Memory Usage"; props["items"]["memory"] = item; + item.clear(); item["value"] = "28"; item["label"] = "Disk I/O"; item["tooltip"] = "Disk I/O"; props["items"]["disk"] = item; + print(component("../components/gauges/needlegauge", props, context)); <>
@@ -70,13 +106,17 @@ RENDER(Request& context) props.clear(); props["id"] = "arc_demo"; props["title"] = "SVG Arc Gauges"; - props["subtitle"] = "Backported from the llm2 overview as reusable KPI-style gauges."; + props["subtitle"] = "Backported from the llm2 overview as reusable KPI-style gauges with optional watermark tracking."; props["style"] = "flex: 2 1 36rem"; props["listen"].set_bool(true); - item.clear(); item["label"] = "System Load"; item["value"] = "1.8"; item["max"] = "8"; item["precision"] = "1"; item["caption"] = "LOAD 1M"; item["meta"] = "4 cores available"; props["items"]["load"] = item; - item.clear(); item["label"] = "Memory"; item["value"] = "62"; item["max"] = "100"; item["precision"] = "0"; item["unit"] = "%"; item["caption"] = "MEMORY"; item["meta"] = "9.9 / 16 GB"; props["items"]["memory_arc"] = item; - item.clear(); item["label"] = "Network"; item["value"] = "18"; item["max"] = "100"; item["precision"] = "0"; item["unit"] = " MB/s"; item["caption"] = "THROUGHPUT"; item["meta"] = "Inbound + outbound"; props["items"]["network"] = item; - print(component("../components/gauges/gauges:ARCGAUGE", props, context)); + item.clear(); item["label"] = "System Load"; item["value"] = "1.8"; item["max"] = "8"; item["precision"] = "1"; item["caption"] = "LOAD 1M"; item["meta"] = "4 cores available"; item["watermark_prefix"] = "loadDemo"; + item["color"]["0"]["from"] = "0"; item["color"]["0"]["to"] = "3.5"; item["color"]["0"]["color"] = "var(--success, #10b981)"; + item["color"]["1"]["from"] = "3.5"; item["color"]["1"]["to"] = "6"; item["color"]["1"]["color"] = "var(--warning, #f59e0b)"; + item["color"]["2"]["from"] = "6"; item["color"]["2"]["to"] = "8"; item["color"]["2"]["color"] = "var(--error, #ef4444)"; + props["items"]["load"] = item; + item.clear(); item["label"] = "Memory"; item["value"] = "62"; item["max"] = "100"; item["precision"] = "0"; item["unit"] = "%"; item["caption"] = "MEMORY"; item["meta"] = "9.9 / 16 GB"; item["watermark_prefix"] = "memoryDemo"; props["items"]["memory_arc"] = item; + item.clear(); item["label"] = "Network"; item["value"] = "18"; item["max"] = "100"; item["precision"] = "0"; item["unit"] = " MB/s"; item["caption"] = "THROUGHPUT"; item["meta"] = "Inbound + outbound"; item["watermark_prefix"] = "networkDemo"; props["items"]["network"] = item; + print(component("../components/gauges/arcgauge", props, context)); <>
diff --git a/site/test/call_file.uce b/site/test/call_file.uce index c74dd25..bb6cf87 100644 --- a/site/test/call_file.uce +++ b/site/test/call_file.uce @@ -7,12 +7,12 @@ RENDER(Request& context)

UCE Test: - call_file() + unit_call()

diff --git a/site/test/call_file_funcs.uce b/site/test/call_file_funcs.uce index f8aa873..84026d7 100644 --- a/site/test/call_file_funcs.uce +++ b/site/test/call_file_funcs.uce @@ -1,6 +1,7 @@ -// Minimal exported helper used by the `call_file()` demo page. +// Minimal exported helper used by the `unit_call()` demo page. -EXPORT void test_func() +EXPORT DTree* test_func(DTree* call_param) { print("HELLO FROM TEST FUNCTION"); + return(0); } diff --git a/site/test/components.uce b/site/test/components.uce index 0775cbb..9de84c4 100644 --- a/site/test/components.uce +++ b/site/test/components.uce @@ -28,6 +28,6 @@ RENDER(Request& context)

Named Component

- + } diff --git a/site/test/components/card.uce b/site/test/components/card.uce index b322a34..4ffd73f 100644 --- a/site/test/components/card.uce +++ b/site/test/components/card.uce @@ -3,8 +3,8 @@ COMPONENT(Request& context) { <>
- - + +
} diff --git a/site/test/file_append.uce b/site/test/file_append.uce index f0e1f2b..442fe6b 100644 --- a/site/test/file_append.uce +++ b/site/test/file_append.uce @@ -20,7 +20,7 @@ RENDER(Request& context) if(context.get["cmd"] == "clear") file_put_contents("/tmp/test.txt", ""); - file_append("/tmp/test.txt", context.server->request_count, "\thello world\t", 2, "\t", time(), "\t", microtime(), "\n"); + file_append("/tmp/test.txt", context.server->request_count, "\thello world\t", 2, "\t", time(), "\t", time_precise(), "\n"); print(file_get_contents("/tmp/test.txt")); diff --git a/site/test/index.uce b/site/test/index.uce index 0d34f48..7323524 100644 --- a/site/test/index.uce +++ b/site/test/index.uce @@ -1,57 +1,81 @@ +void render_card(String url, String title, String desc) +{ + <> + + + +} + RENDER(Request& context) { DTree p; p.set(context.params); - <> + <> + + + +

- UCE Test: - Index + UCE Test Suite + API Docs →

- -
str().length(), " \n");
-			print("Request #", context.server->request_count, "\n");
-		?>
-
-
str().length(), " \n"); - ?>
- +
+
Basics
+ + + + - //context.flags.log_request = false; +
Data Types & Parsing
+ + + + + + + + +
HTTP & Forms
+ + + + + + +
Storage & I/O
+ + + + + + +
Advanced
+ + + + + + + +
+
+

System Info

+
str().length(), "\n");
+				print("Request #", context.server->request_count, "\n");
+			?>
+
+
+ Request Parameters +
+
+ + } diff --git a/site/test/parse_time.uce b/site/test/parse_time.uce index ecdfbf5..5bd87a1 100644 --- a/site/test/parse_time.uce +++ b/site/test/parse_time.uce @@ -8,7 +8,7 @@ RENDER(Request& context)

UCE Test: - parse_time + time_parse

@@ -21,7 +21,7 @@ RENDER(Request& context)
diff --git a/site/test/session.uce b/site/test/session.uce index 487d538..83d7754 100644 --- a/site/test/session.uce +++ b/site/test/session.uce @@ -22,7 +22,7 @@ RENDER(Request& context) } else if(action == "store") { - context.session["stored-value"] = make_session_id(); + context.session["stored-value"] = session_id_create(); print("action: storing value "+context.session["stored-value"]); } else if(action == "destroy") diff --git a/site/test/style.css b/site/test/style.css index 1ac1b4f..9b27be3 100644 --- a/site/test/style.css +++ b/site/test/style.css @@ -1,82 +1,382 @@ +/* UCE Test Suite — Theme + Color palette: #139 deep-blue base · white text · yellow accents */ + +:root { + --bg: #113399; + --bg-surface: rgba(255, 255, 255, 0.065); + --bg-surface-hover: rgba(255, 255, 255, 0.11); + --bg-inset: rgba(0, 0, 0, 0.22); + --bg-code: rgba(0, 0, 0, 0.28); + --text: #e8ecf4; + --text-dim: rgba(255, 255, 255, 0.55); + --text-muted: rgba(255, 255, 255, 0.35); + --accent: #ffd644; + --accent-dim: rgba(255, 214, 68, 0.1); + --accent-hover: #ffe680; + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.18); + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace; + --radius: 8px; + --radius-lg: 14px; + --ease: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ── Reset ── */ + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + * { font-family: inherit; font-size: inherit; - box-sizing: inherit; color: inherit; line-height: inherit; } -a { - color: yellow; -} +/* ── Base ── */ -h1 { - font-size: 200%; - padding-top: 8px; - padding-bottom: 8px; - font-family: monospace; +html { + font-size: 15px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } body { - max-width: 1024px; - margin-left: auto; - margin-right: auto; - padding-left: 16px; - padding-right: 16px; - font-family: Tahoma, Helvetica, Arial; - font-size: 1.2em; - box-sizing: border-box; - background: #139; - color: white; - line-height: 150%; + max-width: 1100px; + margin: 0 auto; + padding: 0 24px 64px; + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.65; + background: var(--bg); + color: var(--text); } -body > * { - background: rgba(255,255,255,0.1); - padding: 32px; - margin: 16px; +/* ── Typography ── */ + +h1 { + font-family: var(--font-mono); + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.02em; + padding: 28px 0 20px; + border-bottom: 1px solid var(--border); + margin-bottom: 28px; } +h1 a { + text-decoration: none; + color: var(--text); + transition: color 150ms var(--ease); +} + +h1 a:hover { + color: var(--accent); +} + +h2 { + font-family: var(--font-mono); + font-size: 1.15rem; + font-weight: 600; + letter-spacing: -0.01em; + margin: 24px 0 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); + color: var(--text-dim); +} + +h3 { + font-family: var(--font-mono); + font-size: 0.82rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-dim); + margin: 0 0 10px; +} + +a { + color: var(--accent); + text-decoration: none; + transition: color 150ms var(--ease); +} + +a:hover { + color: var(--accent-hover); + text-decoration: underline; + text-underline-offset: 2px; +} + +/* ── Page sections ── */ + +body > section, +body > div:not(.test-grid):not(.system-info):not(.grid-heading), +body > p { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px 24px; + margin-bottom: 16px; +} + +/* ── Test Card Grid ── */ + +.test-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 10px; + margin-bottom: 24px; +} + +.test-card { + display: flex; + flex-direction: column; + gap: 4px; + padding: 16px 20px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + text-decoration: none; + color: var(--text); + transition: all 150ms var(--ease); +} + +.test-card:hover { + background: var(--bg-surface-hover); + border-color: var(--border-strong); + text-decoration: none; + transform: translateY(-1px); +} + +.test-card strong { + font-family: var(--font-mono); + font-size: 0.92rem; + color: var(--accent); +} + +.test-card span { + font-size: 0.82rem; + color: var(--text-dim); + line-height: 1.4; +} + +.grid-heading { + grid-column: 1 / -1; + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + margin: 0; + padding: 14px 0 2px; + border: none; + background: none; +} + +.grid-heading:first-child { + padding-top: 0; +} + +.docs-link { + float: right; + font-size: 0.55em; + font-weight: 400; + opacity: 0.5; + transition: opacity 150ms var(--ease); +} + +.docs-link:hover { + opacity: 1; +} + +.dim { + opacity: 0.4; +} + +/* ── Forms ── */ + form > div, label { display: block; - padding-top: 8px; - padding-bottom: 8px; + padding: 6px 0; } -input, textarea { - background: rgba(0,0,0,0.2); - border: 2px solid rgba(255,255,255,0.2); +label { + font-size: 0.88rem; + font-weight: 600; + color: var(--text-dim); } -input[type=submit], button { - padding: 8px; - cursor: pointer; -} - -input[type=submit]:hover, button:hover { - color: yellow; - background: rgba(0,0,0,0.5); +input, textarea, select { + background: var(--bg-inset); + border: 1px solid var(--border-strong); + border-radius: 6px; + color: var(--text); + font-family: var(--font-sans); + transition: border-color 150ms var(--ease), background 150ms var(--ease); } input[type=text], textarea { - padding: 8px; + padding: 10px 14px; width: 100%; } -input[type=text]:hover, textarea:hover { - background: rgba(0,0,0,0.25); +input[type=text]:hover, textarea:hover, +input[type=text]:focus, textarea:focus { + border-color: var(--accent); + outline: none; + background: rgba(0, 0, 0, 0.28); +} + +input[type=submit], button { + padding: 10px 20px; + font-weight: 600; + cursor: pointer; + background: var(--accent-dim); + border: 1px solid rgba(255, 214, 68, 0.25); + color: var(--accent); + border-radius: 6px; +} + +input[type=submit]:hover, button:hover { + background: rgba(255, 214, 68, 0.2); + border-color: var(--accent); + color: var(--accent-hover); } textarea { - height: 20%; + min-height: 120px; + resize: vertical; } +/* ── Code / Output ── */ + pre { - height: 20%; + padding: 16px 20px; overflow: auto; - padding: 8px; - border: 2px solid rgba(0,0,0,0.2); - background: rgba(255,255,255,0.1); - font-family: monospace; + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: var(--radius); + font-family: var(--font-mono); + font-size: 0.85rem; + line-height: 1.55; white-space: pre-wrap; } + +code { + font-family: var(--font-mono); + font-size: 0.9em; + padding: 2px 6px; + background: var(--bg-inset); + border-radius: 4px; +} + +pre code { + padding: 0; + background: none; +} + +/* ── Lists ── */ + +ul { + list-style: none; + padding: 0; +} + +li { + padding: 4px 0; +} + +li a { + font-family: var(--font-mono); + font-size: 0.92rem; +} + +/* ── System Info ── */ + +.system-info { + margin-top: 32px; + padding: 16px 20px; + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.system-info h3 { + margin-bottom: 12px; +} + +.system-info pre { + border: none; + padding: 0; + background: transparent; + margin: 0; + font-size: 0.82rem; +} + +/* ── Details / Summary ── */ + +details { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 16px; +} + +summary { + padding: 14px 20px; + cursor: pointer; + font-weight: 600; + user-select: none; + transition: color 150ms var(--ease); +} + +summary:hover { + color: var(--accent); +} + +details[open] summary { + border-bottom: 1px solid var(--border); +} + +details pre { + border: none; + border-radius: 0 0 var(--radius) var(--radius); + margin: 0; +} + +/* ── Sections (markdown, etc.) ── */ + +section { + margin-bottom: 16px; +} + +section h2 { + border-bottom: none; + margin: 0 0 12px; + padding: 0; +} + +/* ── Scrollbar ── */ + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.25); +} diff --git a/site/test/test2/working-dir-test.uce b/site/test/test2/working-dir-test.uce index 99bab3e..6459123 100644 --- a/site/test/test2/working-dir-test.uce +++ b/site/test/test2/working-dir-test.uce @@ -2,5 +2,5 @@ RENDER(Request& context) { - print("Sub-Invoke Working dir: ", get_cwd(), "\n"); + print("Sub-Invoke Working dir: ", cwd_get(), "\n"); } diff --git a/site/test/unit-info.uce b/site/test/unit-info.uce new file mode 100644 index 0000000..a27e7f8 --- /dev/null +++ b/site/test/unit-info.uce @@ -0,0 +1,22 @@ +RENDER(Request& context) +{ + DTree payload; + + if(context.get["compile"] != "") + payload["compile_ok"].set_bool(unit_compile(context.get["compile"])); + + payload["current"] = unit_info(); + + DTree units; + units.set_array(); + for(auto& unit_path : units_list()) + { + DTree item; + item = unit_path; + units.push(item); + } + payload["units"] = units; + + context.header["Content-Type"] = "application/json; charset=utf-8"; + print(json_encode(payload)); +} diff --git a/site/test/websockets.ws.uce b/site/test/websockets.ws.uce index 039b740..a44c5f2 100644 --- a/site/test/websockets.ws.uce +++ b/site/test/websockets.ws.uce @@ -16,7 +16,7 @@ DTree chat_event(Request& context, String type, String body) event["connection_id"] = ws_connection_id(); event["scope"] = first(context.params["DOCUMENT_URI"], ws_scope()); event["online"] = (f64)ws_connection_count(); - event["at"] = gmdate("%H:%M:%S"); + event["at"] = time_format_utc("%H:%M:%S"); event["name"] = context.connection["name"].to_string(); event["message_count"] = context.connection["message_count"]; return(event); @@ -171,7 +171,7 @@ WS(Request& context) if(type == "join") { - context.connection["joined_at"] = gmdate("%Y-%m-%d %H:%M:%S"); + context.connection["joined_at"] = time_format_utc("%Y-%m-%d %H:%M:%S"); context.connection["last_type"] = type; ws_send(json_encode(chat_event(context, "join", name + " joined the room"))); return; diff --git a/site/test/working-dir.uce b/site/test/working-dir.uce index e063fbc..e1f0df8 100644 --- a/site/test/working-dir.uce +++ b/site/test/working-dir.uce @@ -10,8 +10,8 @@ RENDER(Request& context) Working Directory
diff --git a/src/fastcgi/src/fcgicc.cc b/src/fastcgi/src/fcgicc.cc index ff1322f..56e151a 100644 --- a/src/fastcgi/src/fcgicc.cc +++ b/src/fastcgi/src/fcgicc.cc @@ -102,7 +102,7 @@ FastCGIServer::shutdown() for (std::vector::iterator it = listen_unlink.begin(); it != listen_unlink.end(); ++it) - unlink(it->c_str()); + file_unlink(*it); for (std::map::iterator it = client_sockets.begin(); it != client_sockets.end(); ++it) @@ -188,7 +188,7 @@ FastCGIServer::listen(const std::string& local_path) std::memcpy(sa.sun_path, local_path.data(), size); - unlink(local_path.c_str()); + file_unlink(local_path); try { if (bind(server_socket, (struct sockaddr*)&sa, sizeof(sa) - (sizeof(sa.sun_path) - size - 1)) == -1) @@ -204,7 +204,7 @@ FastCGIServer::listen(const std::string& local_path) printf("(P) listening to #%i socket %s\n", server_socket, local_path.c_str()); } catch (...) { - unlink(local_path.c_str()); + file_unlink(local_path); throw; } @@ -350,7 +350,7 @@ FastCGIServer::process(int timeout_ms) FastCGIRequest* new_request = new FastCGIRequest(); new_request->resources.client_socket = client_socket; new_request->resources.server_socket = socket_handle; - new_request->stats.time_init = microtime(); + new_request->stats.time_init = time_precise(); client_sockets[client_socket]->requests[client_socket] = new_request; } } @@ -467,7 +467,7 @@ FastCGIServer::process_http_request(FastCGIRequest& request, String& data) request.params["SCRIPT_FILENAME"] = request.params["HTTP_SCRIPT_FILENAME"]; else if(request.params["SCRIPT_FILENAME"] == "" && request.params["DOCUMENT_URI"] != "") { - String document_root = first(request.params["DOCUMENT_ROOT"], get_cwd()); + String document_root = first(request.params["DOCUMENT_ROOT"], cwd_get()); if(document_root.length() > 1 && document_root[document_root.length()-1] == '/') document_root.resize(document_root.length()-1); request.params["DOCUMENT_ROOT"] = document_root; @@ -899,7 +899,7 @@ FastCGIServer::read_fgci(Connection& connection) FastCGIRequest* new_request = new FastCGIRequest(); new_request->resources.client_socket = connection.client_socket; new_request->resources.server_socket = connection.server_socket; - new_request->stats.time_init = microtime(); + new_request->stats.time_init = time_precise(); connection.requests[request_id] = new_request; break; @@ -1019,7 +1019,7 @@ FastCGIServer::assemble_output_buffer(FastCGIRequest& request, Connection* conne } request.ob_stack.clear(); request.flags.output_closed = true; - request.stats.time_end = microtime(); + request.stats.time_end = time_precise(); if(request.flags.log_request) printf("(r) pid:%i\t%s\t%0.6fs\tfps:%0.0f\tout:%0.1fkB\tmem:%0.0f/%0.0fkB\n", my_pid, diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index 4f57a71..dda5326 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -1,6 +1,329 @@ #include "compiler.h" +#include +#include #include +namespace { + +struct SharedUnitFilesystemState +{ + bool source_exists = false; + time_t source_time = 0; + time_t compiled_time = 0; + time_t setup_template_time = 0; + time_t required_time = 0; +}; + +String compiler_registry_file_name(Request* context) +{ + return(context->server->config["BIN_DIRECTORY"] + "/known-uce-files.txt"); +} + +String compiler_registry_lock_file_name(Request* context) +{ + return(compiler_registry_file_name(context) + ".lock"); +} + +int compiler_open_lock_file(String file_name) +{ + int fdlock = open(file_name.c_str(), O_RDWR | O_CREAT, 0666); + if(fdlock == -1) + printf("(!) Could not open lock file %s\n", file_name.c_str()); + return(fdlock); +} + +void compiler_close_lock_file(int fdlock) +{ + if(fdlock == -1) + return; + flock(fdlock, LOCK_UN); + close(fdlock); +} + +String compiler_normalize_unit_path(Request* context, String file_name) +{ + file_name = trim(file_name); + if(file_name == "") + return(""); + if(file_name[0] != '/') + file_name = expand_path(file_name, context->server->config["COMPILER_SYS_PATH"]); + return(file_name); +} + +bool compiler_is_known_unit_file(String file_name) +{ + return(file_name.length() >= 4 && file_name.substr(file_name.length() - 4) == ".uce"); +} + +StringList compiler_normalize_unit_list(Request* context, StringList files) +{ + StringList normalized; + for(auto& file_name : files) + { + auto normalized_name = compiler_normalize_unit_path(context, trim(file_name)); + if(normalized_name != "" && compiler_is_known_unit_file(normalized_name)) + normalized.push_back(normalized_name); + } + std::sort(normalized.begin(), normalized.end()); + normalized.erase(std::unique(normalized.begin(), normalized.end()), normalized.end()); + return(normalized); +} + +StringList compiler_read_known_units_unlocked(Request* context) +{ + auto content = trim(file_get_contents(compiler_registry_file_name(context))); + if(content == "") + return(StringList()); + return(compiler_normalize_unit_list(context, split(content, "\n"))); +} + +void compiler_write_known_units_unlocked(Request* context, StringList files) +{ + files = compiler_normalize_unit_list(context, files); + if(files.size() == 0) + { + file_put_contents(compiler_registry_file_name(context), ""); + return; + } + file_put_contents(compiler_registry_file_name(context), join(files, "\n") + "\n"); +} + +template +auto compiler_with_registry_lock(Request* context, TCallback callback) -> decltype(callback()) +{ + auto lock_file_name = compiler_registry_lock_file_name(context); + int fdlock = compiler_open_lock_file(lock_file_name); + if(fdlock != -1) + flock(fdlock, LOCK_EX); + auto result = callback(); + compiler_close_lock_file(fdlock); + return(result); +} + +SharedUnitFilesystemState inspect_shared_unit_filesystem(Request* context, SharedUnit* su) +{ + SharedUnitFilesystemState state; + state.source_exists = file_exists(su->file_name); + if(state.source_exists) + state.source_time = file_mtime(su->file_name); + state.setup_template_time = file_mtime( + context->server->config["COMPILER_SYS_PATH"] + "/" + + context->server->config["SETUP_TEMPLATE"]); + state.required_time = std::max(state.source_time, state.setup_template_time); + state.compiled_time = file_mtime(su->so_name); + return(state); +} + +bool shared_unit_cache_is_stale(Request* context, SharedUnit* su) +{ + if(!su) + return(true); + + auto state = inspect_shared_unit_filesystem(context, su); + if(!state.source_exists) + return(true); + if(state.compiled_time == 0) + return(true); + if(state.compiled_time < state.required_time) + return(true); + if(su->last_compiled != 0 && state.compiled_time != su->last_compiled) + return(true); + return(false); +} + +void release_shared_unit_cache_entry(Request* context, String file_name) +{ + auto it = context->server->units.find(file_name); + if(it == context->server->units.end()) + return; + delete it->second; + context->server->units.erase(it); +} + +String compiler_current_unit_path(Request* context) +{ + if(!context) + return(""); + return(first( + context->resources.current_unit_file, + context->params["SCRIPT_FILENAME"] + )); +} + +String compiler_resolve_unit_path(Request* context, String file_name, String current_path = "") +{ + if(!context) + return(""); + + file_name = trim(file_name); + if(file_name == "") + file_name = compiler_current_unit_path(context); + + if(file_name == "") + return(""); + + if(current_path == "") + { + auto current_unit_file = compiler_current_unit_path(context); + if(current_unit_file != "") + current_path = dirname(current_unit_file); + else + current_path = cwd_get(); + } + + if(file_name[0] != '/') + file_name = expand_path(file_name, current_path); + + return(compiler_normalize_unit_path(context, file_name)); +} + +f64 compiler_average(f64 total, u64 count) +{ + if(count == 0) + return(0); + return(total / (f64)count); +} + +void compiler_record_compile_result(SharedUnit* su, f64 duration, bool success, String status, String error_status = "") +{ + su->compile_count += 1; + su->last_compile_duration = duration; + su->total_compile_duration += duration; + if(su->best_compile_duration == 0 || duration < su->best_compile_duration) + su->best_compile_duration = duration; + if(duration > su->worst_compile_duration) + su->worst_compile_duration = duration; + + if(success) + { + su->compile_success_count += 1; + su->compile_status = status; + su->compile_error_status = ""; + } + else + { + su->compile_failure_count += 1; + su->compile_status = status; + su->compile_error_status = error_status; + su->last_error = time(); + } +} + +void compiler_begin_render_result(SharedUnit* su, bool count_request) +{ + su->invoke_count += 1; + if(count_request) + su->request_count += 1; + su->last_rendered = time(); +} + +void compiler_record_render_result(SharedUnit* su, f64 duration, bool success, String error_status = "") +{ + su->last_render_duration = duration; + su->total_render_duration += duration; + if(su->best_render_duration == 0 || duration < su->best_render_duration) + su->best_render_duration = duration; + if(duration > su->worst_render_duration) + su->worst_render_duration = duration; + + if(success) + { + su->runtime_error_status = ""; + } + else + { + su->runtime_error_count += 1; + su->runtime_error_status = error_status; + su->last_error = time(); + } +} + +String compiler_error_status(SharedUnit* su) +{ + if(!su) + return(""); + if(trim(su->compile_error_status) != "") + return(su->compile_error_status); + return(su->runtime_error_status); +} + +String compiler_status_from_filesystem(const SharedUnitFilesystemState& state, SharedUnit* su = 0) +{ + if(!state.source_exists) + return("missing_source"); + if(state.compiled_time == 0) + return("not_compiled"); + if(state.compiled_time < state.required_time) + return("stale"); + if(su && su->so_handle) + return("loaded"); + return("compiled"); +} + +StringList compiler_unit_exports(SharedUnit* su) +{ + StringList exports; + if(su && su->api_declarations.size() > 0) + exports = su->api_declarations; + for(auto it = exports.begin(); it != exports.end();) + { + *it = trim(*it); + if(*it == "") + { + it = exports.erase(it); + continue; + } + ++it; + } + return(exports); +} + +String compiler_unit_exports_text(SharedUnit* su) +{ + if(!su || su->api_file_name == "") + return(""); + return(trim(file_get_contents(su->api_file_name))); +} + +void compiler_tree_push_string(DTree& tree, String value) +{ + DTree item; + item = value; + tree.push(item); +} + +void compiler_tree_push_strings(DTree& tree, StringList values) +{ + tree.set_array(); + for(auto& value : values) + compiler_tree_push_string(tree, value); +} + +void compiler_tree_set_bool(DTree& tree, String key, bool value) +{ + tree[key].set_bool(value); +} + +bool compiler_is_request_entry_unit(Request* context, SharedUnit* su) +{ + if(!context || !su) + return(false); + auto request_file = compiler_normalize_unit_path(context, context->params["SCRIPT_FILENAME"]); + return(request_file != "" && request_file == su->file_name); +} + +bool compiler_can_write_response(Request* context) +{ + return( + context && + context->params["REQUEST_METHOD"] != "" && + context->ob && + context->ob_stack.size() > 0 + ); +} + +} + String process_text_literal(Request* context, SharedUnit* su, String content) { String pc; @@ -98,6 +421,46 @@ String process_text_literal(Request* context, SharedUnit* su, String content) pc.append(HT_END + code_buffer + HT_START); } } + else if(c == '<' && c1 == '>') + { + // Nested <> markup block inside code + String sub_buffer = ""; + u32 sub_depth = 0; + u32 j = i + 2; + while(j < content.length()) + { + char jc = content[j]; + char jc1 = (j + 1 < content.length()) ? content[j + 1] : '\0'; + char jc2 = (j + 2 < content.length()) ? content[j + 2] : '\0'; + if(jc == '<' && jc1 == '/' && jc2 == '>') + { + if(sub_depth > 0) + { + sub_depth--; + sub_buffer.append(""); + j += 3; + } + else + { + j += 3; + break; + } + } + else if(jc == '<' && jc1 == '>') + { + sub_depth++; + sub_buffer.append("<>"); + j += 2; + } + else + { + sub_buffer.append(1, jc); + j++; + } + } + i = j - 1; // -1 because outer for loop increments + code_buffer.append(process_text_literal(context, su, sub_buffer)); + } else { code_buffer.append(1, c); @@ -143,7 +506,7 @@ String preprocess_named_render_syntax(String content) String render_name = trim(signature.substr(0, open_paren_pos)); String render_signature = signature.substr(open_paren_pos); if(render_name != "") - line = indent + "EXPORT void render_" + safe_name(render_name) + render_signature; + line = indent + "EXPORT void __unit_render_" + safe_name(render_name) + render_signature; } } else if(trimmed.rfind("COMPONENT:", 0) == 0) @@ -155,7 +518,7 @@ String preprocess_named_render_syntax(String content) String render_name = trim(signature.substr(0, open_paren_pos)); String render_signature = signature.substr(open_paren_pos); if(render_name != "") - line = indent + "EXPORT void component_render_" + safe_name(render_name) + render_signature; + line = indent + "EXPORT void __unit_component_" + safe_name(render_name) + render_signature; } } @@ -185,6 +548,7 @@ String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String String token = ""; String html_buffer = ""; u8 mode = 0; + u32 depth = 0; bool inside_quote = false; u32 source_length = content.length(); String current_line = ""; @@ -198,9 +562,26 @@ String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String { if(c == '<' && c1 == '/' && c2 == '>') { - i += 2; - pc.append(process_text_literal(context, su, html_buffer)); - mode = 0; + if(depth > 0) + { + depth -= 1; + token.append(""); + html_buffer.append(""); + i += 2; + } + else + { + i += 2; + pc.append(process_text_literal(context, su, html_buffer)); + mode = 0; + } + } + else if(c == '<' && c1 == '>') + { + depth += 1; + token.append("<>"); + html_buffer.append("<>"); + i += 1; } else { @@ -300,14 +681,21 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name) su->on_component = 0; su->on_websocket = 0; su->on_setup = 0; + su->so_handle = 0; su->compiler_messages = ""; if(!file_exists(su->so_name)) { if(su->opt_so_optional) + { + su->compile_status = "not_compiled"; return; + } //printf("(i) unit file not found: %s\n", su->so_name.c_str()); su->compiler_messages = "unit file not found"; + su->compile_status = "not_compiled"; + su->compile_error_status = su->compiler_messages; + su->last_error = time(); return; } @@ -315,15 +703,18 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name) if(su->so_handle) { su->last_compiled = file_mtime(su->so_name); + su->last_loaded = time(); + su->compile_status = "loaded"; + su->compile_error_status = ""; char *error; su->on_setup = (request_handler)dlsym(su->so_handle, "set_current_request"); if ((error = dlerror()) != NULL) printf("Error - %s in %s\n", error, su->file_name.c_str()); - su->on_render = (request_ref_handler)dlsym(su->so_handle, "render"); + su->on_render = (request_ref_handler)dlsym(su->so_handle, "__unit_render"); dlerror(); - su->on_component = (request_ref_handler)dlsym(su->so_handle, "component_render"); + su->on_component = (request_ref_handler)dlsym(su->so_handle, "__unit_component"); dlerror(); - su->on_websocket = (request_ref_handler)dlsym(su->so_handle, "websocket"); + su->on_websocket = (request_ref_handler)dlsym(su->so_handle, "__unit_websocket"); dlerror(); su->api_declarations = split(file_get_contents(su->api_file_name), "\n"); //else @@ -331,6 +722,10 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name) } else { + su->compiler_messages = "could not open " + su->so_name; + su->compile_status = "load_error"; + su->compile_error_status = su->compiler_messages; + su->last_error = time(); printf("Error loading unit %s, could not open %s\n", su->file_name.c_str(), su->so_name.c_str()); } } @@ -350,11 +745,13 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name) void compile_shared_unit(Request* context, SharedUnit* su, String file_name) { //setup_unit_paths(context, su, file_name); - f64 comp_start = microtime(); + f64 comp_start = time_precise(); if(!file_exists(su->file_name)) { su->compiler_messages = "source file not found (" + su->file_name + ")"; + compiler_untrack_known_unit(context, su->file_name); + compiler_record_compile_result(su, time_precise() - comp_start, false, "missing_source", su->compiler_messages); return; } @@ -374,76 +771,89 @@ void compile_shared_unit(Request* context, SharedUnit* su, String file_name) if(su->compiler_messages.length() > 0) { + compiler_record_compile_result(su, time_precise() - comp_start, false, "compile_error", su->compiler_messages); printf("%s \n", su->compiler_messages.c_str()); } else { load_shared_unit(context, su, file_name); + compiler_record_compile_result( + su, + time_precise() - comp_start, + (su->so_handle != 0), + (su->so_handle ? "loaded" : "load_error"), + compiler_error_status(su) + ); printf("(i) compiled unit %s in %f s\n", (su->pre_path + "/" + su->pre_file_name).c_str(), - microtime() - comp_start); + time_precise() - comp_start); } } +SharedUnit* compiler_get_shared_unit_internal(Request* context, String file_name, bool opt_so_optional, bool force_recompile) +{ + file_name = compiler_normalize_unit_path(context, file_name); + + auto existing = context->server->units.find(file_name); + bool cache_mode_matches = ( + existing != context->server->units.end() && + existing->second->opt_so_optional == opt_so_optional + ); + if(!force_recompile && existing != context->server->units.end() && cache_mode_matches && !shared_unit_cache_is_stale(context, existing->second)) + return(existing->second); + + if(existing != context->server->units.end()) + release_shared_unit_cache_entry(context, file_name); + + SharedUnit* su = new SharedUnit(); + setup_unit_paths(context, su, file_name); + su->opt_so_optional = opt_so_optional; + + int fdlock = compiler_open_lock_file(su->so_name + ".lock"); + if(fdlock != -1) + flock(fdlock, LOCK_EX); + + existing = context->server->units.find(file_name); + cache_mode_matches = ( + existing != context->server->units.end() && + existing->second->opt_so_optional == opt_so_optional + ); + if(existing != context->server->units.end() && (force_recompile || !cache_mode_matches || shared_unit_cache_is_stale(context, existing->second))) + release_shared_unit_cache_entry(context, file_name); + existing = context->server->units.find(file_name); + cache_mode_matches = ( + existing != context->server->units.end() && + existing->second->opt_so_optional == opt_so_optional + ); + if(!force_recompile && existing != context->server->units.end() && cache_mode_matches && !shared_unit_cache_is_stale(context, existing->second)) + { + compiler_close_lock_file(fdlock); + delete su; + return(existing->second); + } + + auto state = inspect_shared_unit_filesystem(context, su); + bool do_recompile = force_recompile || !state.source_exists || state.compiled_time == 0 || state.compiled_time < state.required_time; + if(do_recompile) + { + compile_shared_unit(context, su, file_name); + } + else + { + load_shared_unit(context, su, file_name); + if(!su->so_handle) + compile_shared_unit(context, su, file_name); + } + + compiler_close_lock_file(fdlock); + + context->server->units[file_name] = su; + return(su); +} + SharedUnit* get_shared_unit(Request* context, String file_name, bool opt_so_optional) { - SharedUnit* su = context->server->units[file_name]; - auto mod_time = file_mtime(file_name); - auto setup_template_time = file_mtime( - context->server->config["COMPILER_SYS_PATH"] + "/" + - context->server->config["SETUP_TEMPLATE"]); - if(setup_template_time > mod_time) - mod_time = setup_template_time; - auto compiled_time = su ? file_mtime(su->so_name) : 0; - bool do_recompile = false; - if(su && (compiled_time < mod_time || mod_time == 0)) - { - delete su; - su = 0; - do_recompile = true; - } - else if(su && (su->last_compiled < mod_time)) - { - delete su; - su = 0; - do_recompile = false; - } - if(!su) - { - su = new SharedUnit(); - setup_unit_paths(context, su, file_name); - su->opt_so_optional = opt_so_optional; - if(compiled_time != 0) - { - // if we didn't decide to force recompile yet, we need to check - // (this case should only happen if the SU cache for that entry is cold - // but the SO itself exists _AND_ is stale) - su->last_compiled = compiled_time; - if(su->last_compiled < mod_time || mod_time == 0) - do_recompile = true; - } - - int fdlock = open((su->so_name+".lock").c_str(), O_RDWR | O_CREAT, 0666 ); - int fl_excl = flock(fdlock, LOCK_EX); - - if(do_recompile) - { - compile_shared_unit(context, su, file_name); - } - else - { - load_shared_unit(context, su, file_name); - if(!su->so_handle) - compile_shared_unit(context, su, file_name); - } - - flock(fdlock, LOCK_UN); - close(fdlock); - remove((su->so_name+".lock").c_str()); - - context->server->units[file_name] = su; - } - return(su); + return(compiler_get_shared_unit_internal(context, file_name, opt_so_optional, false)); } SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String current_path, bool opt_so_optional) @@ -451,10 +861,13 @@ SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String context->stats.invoke_count++; - if(file_name[0] != '/') - { - file_name = expand_path(file_name, current_path); - } + file_name = compiler_resolve_unit_path(context, file_name, current_path); + if(file_name == "") + return(0); + if(file_exists(file_name)) + compiler_track_known_unit(context, file_name); + else + compiler_untrack_known_unit(context, file_name); //printf("(i) load '%s'\n", file_name.c_str()); @@ -464,14 +877,17 @@ SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String if(!su) { printf("Error loading unit %s\n", file_name.c_str()); - print("Error loading unit: "+file_name); + if(compiler_can_write_response(context)) + print("Error loading unit: " + file_name); return(0); } else if(su->compiler_messages.length() > 0) { - if(context->stats.invoke_count == 1) + printf("%s\n", su->compiler_messages.c_str()); + if(compiler_can_write_response(context) && context->stats.invoke_count == 1) context->header["Content-Type"] = "text/plain"; - print(su->compiler_messages); + if(compiler_can_write_response(context)) + print(su->compiler_messages); return(0); } else @@ -481,6 +897,234 @@ SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String } +String compiler_site_directory(Request* context) +{ + String site_directory = first( + context->server->config["PRECOMPILE_FILES_IN"], + context->server->config["SITE_DIRECTORY"], + "site" + ); + return(compiler_normalize_unit_path(context, site_directory)); +} + +StringList compiler_scan_site_units(Request* context) +{ + StringList files; + auto site_directory = compiler_site_directory(context); + if(site_directory == "" || !file_exists(site_directory)) + return(files); + + std::error_code walk_error; + auto options = std::filesystem::directory_options::skip_permission_denied; + for(auto it = std::filesystem::recursive_directory_iterator(site_directory, options, walk_error); + it != std::filesystem::recursive_directory_iterator(); + it.increment(walk_error)) + { + if(walk_error) + { + printf("(!) proactive scan warning in %s: %s\n", + site_directory.c_str(), + walk_error.message().c_str()); + walk_error.clear(); + continue; + } + + std::error_code entry_error; + if(!it->is_regular_file(entry_error)) + continue; + if(entry_error) + continue; + auto path = it->path().string(); + if(path.length() >= 4 && path.substr(path.length() - 4) == ".uce") + files.push_back(path); + } + return(compiler_normalize_unit_list(context, files)); +} + +StringList compiler_list_known_units(Request* context) +{ + auto files = compiler_with_registry_lock(context, [&]() { + return(compiler_read_known_units_unlocked(context)); + }); + context->server->known_unit_files.clear(); + for(auto& file_name : files) + context->server->known_unit_files[file_name] = true; + return(files); +} + +void compiler_set_known_units(Request* context, StringList files) +{ + files = compiler_normalize_unit_list(context, files); + compiler_with_registry_lock(context, [&]() { + compiler_write_known_units_unlocked(context, files); + return(0); + }); + context->server->known_unit_files.clear(); + for(auto& file_name : files) + context->server->known_unit_files[file_name] = true; +} + +void compiler_track_known_unit(Request* context, String file_name) +{ + file_name = compiler_normalize_unit_path(context, file_name); + if(file_name == "" || !compiler_is_known_unit_file(file_name) || context->server->known_unit_files[file_name]) + return; + + compiler_with_registry_lock(context, [&]() { + auto files = compiler_read_known_units_unlocked(context); + files.push_back(file_name); + compiler_write_known_units_unlocked(context, files); + return(0); + }); + context->server->known_unit_files[file_name] = true; +} + +void compiler_untrack_known_unit(Request* context, String file_name) +{ + file_name = compiler_normalize_unit_path(context, file_name); + if(file_name == "") + return; + + compiler_with_registry_lock(context, [&]() { + auto files = compiler_read_known_units_unlocked(context); + files.erase(std::remove(files.begin(), files.end(), file_name), files.end()); + compiler_write_known_units_unlocked(context, files); + return(0); + }); + context->server->known_unit_files.erase(file_name); +} + +bool compiler_unit_needs_recompile(Request* context, String file_name, bool* source_missing) +{ + file_name = compiler_normalize_unit_path(context, file_name); + SharedUnit su; + setup_unit_paths(context, &su, file_name); + auto state = inspect_shared_unit_filesystem(context, &su); + if(source_missing) + *source_missing = !state.source_exists; + if(!state.source_exists) + return(false); + if(state.compiled_time == 0) + return(true); + return(state.compiled_time < state.required_time); +} + +DTree unit_info(String path) +{ + DTree info; + if(!context) + return(info); + + String resolved_path = compiler_resolve_unit_path(context, path); + if(resolved_path == "") + return(info); + + SharedUnit* su = 0; + auto it = context->server->units.find(resolved_path); + if(it != context->server->units.end()) + { + su = it->second; + } + else + { + auto known_units = compiler_list_known_units(context); + if(std::find(known_units.begin(), known_units.end(), resolved_path) == known_units.end() && !file_exists(resolved_path)) + return(info); + } + + SharedUnit temp_unit; + if(!su) + { + su = &temp_unit; + setup_unit_paths(context, su, resolved_path); + } + + auto fs_state = inspect_shared_unit_filesystem(context, su); + auto exports_text = compiler_unit_exports_text(su); + auto exports = compiler_unit_exports(su); + if(exports.size() == 0 && exports_text != "") + exports = split(exports_text, "\n"); + + info["path"] = resolved_path; + info["file_name"] = su->file_name; + info["src_path"] = su->src_path; + info["bin_path"] = su->bin_path; + info["pre_path"] = su->pre_path; + info["src_file_name"] = su->src_file_name; + info["bin_file_name"] = su->bin_file_name; + info["pre_file_name"] = su->pre_file_name; + info["so_name"] = su->so_name; + info["api_file_name"] = su->api_file_name; + info["compile_status"] = (su->compile_status != "unknown" ? su->compile_status : compiler_status_from_filesystem(fs_state, su)); + info["compile_error_status"] = su->compile_error_status; + info["runtime_error_status"] = su->runtime_error_status; + info["error_status"] = compiler_error_status(su); + info["compiler_messages"] = su->compiler_messages; + info["last_compiled"] = (f64)su->last_compiled; + info["last_loaded"] = (f64)su->last_loaded; + info["last_rendered"] = (f64)su->last_rendered; + info["last_error"] = (f64)su->last_error; + info["request_count"] = (f64)su->request_count; + info["invoke_count"] = (f64)su->invoke_count; + info["runtime_error_count"] = (f64)su->runtime_error_count; + info["compile_count"] = (f64)su->compile_count; + info["compile_success_count"] = (f64)su->compile_success_count; + info["compile_failure_count"] = (f64)su->compile_failure_count; + info["last_compile_time"] = su->last_compile_duration; + info["average_compile_time"] = compiler_average(su->total_compile_duration, su->compile_count); + info["best_compile_time"] = su->best_compile_duration; + info["worst_compile_time"] = su->worst_compile_duration; + info["last_render_time"] = su->last_render_duration; + info["average_render_time"] = compiler_average(su->total_render_duration, su->invoke_count); + info["best_render_time"] = su->best_render_duration; + info["worst_render_time"] = su->worst_render_duration; + info["source_mtime"] = (f64)fs_state.source_time; + info["compiled_mtime"] = (f64)fs_state.compiled_time; + info["setup_template_mtime"] = (f64)fs_state.setup_template_time; + info["required_mtime"] = (f64)fs_state.required_time; + compiler_tree_set_bool(info, "known", context->server->known_unit_files[resolved_path]); + compiler_tree_set_bool(info, "current_unit", resolved_path == compiler_current_unit_path(context)); + compiler_tree_set_bool(info, "loaded", su->so_handle != 0); + compiler_tree_set_bool(info, "source_exists", fs_state.source_exists); + compiler_tree_set_bool(info, "compiled_exists", fs_state.compiled_time != 0); + compiler_tree_set_bool(info, "stale", fs_state.source_exists && fs_state.compiled_time != 0 && fs_state.compiled_time < fs_state.required_time); + compiler_tree_set_bool(info, "has_render", su->on_render != 0); + compiler_tree_set_bool(info, "has_component", su->on_component != 0); + compiler_tree_set_bool(info, "has_websocket", su->on_websocket != 0); + compiler_tree_set_bool(info, "has_setup", su->on_setup != 0); + compiler_tree_set_bool(info, "has_error", trim(compiler_error_status(su)) != ""); + + compiler_tree_push_strings(info["exports"], exports); + info["exports_text"] = exports_text; + + return(info); +} + +StringList units_list() +{ + if(!context) + return(StringList()); + + auto known_units = compiler_list_known_units(context); + for(auto& it : context->server->units) + known_units.push_back(it.first); + return(compiler_normalize_unit_list(context, known_units)); +} + +bool unit_compile(String path) +{ + if(!context) + return(false); + + String resolved_path = compiler_resolve_unit_path(context, path); + if(resolved_path == "") + return(false); + + compiler_track_known_unit(context, resolved_path); + auto su = compiler_get_shared_unit_internal(context, resolved_path, false, true); + return(su && trim(su->compiler_messages) == "" && su->so_handle != 0); +} + namespace { struct UnitInvocationScope @@ -492,9 +1136,9 @@ struct UnitInvocationScope UnitInvocationScope(Request* context, SharedUnit* su) { this->context = context; - previous_working_directory = get_cwd(); + previous_working_directory = cwd_get(); previous_unit_file = context->resources.current_unit_file; - set_cwd(su->src_path); + cwd_set(su->src_path); context->resources.current_unit_file = su->file_name; } @@ -503,7 +1147,7 @@ struct UnitInvocationScope if(!context) return; context->resources.current_unit_file = previous_unit_file; - set_cwd(previous_working_directory); + cwd_set(previous_working_directory); } }; @@ -569,7 +1213,7 @@ String component_resolve_path(String name, Request* request_context = 0) seen[candidate] = true; String resolved = candidate; if(resolved[0] != '/') - resolved = expand_path(resolved, get_cwd()); + resolved = expand_path(resolved, cwd_get()); if(file_exists(resolved)) return(resolved); } @@ -581,22 +1225,22 @@ String page_render_handler_symbol(String render_name) { render_name = trim(render_name); if(render_name == "" || render_name == "render") - return("render"); - return("render_" + safe_name(render_name)); + return("__unit_render"); + return("__unit_render_" + safe_name(render_name)); } String component_handler_symbol(String render_name) { render_name = trim(render_name); if(render_name == "") - return("component_render"); - return("component_render_" + safe_name(render_name)); + return("__unit_component"); + return("__unit_component_" + safe_name(render_name)); } request_ref_handler get_page_render_handler(SharedUnit* su, String render_name) { String symbol = page_render_handler_symbol(render_name); - if(symbol == "render") + if(symbol == "__unit_render") return(su->on_render); auto it = su->api_functions.find(symbol); @@ -612,7 +1256,7 @@ request_ref_handler get_page_render_handler(SharedUnit* su, String render_name) request_ref_handler get_component_handler(SharedUnit* su, String render_name) { String symbol = component_handler_symbol(render_name); - if(symbol == "component_render") + if(symbol == "__unit_component") return(su->on_component); auto it = su->api_functions.find(symbol); @@ -653,7 +1297,24 @@ bool compiler_invoke_render(Request* context, String file_name, String render_na UnitInvocationScope invoke_scope(context, su); su->on_setup(context); - handler(*context); + f64 render_start = time_precise(); + bool count_request = compiler_is_request_entry_unit(context, su); + compiler_begin_render_result(su, count_request); + try + { + handler(*context); + compiler_record_render_result(su, time_precise() - render_start, true); + } + catch(...) + { + compiler_record_render_result( + su, + time_precise() - render_start, + false, + "uncaught exception during render" + ); + throw; + } return(true); } @@ -685,7 +1346,23 @@ bool compiler_invoke_component(Request* context, String file_name, String render UnitInvocationScope invoke_scope(context, su); su->on_setup(context); - handler(*context); + f64 render_start = time_precise(); + compiler_begin_render_result(su, false); + try + { + handler(*context); + compiler_record_render_result(su, time_precise() - render_start, true); + } + catch(...) + { + compiler_record_render_result( + su, + time_precise() - render_start, + false, + "uncaught exception during component render" + ); + throw; + } return(true); } @@ -721,16 +1398,33 @@ void compiler_invoke_websocket(Request* context, String file_name) UnitInvocationScope invoke_scope(context, su); su->on_setup(context); - su->on_websocket(*context); + f64 render_start = time_precise(); + bool count_request = compiler_is_request_entry_unit(context, su); + compiler_begin_render_result(su, count_request); + try + { + su->on_websocket(*context); + compiler_record_render_result(su, time_precise() - render_start, true); + } + catch(...) + { + compiler_record_render_result( + su, + time_precise() - render_start, + false, + "uncaught exception during websocket handler" + ); + throw; + } } -void render_file(String file_name) +void unit_render(String file_name) { - //printf("(i) render_file(%s)\n", file_name.c_str()); + //printf("(i) unit_render(%s)\n", file_name.c_str()); compiler_invoke(context, file_name); } -void render_file(String file_name, Request& context) +void unit_render(String file_name, Request& context) { compiler_invoke(&context, file_name); } @@ -750,24 +1444,24 @@ String component_error_banner(String message) return("
" + html_escape(message) + "
"); } -void render_component(String name) +void component_render(String name) { DTree props; - render_component(name, props, *context); + component_render(name, props, *context); } -void render_component(String name, Request& context) +void component_render(String name, Request& context) { DTree props; - render_component(name, props, context); + component_render(name, props, context); } -void render_component(String name, DTree props) +void component_render(String name, DTree props) { - render_component(name, props, *context); + component_render(name, props, *context); } -void render_component(String name, DTree props, Request& context) +void component_render(String name, DTree props, Request& context) { String file_name; String render_name; @@ -810,11 +1504,11 @@ String component(String name, DTree props) String component(String name, DTree props, Request& context) { ob_start(); - render_component(name, props, context); + component_render(name, props, context); return(ob_get_close()); } -SharedUnit* load_file(String file_name) +SharedUnit* unit_load(String file_name) { auto su = compiler_load_shared_unit(context, file_name, "", false); if(su && su->so_handle) @@ -827,9 +1521,9 @@ SharedUnit* load_file(String file_name) } } -DTree* call_file(String file_name, String function_name, DTree* call_param) +DTree* unit_call(String file_name, String function_name, DTree* call_param) { - DTree* result; + DTree* result = 0; auto su = compiler_load_shared_unit(context, file_name, "", false); if(su && su->so_handle) { @@ -842,7 +1536,7 @@ DTree* call_file(String file_name, String function_name, DTree* call_param) auto f = (dtree_call_handler)dlsym(su->so_handle, function_name.c_str()); if(!f) { - print("Error: call_file() function '", function_name, "' not found"); + print("Error: unit_call() function '", function_name, "' not found"); } else { @@ -854,7 +1548,7 @@ DTree* call_file(String file_name, String function_name, DTree* call_param) } else { - print("Error: call_file() could not load unit file '", file_name, "'"); + print("Error: unit_call() could not load unit file '", file_name, "'"); } return(result); } diff --git a/src/lib/compiler.h b/src/lib/compiler.h index dbd98bd..41c8537 100644 --- a/src/lib/compiler.h +++ b/src/lib/compiler.h @@ -1,8 +1,8 @@ #pragma once -#define RENDER(X) extern "C" void render(Request& context) -#define COMPONENT(X) extern "C" void component_render(Request& context) -#define WS(X) extern "C" void websocket(Request& context) +#define RENDER(X) extern "C" void __unit_render(Request& context) +#define COMPONENT(X) extern "C" void __unit_component(Request& context) +#define WS(X) extern "C" void __unit_websocket(Request& context) #define EXPORT extern "C" String process_html_literal(Request* context, SharedUnit* su, String content); @@ -14,20 +14,28 @@ SharedUnit* get_shared_unit(Request* context, String file_name, bool opt_so_opti void compiler_invoke(Request* context, String file_name); void compiler_invoke_websocket(Request* context, String file_name); SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String current_path = "", bool opt_so_optional = false); +String compiler_site_directory(Request* context); +StringList compiler_scan_site_units(Request* context); +StringList compiler_list_known_units(Request* context); +void compiler_set_known_units(Request* context, StringList files); +void compiler_track_known_unit(Request* context, String file_name); +void compiler_untrack_known_unit(Request* context, String file_name); +bool compiler_unit_needs_recompile(Request* context, String file_name, bool* source_missing = 0); +DTree unit_info(String path = ""); +StringList units_list(); +bool unit_compile(String path = ""); -SharedUnit* load_file(String file_name); -void render_file(String file_name); -void render_file(String file_name, Request& context); -DTree* call_file(String file_name, String function_name, DTree* call_param = 0); +SharedUnit* unit_load(String file_name); +void unit_render(String file_name); +void unit_render(String file_name, Request& context); +DTree* unit_call(String file_name, String function_name, DTree* call_param = 0); String component_resolve(String name); bool component_exists(String name); -void render_component(String name); -void render_component(String name, Request& context); -void render_component(String name, DTree props); -void render_component(String name, DTree props, Request& context); +void component_render(String name); +void component_render(String name, Request& context); +void component_render(String name, DTree props); +void component_render(String name, DTree props, Request& context); String component(String name); String component(String name, Request& context); String component(String name, DTree props); String component(String name, DTree props, Request& context); - -StringList precompile_jobs; diff --git a/src/lib/dtree.cpp b/src/lib/dtree.cpp index c694344..8dc81c8 100644 --- a/src/lib/dtree.cpp +++ b/src/lib/dtree.cpp @@ -16,6 +16,20 @@ TreePtr dtree_resolve_reference(TreePtr tree) return(tree); } +bool dtree_key_is_index(String key, s64 expected_index = -1) +{ + if(key == "") + return(false); + for(auto c : key) + { + if(!isdigit(c)) + return(false); + } + if(expected_index >= 0) + return(key == std::to_string(expected_index)); + return(true); +} + } void DTree::each(std::function f) @@ -40,6 +54,23 @@ bool DTree::is_array() return(deref().type == 'M'); } +bool DTree::is_list() const +{ + const DTree& target = deref(); + if(target.type != 'M') + return(false); + if(target._map.size() == 0) + return(target._list_mode); + s64 expected_index = 0; + for(const auto& entry : target._map) + { + if(!dtree_key_is_index(entry.first, expected_index)) + return(false); + expected_index += 1; + } + return(true); +} + String DTree::to_string() { const DTree& target = deref(); @@ -213,6 +244,7 @@ void DTree::set_type(char t) case('M'): _map.clear(); _array_index = 0; + _list_mode = false; break; } } @@ -228,6 +260,7 @@ void DTree::set(String s) } set_type('S'); _String = s; + _list_mode = false; } void DTree::set(void* p) @@ -240,6 +273,7 @@ void DTree::set(void* p) } set_type('P'); _ptr = p; + _list_mode = false; } void DTree::set(f64 f) @@ -252,6 +286,7 @@ void DTree::set(f64 f) } set_type('F'); _float = f; + _list_mode = false; } void DTree::set_bool(bool b) @@ -264,6 +299,7 @@ void DTree::set_bool(bool b) } set_type('B'); _bool = b; + _list_mode = false; } void DTree::set(DTree source) @@ -279,21 +315,28 @@ void DTree::set(DTree source) { case('S'): _String = source._String; + _list_mode = false; break; case('F'): _float = source._float; + _list_mode = false; break; case('B'): _bool = source._bool; + _list_mode = false; break; case('M'): _map = source._map; + _array_index = source._array_index; + _list_mode = source._list_mode; break; case('P'): _ptr = source._ptr; + _list_mode = false; break; case('R'): _ptr = source._ptr; + _list_mode = false; break; } } @@ -307,12 +350,28 @@ void DTree::set(StringMap source) return; } set_type('M'); + _array_index = 0; + _list_mode = false; for (auto it = source.begin(); it != source.end(); ++it) { _map[it->first] = it->second; } } +void DTree::set_array() +{ + DTree* target = reference_target(); + if(target) + { + target->set_array(); + return; + } + type = 'M'; + _map.clear(); + _array_index = 0; + _list_mode = true; +} + void DTree::set_reference(DTree* target) { type = 'R'; @@ -333,6 +392,8 @@ DTree& DTree::operator [] (String s) { if(target) return((*target)[s]); set_type('M'); + if(_list_mode && !dtree_key_is_index(s)) + _list_mode = false; return(_map[s]); } @@ -351,6 +412,25 @@ void DTree::push(DTree& child) return; } set_type('M'); + if(_map.size() == 0) + { + _list_mode = true; + _array_index = 0; + } + else + { + if(is_list()) + { + _list_mode = true; + _array_index = _map.size(); + } + else + { + _list_mode = false; + while(_map.find(std::to_string(_array_index)) != _map.end()) + _array_index += 1; + } + } _map[std::to_string(_array_index)] = child; _array_index += 1; } @@ -364,6 +444,8 @@ DTree DTree::pop() auto last = _map.rbegin(); DTree result = last->second; _map.erase(last->first); + if(_list_mode) + _array_index = _map.size(); return(result); } @@ -377,6 +459,8 @@ void DTree::remove(String s) } set_type('M'); _map.erase(s); + if(_map.size() == 0) + _array_index = 0; } void DTree::clear() @@ -389,6 +473,7 @@ void DTree::clear() } set_type('M'); _map.clear(); + _array_index = 0; } String to_String(DTree t) diff --git a/src/lib/dtree.h b/src/lib/dtree.h index 17a41b9..8e082e8 100644 --- a/src/lib/dtree.h +++ b/src/lib/dtree.h @@ -8,13 +8,15 @@ struct DTree { String _String; f64 _float; - s64 _array_index; + s64 _array_index = 0; bool _bool; + bool _list_mode = false; void* _ptr; std::map _map; void each(std::function f); bool is_array(); + bool is_list() const; String to_string(); String to_json(char quote_char = '"'); String get_type_name(); @@ -31,6 +33,7 @@ struct DTree { void set_bool(bool b); void set(DTree source); void set(StringMap source); + void set_array(); void set_reference(DTree* target); DTree* key(String s); DTree& operator [] (String s); diff --git a/src/lib/functionlib.cpp b/src/lib/functionlib.cpp index b748910..ebde05e 100644 --- a/src/lib/functionlib.cpp +++ b/src/lib/functionlib.cpp @@ -396,19 +396,41 @@ String json_encode(DTree t, char quote_char) String result = ""; if(t.is_array()) { - result += "{"; - u32 count = 0; - t.each([&] (DTree item, String key) { - if(count > 0) - result += ", "; - count += 1; - result += json_escape(key, quote_char) + ": " + json_encode(item, quote_char); - }); - result += "}"; + if(t.is_list()) + { + result += "["; + u32 count = 0; + for(u32 i = 0; i < t.deref()._map.size(); i += 1) + { + if(count > 0) + result += ", "; + count += 1; + auto it = t.deref()._map.find(std::to_string(i)); + if(it == t.deref()._map.end()) + { + result += "null"; + continue; + } + result += json_encode(it->second, quote_char); + } + result += "]"; + } + else + { + result += "{"; + u32 count = 0; + t.each([&] (DTree item, String key) { + if(count > 0) + result += ", "; + count += 1; + result += json_escape(key, quote_char) + ": " + json_encode(item, quote_char); + }); + result += "}"; + } } else { - result = t.to_json(); + result = t.to_json(quote_char); } return(result); } @@ -469,6 +491,7 @@ String json_decode_String(String s, u32& i, char termination_char) } DTree json_decode_map(String s, u32& i); +DTree json_decode_array(String s, u32& i); void json_consume_space(String s, u32& i) { @@ -542,6 +565,11 @@ DTree json_decode_value(String s, u32& i) i += 1; return(json_decode_map(s, i)); } + else if(c == '[') + { + i += 1; + return(json_decode_array(s, i)); + } else { value = json_decode_keyword(s, i); @@ -599,6 +627,33 @@ DTree json_decode_map(String s, u32& i) return(result); } +DTree json_decode_array(String s, u32& i) +{ + DTree result; + result.set_array(); + json_consume_space(s, i); + while(i < s.length()) + { + char c = s[i]; + if(c == ']') + { + i += 1; + return(result); + } + else if(c == ',') + { + i += 1; + } + else + { + DTree v = json_decode_value(s, i); + result.push(v); + } + json_consume_space(s, i); + } + return(result); +} + DTree json_decode(String s) { u32 i = 0; diff --git a/src/lib/sys.cpp b/src/lib/sys.cpp index b54be76..7b2886f 100644 --- a/src/lib/sys.cpp +++ b/src/lib/sys.cpp @@ -214,12 +214,12 @@ bool file_put_contents(String file_name, String content) return(true); } -String get_cwd() +String cwd_get() { return(std::filesystem::current_path()); } -void set_cwd(String path) +void cwd_set(String path) { chdir(path.c_str()); } @@ -237,7 +237,7 @@ time_t file_mtime(String file_name) } } -void unlink(String file_name) +void file_unlink(String file_name) { remove(file_name.c_str()); } @@ -247,7 +247,7 @@ String expand_path(String path, String relative_to_path) String result; if(relative_to_path == "") - relative_to_path = get_cwd(); + relative_to_path = cwd_get(); auto base_path = split(relative_to_path, "/"); auto rel_path = split(path, "/"); @@ -271,7 +271,7 @@ String expand_path(String path, String relative_to_path) return(join(base_path, "/")); } -f64 microtime() +f64 time_precise() { return ((f64)std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch()).count()) / 1000000; @@ -282,7 +282,7 @@ u64 time() return(std::time(0)); } -String date(String format, u64 timestamp) +String time_format_local(String format, u64 timestamp) { String ts; String fmt; @@ -293,7 +293,7 @@ String date(String format, u64 timestamp) return(trim(shell_exec("date "+ts+" "+fmt))); } -String gmdate(String format, u64 timestamp) +String time_format_utc(String format, u64 timestamp) { String ts; String fmt; @@ -306,7 +306,7 @@ String gmdate(String format, u64 timestamp) return(trim(shell_exec("date -u "+ts+" "+fmt))); } -u64 parse_time(String time_String) +u64 time_parse(String time_String) { return(int_val(trim(shell_exec("date -u -d "+shell_escape(time_String)+" +'%s'")))); } @@ -503,6 +503,11 @@ pid_t spawn_subprocess(std::function exec_after_spawn) } } +int task_kill(pid_t pid, int sig) +{ + return(kill(pid, sig)); +} + pid_t task_pid(String key) { String status_file_name = context->server->config["BIN_DIRECTORY"] + "/task-" + key; @@ -511,9 +516,9 @@ pid_t task_pid(String key) if(status_file != "") { p = int_val(status_file); - if(kill(p, 0) == 0) // process is still running + if(task_kill(p, 0) == 0) // process is still running return(p); - unlink(status_file_name); + file_unlink(status_file_name); } return(p); } @@ -526,13 +531,13 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) if(status_file != "") { p = int_val(status_file); - if(kill(p, 0) == 0) // process is still running + if(task_kill(p, 0) == 0) // process is still running { printf("(P) worker process '%s' already running: PID %i\n", key.c_str(), p); return(p); } //printf("(P) worker process '%s' had crashed: PID %i\n", key.c_str(), p); - unlink(status_file_name); + file_unlink(status_file_name); } p = fork(); if(p == 0) @@ -545,7 +550,7 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) //printf("(C) child procress started, PID:%i\n", my_pid); //prctl(PR_SET_PDEATHSIG, SIGHUP); exec_after_spawn(); - unlink(status_file_name); + file_unlink(status_file_name); printf("(P) worker process '%s' terminated: PID %i\n", key.c_str(), my_pid); exit(0); } @@ -603,7 +608,9 @@ StringMap make_server_settings() cfg["TMP_UPLOAD_PATH"] = "/tmp/uce/uploads"; cfg["SESSION_PATH"] = "/tmp/uce/sessions"; cfg["COMPILER_SYS_PATH"] = "."; - cfg["PRECOMPILE_FILES_IN"] = "."; + cfg["PRECOMPILE_FILES_IN"] = ""; + cfg["SITE_DIRECTORY"] = "site"; + cfg["PROACTIVE_COMPILE_CHECK_INTERVAL"] = std::to_string(60); cfg["HTTP_PORT"] = std::to_string(8080); cfg["SESSION_TIME"] = std::to_string(60*60*24*30); diff --git a/src/lib/sys.h b/src/lib/sys.h index 7b8e011..08de282 100644 --- a/src/lib/sys.h +++ b/src/lib/sys.h @@ -21,18 +21,18 @@ bool file_append(String file_name, Ts... args) fout.close(); return(true); } -String get_cwd(); -void set_cwd(String path); +String cwd_get(); +void cwd_set(String path); time_t file_mtime(String file_name); -void unlink(String file_name); +void file_unlink(String file_name); String expand_path(String path, String relative_to_path = ""); StringList ls(String dir); -f64 microtime(); +f64 time_precise(); u64 time(); -String date(String format = "", u64 timestamp = 0); -String gmdate(String format = "", u64 timestamp = 0); -u64 parse_time(String time_String); +String time_format_local(String format = "", u64 timestamp = 0); +String time_format_utc(String format = "", u64 timestamp = 0); +u64 time_parse(String time_String); u64 socket_connect(String host, short port); void socket_close(u64 sockfd); @@ -66,6 +66,7 @@ pid_t parent_pid = 0; pid_t my_pid = 0; void on_segfault(int sig); +int task_kill(pid_t pid, int sig = 0); pid_t task(String key, std::function exec_after_spawn, u64 timeout = 60*10); pid_t task_repeat(String key, f64 interval, std::function exec_after_spawn, u64 timeout = 60*10); diff --git a/src/lib/types.h b/src/lib/types.h index 235dc4a..325d794 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -76,15 +76,39 @@ struct SharedUnit { String bin_file_name; String pre_file_name; - void* so_handle; + void* so_handle = 0; - request_handler on_setup; - request_ref_handler on_render; - request_ref_handler on_component; - request_ref_handler on_websocket; + request_handler on_setup = 0; + request_ref_handler on_render = 0; + request_ref_handler on_component = 0; + request_ref_handler on_websocket = 0; String compiler_messages; - time_t last_compiled; + String compile_status = "unknown"; + String compile_error_status = ""; + String runtime_error_status = ""; + time_t last_compiled = 0; + time_t last_loaded = 0; + time_t last_rendered = 0; + time_t last_error = 0; + + u64 request_count = 0; + u64 invoke_count = 0; + u64 runtime_error_count = 0; + + u64 compile_count = 0; + u64 compile_success_count = 0; + u64 compile_failure_count = 0; + + f64 last_compile_duration = 0; + f64 total_compile_duration = 0; + f64 best_compile_duration = 0; + f64 worst_compile_duration = 0; + + f64 last_render_duration = 0; + f64 total_render_duration = 0; + f64 best_render_duration = 0; + f64 worst_render_duration = 0; bool opt_so_optional = false; @@ -100,6 +124,7 @@ struct UploadedFile { struct ServerState { std::map units; + std::map known_unit_files; StringMap config; u32 request_count = 0; @@ -120,7 +145,7 @@ void compiler_invoke(Request* context, String file_name); struct Request { - ServerState* server; + ServerState* server = 0; StringMap params; StringMap get; @@ -141,11 +166,11 @@ struct Request { StringMap header; StringList set_cookies; - u64 random_seed; - u64 random_index; + u64 random_seed = 0; + u64 random_index = 0; std::vector ob_stack; - ByteStream* ob; + ByteStream* ob = 0; String in; String out; @@ -161,10 +186,10 @@ struct Request { } flags; struct Stats { - u32 bytes_written; - f64 time_init; - f64 time_start; - f64 time_end; + u32 bytes_written = 0; + f64 time_init = 0; + f64 time_start = 0; + f64 time_end = 0; u64 mem_high = 0; u64 mem_alloc = 0; u32 invoke_count = 0; diff --git a/src/lib/uri.cpp b/src/lib/uri.cpp index 4b7fe9d..2408521 100644 --- a/src/lib/uri.cpp +++ b/src/lib/uri.cpp @@ -291,7 +291,7 @@ String make_upload_tmp_name() String upload_path = context->server->config["TMP_UPLOAD_PATH"]; if(upload_path.length() > 0 && upload_path[upload_path.length()-1] != '/') upload_path.append(1, '/'); - return(upload_path + make_session_id()); + return(upload_path + session_id_create()); } StringMap parse_multipart(String q, String boundary, std::vector& uploaded_files) @@ -495,7 +495,7 @@ void set_cookie( String cookie = "Set-Cookie: "; cookie.append(uri_encode(name) + "=" + uri_encode(value)); if(expires > 0) - cookie.append(String("; Expires=") + gmdate("RFC1123", expires)); + cookie.append(String("; Expires=") + time_format_utc("RFC1123", expires)); context->set_cookies.push_back(cookie); context->cookies[name] = value; } @@ -512,7 +512,7 @@ StringMap parse_cookies(String cookie_String) return(result); } -String make_session_id() +String session_id_create() { return(to_hex(rand())+to_hex(rand())+to_hex(rand())+to_hex(rand())); } @@ -563,7 +563,7 @@ String session_start(String session_name) if(session_id.length() == 0) { - session_id = make_session_id(); + session_id = session_id_create(); set_cookie(session_name, session_id, time() + int_val(context->server->config["SESSION_TIME"])); } context->session_id = session_id; diff --git a/src/lib/uri.h b/src/lib/uri.h index 12e04fe..ad4ee2a 100644 --- a/src/lib/uri.h +++ b/src/lib/uri.h @@ -14,7 +14,7 @@ void set_cookie( u64 expires = 0, String path = "/", String domain = "", bool secure = false, bool http_only = true); StringMap parse_cookies(String cookie_String); -String make_session_id(); +String session_id_create(); StringMap load_session_data(String session_id); void save_session_data(String session_id, StringMap data); String session_start(String session_name = "uce-session"); diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 01e90f1..2f131f7 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -7,6 +7,7 @@ ServerState server_state; FastCGIServer server; pid_t http_worker_pid = 0; +pid_t proactive_compiler_pid = 0; bool worker_accepts_http = false; static sigjmp_buf request_fault_jmp; static volatile sig_atomic_t request_fault_active = 0; @@ -134,7 +135,7 @@ String normalize_ws_scope(String scope) return(current_ws_scope()); if(scope[0] == '/') return(scope); - return(expand_path(scope, get_cwd())); + return(expand_path(scope, cwd_get())); } String ws_message() @@ -235,7 +236,7 @@ int handle_complete(FastCGIRequest& request) { Request* previous_context = set_active_request(request); server_state.request_count += 1; request.server = &server_state; - request.stats.time_start = microtime(); + request.stats.time_start = time_precise(); //request.stats.mem_alloc = 0; //request.stats.mem_high = 0; request.header["Content-Type"] = context->server->config["CONTENT_TYPE"]; @@ -307,7 +308,7 @@ int handle_complete(FastCGIRequest& request) { for( auto &f : request.uploaded_files) { - unlink(f.tmp_name); + file_unlink(f.tmp_name); } if(failure_title == "" && request.session_id.length() > 0) @@ -333,7 +334,7 @@ int handle_websocket_message(FastCGIRequest& request, const String& message, u8 event_request.resources = request.resources; if(event_request.resources.websocket_connection_state) event_request.connection.set_reference(event_request.resources.websocket_connection_state); - event_request.stats.time_init = microtime(); + event_request.stats.time_init = time_precise(); event_request.stats.time_start = event_request.stats.time_init; event_request.random_index = 0; event_request.random_seed = gen_noise64(*reinterpret_cast(&event_request.stats.time_start)); @@ -387,19 +388,156 @@ void on_terminate(int sig) exit(1); } -void listen_for_connections() +void clear_shared_unit_cache(ServerState& state) { - if(precompile_jobs.size() > 0) + for(auto& it : state.units) + delete it.second; + state.units.clear(); +} + +void close_inherited_server_sockets() +{ + for(auto socket_handle : server.server_sockets) + close(socket_handle); + server.server_sockets.clear(); + server.server_socket_types.clear(); +} + +bool proactive_compile_queue_has(StringList& queue, String file_name) +{ + return(std::find(queue.begin(), queue.end(), file_name) != queue.end()); +} + +void proactive_compile_queue_push(StringList& queue, String file_name) +{ + if(file_name == "" || proactive_compile_queue_has(queue, file_name)) + return; + queue.push_back(file_name); +} + +void run_proactive_compiler() +{ + Request background_context; + StringList compile_queue; + f64 check_interval = float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]); + f64 failure_retry_interval = 0; + f64 next_scan_at = 0; + std::map retry_after; + if(check_interval < 1) + check_interval = 1; + failure_retry_interval = std::max(check_interval, 60.0); + + my_pid = getpid(); + context = &background_context; + background_context.server = &server_state; + + close_inherited_server_sockets(); + signal(SIGSEGV, on_segfault); + signal(SIGABRT, on_segfault); + signal(SIGBUS, on_segfault); + signal(SIGILL, on_segfault); + signal(SIGFPE, on_segfault); + signal(SIGPIPE, SIG_IGN); + setpriority(PRIO_PROCESS, 0, 10); + + auto known_units = compiler_list_known_units(&background_context); + auto site_units = compiler_scan_site_units(&background_context); + known_units.insert(known_units.end(), site_units.begin(), site_units.end()); + compiler_set_known_units(&background_context, known_units); + next_scan_at = time_precise(); + + for(;;) { - context = new Request(); - context->server = &server_state; - for(auto s : precompile_jobs) + if(compile_queue.size() == 0 && time_precise() >= next_scan_at) { - printf("- worker [%i] precompile %s\n", getpid(), s.c_str()); - get_shared_unit(context, s, false); + auto tracked_units = compiler_list_known_units(&background_context); + StringList existing_units; + + for(auto& file_name : tracked_units) + { + bool source_missing = false; + auto retry_it = retry_after.find(file_name); + bool retry_allowed = (retry_it == retry_after.end() || time_precise() >= retry_it->second); + if(compiler_unit_needs_recompile(&background_context, file_name, &source_missing) && retry_allowed) + proactive_compile_queue_push(compile_queue, file_name); + if(source_missing) + { + printf("(i) proactive compiler forget removed unit %s\n", file_name.c_str()); + retry_after.erase(file_name); + continue; + } + existing_units.push_back(file_name); + } + + if(existing_units.size() != tracked_units.size()) + compiler_set_known_units(&background_context, existing_units); + + next_scan_at = time_precise() + check_interval; } + + if(compile_queue.size() > 0) + { + auto file_name = compile_queue.front(); + compile_queue.erase(compile_queue.begin()); + bool source_missing = false; + auto retry_it = retry_after.find(file_name); + if(retry_it != retry_after.end() && time_precise() < retry_it->second) + continue; + if(compiler_unit_needs_recompile(&background_context, file_name, &source_missing)) + { + printf("(i) proactive compile %s\n", file_name.c_str()); + auto su = get_shared_unit(&background_context, file_name, false); + if(su && su->compiler_messages == "") + retry_after.erase(file_name); + else + retry_after[file_name] = time_precise() + failure_retry_interval; + } + else if(source_missing) + { + printf("(i) proactive compiler forget removed unit %s\n", file_name.c_str()); + compiler_untrack_known_unit(&background_context, file_name); + retry_after.erase(file_name); + } + else + { + retry_after.erase(file_name); + } + clear_shared_unit_cache(server_state); + usleep(250000); + continue; + } + + usleep(250000); + } +} + +bool proactive_compiler_alive() +{ + return(proactive_compiler_pid > 0 && task_kill(proactive_compiler_pid, 0) == 0); +} + +void ensure_proactive_compiler() +{ + if(float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]) <= 0) + return; + + if(proactive_compiler_alive()) + return; + + pid_t p = fork(); + if(p == 0) + { + prctl(PR_SET_PDEATHSIG, SIGHUP); + run_proactive_compiler(); + exit(0); } + proactive_compiler_pid = p; + printf("(P) proactive compiler spawned: PID %i\n", p); +} + +void listen_for_connections() +{ signal(SIGSEGV, on_segfault); signal(SIGABRT, on_segfault); signal(SIGBUS, on_segfault); @@ -431,7 +569,7 @@ void init_base_process() printf("(P) Starting parent server PID:%i\n", getpid()); server_state.config = make_server_settings(); - server_state.config["COMPILER_SYS_PATH"] = get_cwd(); + server_state.config["COMPILER_SYS_PATH"] = cwd_get(); printf("Compiler base path: %s\n", server_state.config["COMPILER_SYS_PATH"].c_str()); server_state.config["COMPILE_SCRIPT"] = @@ -461,52 +599,20 @@ void init_base_process() srand(time()); } -StringList init_precompile(u32& precompile_jobs_per_worker) -{ - StringList precompile_jobs_pending; - if(server_state.config["PRECOMPILE_FILES_IN"] != "" && int_val(server_state.config["WORKER_COUNT"]) >= 2) - { - if(server_state.config["PRECOMPILE_FILES_IN"][0] != '/') - server_state.config["PRECOMPILE_FILES_IN"] = expand_path(server_state.config["PRECOMPILE_FILES_IN"]); - precompile_jobs_pending = split(trim(shell_exec( - "find " + - shell_escape(server_state.config["PRECOMPILE_FILES_IN"]) + - " -iname '*.uce' ")), "\n"); - precompile_jobs_per_worker = 1 + (precompile_jobs_pending.size() / (int_val(server_state.config["WORKER_COUNT"]) -1)); - } - return(precompile_jobs_pending); -} - int main(int argc, char** argv) { - StringList precompile_jobs_pending; - u32 precompile_jobs_per_worker = 1; - init_base_process(); - precompile_jobs_pending = init_precompile(precompile_jobs_per_worker); - - s32 worker_spawn_count = 0; + ensure_proactive_compiler(); for(;;) { + if(!proactive_compiler_alive()) + proactive_compiler_pid = 0; + if(!termination_signal_received) + ensure_proactive_compiler(); + while(workers.size() < int_val(server_state.config["WORKER_COUNT"])) { - worker_spawn_count++; - precompile_jobs.clear(); - // spawn workers with precompile jobs if necessary but - // leave the first worker alone so it can start responding - // to requests right away - if(precompile_jobs_pending.size() > 0 && worker_spawn_count > 1) - { - for(u32 i = 0; i < precompile_jobs_per_worker; i++) - { - if(precompile_jobs_pending.size() > 0) - { - precompile_jobs.push_back(precompile_jobs_pending.back()); - precompile_jobs_pending.pop_back(); - } - } - } if(!termination_signal_received) { worker_accepts_http = (http_worker_pid == 0 || workers.count(http_worker_pid) == 0);