Compare commits
No commits in common. "main" and "1-2026-06-001" have entirely different histories.
main
...
1-2026-06-
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,7 +47,6 @@ __pycache__/
|
|||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.coverage
|
.coverage
|
||||||
htmlcov/
|
htmlcov/
|
||||||
site/doc/examples/_gen/
|
|
||||||
|
|
||||||
# Editor swap artifacts
|
# Editor swap artifacts
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
176
DOCS-TODO.md
176
DOCS-TODO.md
@ -1,176 +0,0 @@
|
|||||||
# DOCS-TODO — `/doc` documentation overhaul
|
|
||||||
|
|
||||||
Briefing for the doc-system overhaul. Read this fully before touching anything.
|
|
||||||
The approved design and the two locked decisions are: **(1) examples are LIVE and
|
|
||||||
SELF-VERIFYING** — real UCE code executed at render time, showing source + actual
|
|
||||||
captured output, gated by the test suite; **(2) FULL SWEEP** of all 257 pages.
|
|
||||||
|
|
||||||
The docs live under `site/doc/`:
|
|
||||||
- `index.uce` — the renderer (index + per-page detail view).
|
|
||||||
- `lib/doc_page.h` — page parsing (`load_doc_page`) + label/kind helpers.
|
|
||||||
- `style.css` — theme + layout.
|
|
||||||
- `pages/*.txt` — 257 page source files (the corpus).
|
|
||||||
- `areas/*.txt` — area groupings; first line is the area title, following lines
|
|
||||||
are page slugs (or `>area` references) that populate the index sidebar.
|
|
||||||
- `search.uce`, `singlepage.uce` — search + single-page render.
|
|
||||||
|
|
||||||
## Why this work exists (the problems)
|
|
||||||
|
|
||||||
- **Title bug:** 15 pages carry a `:title` that literally repeats the raw filename,
|
|
||||||
so `0_StringList` renders as "0_StringList" instead of "StringList". The `:title`
|
|
||||||
override defeats the prefix-stripping label logic already in `doc_page.h`
|
|
||||||
(`doc_default_title` / `doc_index_label` / `doc_method_label`).
|
|
||||||
- **31 slop/stub pages:** the newest APIs (`file_*`, `crypto_equal`, `sha256*`,
|
|
||||||
`hmac*`, `random_bytes`, `http_request*`, `job_*`, `shell_spawn`) are throwaway
|
|
||||||
`# name` + one-line markdown with NO `:sig`/`:params`/`:see`/`:content`.
|
|
||||||
- **134 of 257 pages have no code example.** We want PHP-manual-style
|
|
||||||
code-to-output examples on EVERY page.
|
|
||||||
- **Sidebar overflow:** long monospace identifiers in the 280px index sidebar and
|
|
||||||
220px detail sidebar have no wrapping → they spill.
|
|
||||||
- **Relevancy:** pages lead with cross-membrane mechanics and "Related Concepts"
|
|
||||||
PHP/JS filler instead of usage; cross-references between related pages are missing.
|
|
||||||
|
|
||||||
## Principles for every page
|
|
||||||
|
|
||||||
1. **Usage first.** The opening `:content` sentence says what the function does and
|
|
||||||
when you'd reach for it — in plain terms. No membrane/ABI talk up top.
|
|
||||||
2. **A real example, always.** Every page has at least one `:example` whose output
|
|
||||||
is produced by actually running it. Examples are the primary teaching tool.
|
|
||||||
3. **Cross-link.** Every page's `:see` references its area (`>area`) and its closest
|
|
||||||
sibling APIs, so the reader is never left to search alone.
|
|
||||||
4. **Demote the drivel.** Cross-membrane behavior, lock semantics, and PHP/JS
|
|
||||||
equivalents are at most a short trailing note — never the body. Delete filler
|
|
||||||
that teaches nothing.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1 — Infrastructure (do this FIRST, verify on host before any content)
|
|
||||||
|
|
||||||
### 1a. Title bug
|
|
||||||
- In `lib/doc_page.h` `load_doc_page`: when a `:title` value, trimmed, **equals the
|
|
||||||
page slug**, ignore it (leave `result.title` empty) so the renderer falls through
|
|
||||||
to `doc_default_title`, which already strips `0_/1_/2_/3_` prefixes and renders
|
|
||||||
`Class::method` for `2_*`.
|
|
||||||
- Strip the 15 redundant `:title` blocks from their pages (`0_StringList`, `cli_arg`,
|
|
||||||
`cli_input`, `list_filter`, `list_map`, `map`, `request_base_url`,
|
|
||||||
`request_query_path`, `request_query_route`, `request_script_url`,
|
|
||||||
`route_path_is_safe`, `route_path_normalize`, `route_path_sanitize`, `ucb_encode`,
|
|
||||||
`ucb_decode`).
|
|
||||||
- Fix garbage `:sig` lines on struct pages (e.g. `0_StringList`'s sig is literally
|
|
||||||
`0_StringList`): give struct pages a real one-line type summary or drop the sig box.
|
|
||||||
|
|
||||||
### 1b. Live example mechanism — EXACT SPEC
|
|
||||||
|
|
||||||
Primitives (already confirmed to exist): `unit_compile(path)`, `unit_render(path)`,
|
|
||||||
`ob_start()`, `ob_get_close()`. Relative paths in the doc unit resolve against
|
|
||||||
`site/doc/`.
|
|
||||||
|
|
||||||
- **Parse:** add `:example` handling to `load_doc_page` → `DocPage.examples`
|
|
||||||
(a list; multiple `:example` blocks per page allowed). Capture the raw body lines
|
|
||||||
verbatim (this is both the displayed source AND the executed code).
|
|
||||||
- **Materialize:** for each example, write the body wrapped in a minimal unit to a
|
|
||||||
**stable** path `examples/_gen/<slug>_<n>.uce`:
|
|
||||||
```
|
|
||||||
RENDER(Request& context)
|
|
||||||
{
|
|
||||||
<example body>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Write **only if the content changed** (compare to existing file) so the runtime's
|
|
||||||
compiled-unit cache stays warm. The `_gen` dir must be writable by the worker
|
|
||||||
(www-data) — created/`chown`ed once on the host (see Setup below).
|
|
||||||
- **Execute + capture:** `unit_compile("examples/_gen/<slug>_<n>.uce");` then
|
|
||||||
`ob_start(); unit_render("examples/_gen/<slug>_<n>.uce"); String out = ob_get_close();`
|
|
||||||
- **Render:** an "Example" `doc-section` showing the source block, then the captured
|
|
||||||
output beneath it under an "Output" label (PHP-manual style). If the example
|
|
||||||
traps or output is empty, **surface that visibly** (do not swallow it) — a broken
|
|
||||||
example must be obvious so the gate catches it.
|
|
||||||
- Examples must be **self-contained and deterministic** — no wall-clock/random output
|
|
||||||
shown as canonical (if a function is inherently non-deterministic, show a
|
|
||||||
representative call and describe the shape, or seed it). The gate asserts presence
|
|
||||||
of an output block + absence of an error marker, not an exact value, EXCEPT where
|
|
||||||
the value is deterministic.
|
|
||||||
|
|
||||||
### 1c. Sidebar + example CSS (`style.css`)
|
|
||||||
- Add `overflow-wrap: anywhere; word-break: break-word;` to `.category li a`,
|
|
||||||
`.func-item a`, and `.sidebar-card div`.
|
|
||||||
- Style the example Output block so it's visually distinct from the source (subtle
|
|
||||||
border + an "Output" label), reusing existing `--bg-code`/`--border` tokens.
|
|
||||||
|
|
||||||
### 1d. Format spec page
|
|
||||||
- Create `pages/3_Documentation format.txt` (an `info` page) documenting the canonical
|
|
||||||
page template below, so the format is self-describing inside the docs.
|
|
||||||
|
|
||||||
### Canonical page template
|
|
||||||
|
|
||||||
```
|
|
||||||
:sig
|
|
||||||
<one or more real C++ signatures, exactly as declared>
|
|
||||||
|
|
||||||
:params
|
|
||||||
<name> : <what it is>
|
|
||||||
return value : <what comes back>
|
|
||||||
|
|
||||||
:content
|
|
||||||
<Usage-first prose. What it does, when to use it. 2–5 tight sentences.>
|
|
||||||
|
|
||||||
:example
|
|
||||||
<runnable UCE code that print()s something illustrative>
|
|
||||||
|
|
||||||
:see
|
|
||||||
><area>
|
|
||||||
<closest sibling API slugs>
|
|
||||||
```
|
|
||||||
Optional short trailing note in `:content` for membrane/PHP-JS equivalence — one line,
|
|
||||||
not a section. No `:title` unless it differs from the derived label.
|
|
||||||
|
|
||||||
### Setup (host, one-time, before content work)
|
|
||||||
```sh
|
|
||||||
ssh root@10.4.2.110 'cd /Code/uce.openfu.com/uce && mkdir -p site/doc/examples/_gen && chown -R www-data:www-data site/doc/examples/_gen && chmod 775 site/doc/examples/_gen'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2 — Content sweep (batched by area; one area = one reviewable batch)
|
|
||||||
|
|
||||||
For each area file in `site/doc/areas/` (string, sys, types, sqlite, mysql, memcache,
|
|
||||||
session, websocket, task, time, uri, regex, ob, markup, socket, noise, runtime), bring
|
|
||||||
every listed page to the canonical template:
|
|
||||||
|
|
||||||
1. **Convert the 31 stubs** to full structured pages.
|
|
||||||
2. **Add a live `:example`** to every page missing one (≈134), each verified to render
|
|
||||||
real output.
|
|
||||||
3. **Repair `:see`** — area link + sibling links on every page; demote membrane / PHP-JS
|
|
||||||
filler to a one-line note.
|
|
||||||
4. **Cull dead pages** — remove `concat.txt` ("Removed. Not a current API") and audit
|
|
||||||
internal-only helpers (e.g. `json_consume_space`) for removal; also remove culled
|
|
||||||
slugs from the relevant `areas/*.txt`.
|
|
||||||
|
|
||||||
Per-batch bar: every touched page renders cleanly, its example produces real output,
|
|
||||||
and the suite stays green.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3 — Gate
|
|
||||||
|
|
||||||
- Add a `site/tests/cli_runner.uce` regression test that renders **every** `?p=` page
|
|
||||||
and asserts HTTP 200, no error/trap marker, and (for pages with `:example`) presence
|
|
||||||
of a non-error Output block.
|
|
||||||
- Full host gate (this is the ONLY place builds/tests run — the sshfs mount is
|
|
||||||
edit-only, no WASI SDK on the client):
|
|
||||||
```sh
|
|
||||||
ssh root@10.4.2.110 'cd /Code/uce.openfu.com/uce && bash scripts/build_core_wasm.sh && bash scripts/build_linux.sh && systemctl restart uce.service && sleep 3 && bash scripts/run_cli_tests.sh --include-wasm-kill'
|
|
||||||
```
|
|
||||||
Expect the current 91 to grow by the new doc gate(s), 0 failed.
|
|
||||||
|
|
||||||
## Hard rules (non-negotiable)
|
|
||||||
|
|
||||||
- **NEVER `git commit`/push/tag.** Not pi, not its subagents, not anyone. Leave a clean
|
|
||||||
working tree and report.
|
|
||||||
- **Always run the real host gate** (`bash scripts/build_core_wasm.sh && bash
|
|
||||||
scripts/build_linux.sh && systemctl restart uce.service && ...`) and trust only its
|
|
||||||
output. `g++ -fsyntax-only` / client-side builds do NOT count — there is no WASI SDK
|
|
||||||
on the client and the doc unit must actually render.
|
|
||||||
- Sub-delegation model is exactly `gpt-5.3-codex-spark`.
|
|
||||||
- Verify the example mechanism end-to-end on the host BEFORE authoring content against
|
|
||||||
it — content written against an unproven mechanism is wasted.
|
|
||||||
13
README.md
13
README.md
@ -88,7 +88,7 @@ Useful helpers for that data model include:
|
|||||||
- `zip_create()`, `zip_list()`, `zip_read()`, and `zip_extract()` for minimal ZIP archive workflows
|
- `zip_create()`, `zip_list()`, `zip_read()`, and `zip_extract()` for minimal ZIP archive workflows
|
||||||
- `gz_compress()` and `gz_uncompress()` for gzip-format byte strings
|
- `gz_compress()` and `gz_uncompress()` for gzip-format byte strings
|
||||||
- `server_start_http()` / `server_stop()` for runtime-managed custom HTTP listeners backed by `SERVE_HTTP` handlers
|
- `server_start_http()` / `server_stop()` for runtime-managed custom HTTP listeners backed by `SERVE_HTTP` handlers
|
||||||
- `map()`, `filter()`, `dv_filter()`, `dv_map()`, `dv_pick()`, and related helpers for route/menu/card data shaping near render code
|
- `map()`, `filter()`, `list_unique()`, `dv_filter()`, `dv_map()`, `dv_pick()`, and related helpers for route/menu/card data shaping near render code
|
||||||
|
|
||||||
Named component handlers are also supported:
|
Named component handlers are also supported:
|
||||||
|
|
||||||
@ -311,13 +311,9 @@ The example contains the filesystem and FastCGI settings:
|
|||||||
BIN_DIRECTORY=/var/cache/uce/work
|
BIN_DIRECTORY=/var/cache/uce/work
|
||||||
TMP_UPLOAD_PATH=/var/lib/uce/uploads
|
TMP_UPLOAD_PATH=/var/lib/uce/uploads
|
||||||
SESSION_PATH=/var/lib/uce/sessions
|
SESSION_PATH=/var/lib/uce/sessions
|
||||||
SESSION_COOKIE_SECURE=1
|
|
||||||
|
|
||||||
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||||
FCGI_SOCKET_MODE=0666
|
|
||||||
FCGI_PORT=9993
|
FCGI_PORT=9993
|
||||||
CLI_SOCKET_PATH=/run/uce/cli.sock
|
|
||||||
CLI_SOCKET_MODE=0600
|
|
||||||
|
|
||||||
PRECOMPILE_FILES_IN=
|
PRECOMPILE_FILES_IN=
|
||||||
SITE_DIRECTORY=/var/www/html
|
SITE_DIRECTORY=/var/www/html
|
||||||
@ -332,7 +328,7 @@ For nginx deployments, the most important setting is:
|
|||||||
|
|
||||||
- `FCGI_SOCKET_PATH=/run/uce/fastcgi.sock`
|
- `FCGI_SOCKET_PATH=/run/uce/fastcgi.sock`
|
||||||
|
|
||||||
That is the Unix socket nginx should use for normal `.uce` requests. `CLI_SOCKET_PATH` is for local admin/test execution through `scripts/uce-cli`; keep `CLI_SOCKET_MODE=0600` unless a trusted Unix group explicitly needs access.
|
That is the Unix socket nginx should use for normal `.uce` requests.
|
||||||
|
|
||||||
`FCGI_PORT` is optional if nginx is talking to the Unix socket. Leave it set if you also want a TCP FastCGI listener, or remove it if you want the socket to be the only FastCGI entry point.
|
`FCGI_PORT` is optional if nginx is talking to the Unix socket. Leave it set if you also want a TCP FastCGI listener, or remove it if you want the socket to be the only FastCGI entry point.
|
||||||
|
|
||||||
@ -354,7 +350,6 @@ Recommended deployment notes:
|
|||||||
|
|
||||||
- keep `HTTP_PORT` bound to localhost only at the firewall or by network policy; nginx should be the public entry point
|
- keep `HTTP_PORT` bound to localhost only at the firewall or by network policy; nginx should be the public entry point
|
||||||
- keep `BIN_DIRECTORY`, `TMP_UPLOAD_PATH`, and `SESSION_PATH` on writable local storage
|
- keep `BIN_DIRECTORY`, `TMP_UPLOAD_PATH`, and `SESSION_PATH` on writable local storage
|
||||||
- use `SESSION_COOKIE_SECURE=1` for HTTPS-only deployments; leave it `0` only for local/plain-HTTP development
|
|
||||||
- after editing `/etc/uce/settings.cfg`, restart `uce.service`
|
- after editing `/etc/uce/settings.cfg`, restart `uce.service`
|
||||||
|
|
||||||
### 4. Install and enable the systemd service
|
### 4. Install and enable the systemd service
|
||||||
@ -367,8 +362,8 @@ scripts/systemd/manage-uce-service.sh setup
|
|||||||
|
|
||||||
That script:
|
That script:
|
||||||
|
|
||||||
- installs `scripts/systemd/uce.service` as `/etc/systemd/system/uce.service`, rewriting the repository-root path in the unit to the checkout you ran it from
|
- installs `scripts/systemd/uce.service` as `/etc/systemd/system/uce.service`
|
||||||
- installs `etc/uce/settings.cfg` to `/etc/uce/settings.cfg` if it does not already exist, likewise rewriting checkout-root paths
|
- installs `etc/uce/settings.cfg` to `/etc/uce/settings.cfg` if it does not already exist
|
||||||
- reloads systemd
|
- reloads systemd
|
||||||
- enables the service at boot
|
- enables the service at boot
|
||||||
- starts the runtime immediately
|
- starts the runtime immediately
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
# /doc Documentation Overhaul
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
|
|
||||||
Implement the DOCS-TODO.md overhaul for the UCE `/doc` system on the sshfs source-of-truth tree, verifying builds/tests only on `root@10.4.2.110:/Code/uce.openfu.com/uce`.
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
- [x] Phase 1 infrastructure complete: title bug, live examples, CSS, format spec page.
|
|
||||||
- [x] Example mechanism verified end-to-end on host before broad content authoring.
|
|
||||||
- [x] Phase 2 example coverage sweep: all doc pages now have `:example`; filler/code-fence cleanup applied, resource-bound fences only.
|
|
||||||
- [x] Phase 3 doc rendering gate added.
|
|
||||||
- [x] Current host gate passes with 0 failed.
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
|
|
||||||
- Status: verifying; Phase 1 + gate complete, StringList conversion complete, Phase 2 example/filler sweep complete.
|
|
||||||
- Last updated: 2026-06-16
|
|
||||||
- Source of truth: `/root/mount_ssh/uce-dev-root-htdocs-uce`
|
|
||||||
- Runtime/live target: `root@10.4.2.110:/Code/uce.openfu.com/uce`
|
|
||||||
|
|
||||||
## Goal Tree
|
|
||||||
|
|
||||||
Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` superseded
|
|
||||||
|
|
||||||
- [x] G1: Phase 1 infrastructure
|
|
||||||
- Done when: live examples render source/output on the host and title/sidebar/spec fixes are in place.
|
|
||||||
- Verify: host build/restart/curl spot check.
|
|
||||||
- [x] G1.1: Fix title override behavior and redundant title blocks.
|
|
||||||
- [x] G1.2: Add `:example` parser, materializer, executor, renderer.
|
|
||||||
- [x] G1.3: Add sidebar/example CSS.
|
|
||||||
- [x] G1.4: Add documentation format page.
|
|
||||||
- [x] G1.5: End-to-end host verification with one page example.
|
|
||||||
- [x] G2: Phase 2 content sweep by area
|
|
||||||
- Done when: all 257 pages match canonical template and examples render.
|
|
||||||
- Verify: area batch curls and host gate stays green.
|
|
||||||
- [x] G3: Phase 3 CLI doc gate
|
|
||||||
- Done when: CLI runner checks every doc page and examples.
|
|
||||||
- Verify: full host gate passes.
|
|
||||||
|
|
||||||
## Execution Queue
|
|
||||||
|
|
||||||
1. G2 area batching/delegation now that the mechanism is proven.
|
|
||||||
2. Keep the host suite green after each area batch.
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
- 2026-06-16: Follow DOCS-TODO.md exactly; no git commit/push/tag.
|
|
||||||
|
|
||||||
## Evidence and Verification Log
|
|
||||||
|
|
||||||
- 2026-06-16: Read `DOCS-TODO.md`, `site/doc/lib/doc_page.h`, `site/doc/index.uce`, `site/tests/cli_runner.uce`.
|
|
||||||
- 2026-06-16: Host setup/build/restart/curl for `2_DValue_filter` showed source and captured output `Ada`.
|
|
||||||
- 2026-06-16: Full host gate passed: `Summary: 349 passed, 0 failed, 0 skipped`.
|
|
||||||
- 2026-06-16: Converted retiring `list_unique/list_sort/list_some/list_every/list_find` free functions to `StringList` methods, updated callers and method docs; full host gate passed: `Summary: 353 passed, 0 failed, 0 skipped`.
|
|
||||||
- 2026-06-16: Added live examples across types, string, regex, time, markup, noise, uri, and session area pages; full host gate passed: `Summary: 353 passed, 0 failed, 0 skipped`.
|
|
||||||
- 2026-06-16: Added `:example` blocks to all remaining doc pages, removed full PHP/JS Related Concepts filler, removed duplicate content code fences except resource-bound pages; full host gate passed: `Summary: 353 passed, 0 failed, 0 skipped`.
|
|
||||||
|
|
||||||
## Change Log
|
|
||||||
|
|
||||||
- 2026-06-16: Created initial goal tree.
|
|
||||||
120
docs/setup.md
120
docs/setup.md
@ -11,20 +11,20 @@ A typical deployment has four pieces:
|
|||||||
3. `uce.service`, a systemd service that builds/starts/restarts the runtime.
|
3. `uce.service`, a systemd service that builds/starts/restarts the runtime.
|
||||||
4. nginx or Apache as the public HTTP server.
|
4. nginx or Apache as the public HTTP server.
|
||||||
|
|
||||||
Recommended filesystem layout for a source checkout (replace paths as needed):
|
Recommended filesystem layout for a source checkout:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/path/to/uce-root UCE repository/runtime root (for example `/opt/uce` or `/Code/uce.openfu.com/uce`)
|
/opt/uce/ UCE repository/runtime root
|
||||||
/var/www/html/ public web root served by nginx/Apache
|
/var/www/html/ public web root served by nginx/Apache
|
||||||
/etc/uce/settings.cfg runtime configuration
|
/etc/uce/settings.cfg runtime configuration
|
||||||
/run/uce/fastcgi.sock FastCGI socket used by nginx/Apache
|
/run/uce/fastcgi.sock FastCGI socket used by nginx/Apache
|
||||||
/run/uce/cli.sock local CLI/admin/test socket
|
/run/uce/cli.sock local CLI/admin/test socket
|
||||||
/var/cache/uce/work generated source, wasm modules, caches
|
/var/cache/uce/work generated source, wasm modules, caches
|
||||||
/var/lib/uce/uploads multipart upload scratch space
|
/var/lib/uce/uploads multipart upload scratch space
|
||||||
/var/lib/uce/sessions session files
|
/var/lib/uce/sessions session files
|
||||||
```
|
```
|
||||||
|
|
||||||
For packaged installs, the runtime may live under `/usr/lib/uce` instead of your checkout root. Keep the public web root at `/var/www/html` or another normal web-root path, not under the runtime source tree.
|
For packaged installs, the runtime may live under `/usr/lib/uce` instead of `/opt/uce`. Keep the public web root at `/var/www/html` or another normal web-root path, not under the runtime source tree.
|
||||||
|
|
||||||
## Build requirements
|
## Build requirements
|
||||||
|
|
||||||
@ -32,10 +32,10 @@ On Debian/Ubuntu-like systems, install the distro packages first:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
apt update
|
apt update
|
||||||
apt install -y clang build-essential libpcre2-dev libssl-dev mariadb-client libmariadb-dev curl rsync ca-certificates
|
apt install -y clang build-essential libpcre2-dev mariadb-client libmariadb-dev curl rsync ca-certificates
|
||||||
```
|
```
|
||||||
|
|
||||||
UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans. The `curl` binary is also a pinned runtime package dependency: `http_request()` and `http_request_async()` execute it directly with an explicit argument vector for TLS-capable outbound HTTP.
|
UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans.
|
||||||
|
|
||||||
- **Wasmtime C API / C++ headers** at `/opt/wasmtime` by default. `scripts/build_linux.sh` expects:
|
- **Wasmtime C API / C++ headers** at `/opt/wasmtime` by default. `scripts/build_linux.sh` expects:
|
||||||
- `/opt/wasmtime/include/wasmtime.hh`
|
- `/opt/wasmtime/include/wasmtime.hh`
|
||||||
@ -46,8 +46,6 @@ UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at run
|
|||||||
- `/opt/wasi-sdk/bin/clang++`
|
- `/opt/wasi-sdk/bin/clang++`
|
||||||
- `/opt/wasi-sdk/bin/wasm-ld`
|
- `/opt/wasi-sdk/bin/wasm-ld`
|
||||||
- `/opt/wasi-sdk/bin/llvm-objcopy`
|
- `/opt/wasi-sdk/bin/llvm-objcopy`
|
||||||
- `/opt/wasi-sdk/bin/llvm-nm`
|
|
||||||
- `/opt/wasi-sdk/bin/llvm-dwarfdump`
|
|
||||||
|
|
||||||
You can use different install locations by setting environment variables before building and in the systemd service environment:
|
You can use different install locations by setting environment variables before building and in the systemd service environment:
|
||||||
|
|
||||||
@ -64,11 +62,10 @@ apt install -y nginx
|
|||||||
apt install -y apache2
|
apt install -y apache2
|
||||||
```
|
```
|
||||||
|
|
||||||
Build UCE from your repository root:
|
Build UCE from the repository root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
repo_root=/path/to/uce-root
|
cd /opt/uce
|
||||||
cd "$repo_root"
|
|
||||||
bash scripts/build_core_wasm.sh
|
bash scripts/build_core_wasm.sh
|
||||||
bash scripts/build_linux.sh
|
bash scripts/build_linux.sh
|
||||||
```
|
```
|
||||||
@ -102,15 +99,12 @@ The expected directories are:
|
|||||||
/opt/wasi-sdk/bin/clang++
|
/opt/wasi-sdk/bin/clang++
|
||||||
/opt/wasi-sdk/bin/wasm-ld
|
/opt/wasi-sdk/bin/wasm-ld
|
||||||
/opt/wasi-sdk/bin/llvm-objcopy
|
/opt/wasi-sdk/bin/llvm-objcopy
|
||||||
/opt/wasi-sdk/bin/llvm-nm
|
|
||||||
/opt/wasi-sdk/bin/llvm-dwarfdump
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Install the WASI SDK:
|
Install the WASI SDK:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
repo_root=/path/to/uce-root
|
cd /opt/uce
|
||||||
cd "$repo_root"
|
|
||||||
scripts/install_wasi_sdk.sh
|
scripts/install_wasi_sdk.sh
|
||||||
scripts/install_wasi_sdk.sh --check-only
|
scripts/install_wasi_sdk.sh --check-only
|
||||||
```
|
```
|
||||||
@ -139,8 +133,6 @@ test -f /opt/wasmtime/lib/libwasmtime.so
|
|||||||
/opt/wasi-sdk/bin/clang++ --version
|
/opt/wasi-sdk/bin/clang++ --version
|
||||||
/opt/wasi-sdk/bin/wasm-ld --version
|
/opt/wasi-sdk/bin/wasm-ld --version
|
||||||
/opt/wasi-sdk/bin/llvm-objcopy --version
|
/opt/wasi-sdk/bin/llvm-objcopy --version
|
||||||
/opt/wasi-sdk/bin/llvm-nm --version
|
|
||||||
/opt/wasi-sdk/bin/llvm-dwarfdump --version
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If your paths differ, export the variables for manual builds:
|
If your paths differ, export the variables for manual builds:
|
||||||
@ -171,7 +163,7 @@ systemctl restart uce.service
|
|||||||
|
|
||||||
## Runtime configuration
|
## Runtime configuration
|
||||||
|
|
||||||
Create `/etc/uce/settings.cfg` from `etc/uce/settings.cfg` and adjust paths to your checkout root. Replace checkout-specific settings such as `WASM_CORE_PATH` with `<UCE_REPO_ROOT>/bin/wasm/core.wasm` or leave it relative to `COMPILER_SYS_PATH` as appropriate.
|
Create `/etc/uce/settings.cfg` from `etc/uce/settings.cfg` and adjust paths if your runtime is not under `/opt/uce`. Replace any checkout-specific paths such as `WASM_CORE_PATH` with `/opt/uce/bin/wasm/core.wasm` or your actual runtime path.
|
||||||
|
|
||||||
Minimum useful settings:
|
Minimum useful settings:
|
||||||
|
|
||||||
@ -179,12 +171,9 @@ Minimum useful settings:
|
|||||||
BIN_DIRECTORY=/var/cache/uce/work
|
BIN_DIRECTORY=/var/cache/uce/work
|
||||||
TMP_UPLOAD_PATH=/var/lib/uce/uploads
|
TMP_UPLOAD_PATH=/var/lib/uce/uploads
|
||||||
SESSION_PATH=/var/lib/uce/sessions
|
SESSION_PATH=/var/lib/uce/sessions
|
||||||
SESSION_COOKIE_SECURE=1
|
|
||||||
|
|
||||||
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||||
FCGI_SOCKET_MODE=0666
|
|
||||||
CLI_SOCKET_PATH=/run/uce/cli.sock
|
CLI_SOCKET_PATH=/run/uce/cli.sock
|
||||||
CLI_SOCKET_MODE=0600
|
|
||||||
|
|
||||||
SITE_DIRECTORY=/var/www/html
|
SITE_DIRECTORY=/var/www/html
|
||||||
HTTP_DOCUMENT_ROOT=/var/www/html
|
HTTP_DOCUMENT_ROOT=/var/www/html
|
||||||
@ -194,35 +183,27 @@ PROACTIVE_COMPILE_CHECK_INTERVAL=60
|
|||||||
|
|
||||||
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
|
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
|
||||||
WASM_BACKEND_VERBOSE=0
|
WASM_BACKEND_VERBOSE=0
|
||||||
WASM_CORE_PATH=<UCE_REPO_ROOT>/bin/wasm/core.wasm
|
WASM_CORE_PATH=/opt/uce/bin/wasm/core.wasm
|
||||||
WASM_MEMORY_LIMIT_BYTES=536870912
|
WASM_MEMORY_LIMIT_BYTES=536870912
|
||||||
WASM_EPOCH_DEADLINE_TICKS=200
|
WASM_EPOCH_DEADLINE_TICKS=200
|
||||||
WASM_EPOCH_PERIOD_MS=50
|
WASM_EPOCH_PERIOD_MS=50
|
||||||
MYSQL_PERSISTENT_POOL_SIZE=8
|
|
||||||
|
|
||||||
WORKER_COUNT=4
|
WORKER_COUNT=4
|
||||||
MAX_MEMORY=16777216
|
MAX_MEMORY=16777216
|
||||||
SESSION_TIME=2592000
|
SESSION_TIME=2592000
|
||||||
|
|
||||||
HTTP_PORT=8080
|
HTTP_PORT=8080
|
||||||
WS_BROKER_OUTBOUND_TIMEOUT_SECONDS=30
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Important settings:
|
Important settings:
|
||||||
|
|
||||||
- `FCGI_SOCKET_PATH` is the Unix socket used for normal `.uce` requests. Set it explicitly and keep this value and the web-server `fastcgi_pass` path identical. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
|
- `FCGI_SOCKET_PATH` is the Unix socket used for normal `.uce` requests. Set it explicitly and keep this value and the web-server `fastcgi_pass` path identical. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
|
||||||
- `CLI_SOCKET_PATH` is a local HTTP-over-Unix socket used by `scripts/uce-cli` and test/admin units. Keep it private (`CLI_SOCKET_MODE=0600`) unless you intentionally delegate admin/test execution to a trusted Unix group (`0660`).
|
- `CLI_SOCKET_PATH` is a local HTTP-over-Unix socket used by `scripts/uce-cli` and test/admin units.
|
||||||
- `FCGI_SOCKET_MODE` and `CLI_SOCKET_MODE` are octal permission modes applied after socket bind. Prefer tightening `FCGI_SOCKET_MODE` to `0660` when nginx/Apache can share a trusted group with the UCE worker.
|
- `SITE_DIRECTORY` is the public site tree to scan for `.uce` files. Use `/var/www/html` when the web root is outside the runtime tree; relative paths are resolved from the runtime working directory.
|
||||||
- `SITE_DIRECTORY` is the public site tree to scan for `.uce` files. Use `/var/www/html` when the web root is outside the runtime tree; relative paths are resolved from the runtime working directory. Installed regression gate scripts derive their temporary test root from this setting unless `UCE_TEST_SITE_DIRECTORY` is explicitly provided.
|
|
||||||
- `HTTP_DOCUMENT_ROOT` is the root used by the built-in HTTP/WebSocket listener when it resolves upgrade requests. Set it to the same web root as nginx/Apache.
|
- `HTTP_DOCUMENT_ROOT` is the root used by the built-in HTTP/WebSocket listener when it resolves upgrade requests. Set it to the same web root as nginx/Apache.
|
||||||
- `BIN_DIRECTORY` stores runtime state plus ABI-scoped unit generations. Unit
|
- `BIN_DIRECTORY` stores generated C++, wasm artifacts, compile output, and runtime caches.
|
||||||
C++, wasm, serialized modules, source maps, and compile diagnostics live in
|
|
||||||
`units-c<compiler ABI>-w<core ABI>` so an upgrade cannot mix generations.
|
|
||||||
- `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime.
|
- `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime.
|
||||||
- `SESSION_COOKIE_SECURE=1` adds the `Secure` attribute to UCE-managed session cookies and should be used for HTTPS-only deployments. Leave it `0` only for local/plain-HTTP development.
|
|
||||||
- `MYSQL_PERSISTENT_POOL_SIZE` caps credential-keyed connections retained by each Wasm worker. The default `8` is clamped to `64`; set it to `0` to restore request-lifetime connections. Cached sessions are reset before reuse.
|
|
||||||
- `HTTP_PORT` is the built-in HTTP/WebSocket listener used for WebSocket upgrade traffic and direct local probes. Bind/firewall it for local access only; nginx/Apache should be the public entry point.
|
- `HTTP_PORT` is the built-in HTTP/WebSocket listener used for WebSocket upgrade traffic and direct local probes. Bind/firewall it for local access only; nginx/Apache should be the public entry point.
|
||||||
- `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` controls how long a forwarded WS message can remain queued in the broker before being dropped (default `30`). Set to `0` to disable the timeout.
|
|
||||||
- `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit and uses the pinned WASI SDK on every deployment host.
|
- `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit and uses the pinned WASI SDK on every deployment host.
|
||||||
- `WASM_CORE_PATH` must point at the built `core.wasm` file.
|
- `WASM_CORE_PATH` must point at the built `core.wasm` file.
|
||||||
|
|
||||||
@ -237,12 +218,11 @@ systemctl restart uce.service
|
|||||||
For source-checkout deployments, install the provided service helper:
|
For source-checkout deployments, install the provided service helper:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
repo_root=/path/to/uce-root
|
cd /opt/uce
|
||||||
cd "$repo_root"
|
|
||||||
scripts/systemd/manage-uce-service.sh setup
|
scripts/systemd/manage-uce-service.sh setup
|
||||||
```
|
```
|
||||||
|
|
||||||
That helper installs `scripts/systemd/uce.service` from your checked-out tree, rewrites the checked-out repository path into the installed unit and first-time config, creates runtime directories, enables the service, and starts it. Use a custom unit only if you need a nonstandard runtime layout.
|
That helper installs `scripts/systemd/uce.service`, creates runtime directories, enables the service, and starts it.
|
||||||
|
|
||||||
Useful commands:
|
Useful commands:
|
||||||
|
|
||||||
@ -252,23 +232,7 @@ scripts/systemd/manage-uce-service.sh restart
|
|||||||
scripts/systemd/manage-uce-service.sh logs 200
|
scripts/systemd/manage-uce-service.sh logs 200
|
||||||
```
|
```
|
||||||
|
|
||||||
Managed restart builds and precompiles the complete candidate ABI generation
|
Equivalent manual systemd service for a source checkout:
|
||||||
before it switches the service. Precompile uses two low-priority processes by
|
|
||||||
default; set `PRECOMPILE_JOBS` in `/etc/uce/settings.cfg` to tune the bounded
|
|
||||||
1–16 process count for the host. A failed worker, compile, serialization, or
|
|
||||||
result report aborts the switch and leaves the current service running. The
|
|
||||||
managed invocation is bounded to 900 seconds by default; set
|
|
||||||
`UCE_PRECOMPILE_TIMEOUT` on `manage-uce-service.sh restart` to select another
|
|
||||||
GNU `timeout` duration. Timeout exit 124 also aborts before the service switch.
|
|
||||||
|
|
||||||
A trusted direct `--precompile` process may set `UCE_PRECOMPILE_FILES_IN` and
|
|
||||||
`UCE_PRECOMPILE_BIN_DIRECTORY` to isolate that invocation's source scan and
|
|
||||||
artifact registry. Both overrides apply only to precompile mode; normal server
|
|
||||||
workers continue to use `/etc/uce/settings.cfg`. The parallel precompile
|
|
||||||
regression uses private roots so a running proactive compiler cannot consume
|
|
||||||
or publish its controlled race fixtures.
|
|
||||||
|
|
||||||
Equivalent manual systemd service for a source checkout (`<UCE_REPO>` = checkout root):
|
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
@ -278,14 +242,14 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=<UCE_REPO>
|
WorkingDirectory=/opt/uce
|
||||||
RuntimeDirectory=uce
|
RuntimeDirectory=uce
|
||||||
StateDirectory=uce
|
StateDirectory=uce
|
||||||
CacheDirectory=uce
|
CacheDirectory=uce
|
||||||
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
|
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
|
||||||
ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock
|
ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||||
ExecStartPre=/usr/bin/bash <UCE_REPO>/scripts/build_linux.sh
|
ExecStartPre=/usr/bin/bash /opt/uce/scripts/build_linux.sh
|
||||||
ExecStart=<UCE_REPO>/bin/uce_fastcgi.linux.bin
|
ExecStart=/opt/uce/bin/uce_fastcgi.linux.bin
|
||||||
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
|
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
@ -618,8 +582,7 @@ curl -i http://127.0.0.1/examples/uce-starter/ -H 'Host: example.com'
|
|||||||
Check the local CLI path:
|
Check the local CLI path:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
repo_root=/path/to/uce-root
|
cd /opt/uce
|
||||||
cd "$repo_root"
|
|
||||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||||
scripts/run_cli_tests.sh
|
scripts/run_cli_tests.sh
|
||||||
```
|
```
|
||||||
@ -647,26 +610,10 @@ sock.close()
|
|||||||
PY
|
PY
|
||||||
```
|
```
|
||||||
|
|
||||||
## Password hashing
|
|
||||||
|
|
||||||
Use the native password API for application credentials:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
String encoded = password_hash(password);
|
|
||||||
if(encoded == "")
|
|
||||||
// fail the write; native hashing did not complete
|
|
||||||
|
|
||||||
bool valid = password_verify(candidate, encoded);
|
|
||||||
if(valid && password_needs_rehash(encoded))
|
|
||||||
encoded = password_hash(candidate);
|
|
||||||
```
|
|
||||||
|
|
||||||
`password_hash()` returns a self-contained `$uce$scrypt$...` encoding with a random 16-byte salt and the bounded scrypt parameters `N=65536`, `r=8`, `p=1`. `password_verify()` accepts only structurally valid encodings with bounded cost parameters and compares the derived key in constant time. `password_needs_rehash()` reports malformed, legacy, or non-current parameters so applications can upgrade a credential after a successful legacy verification. Treat an empty hash as an operational failure and never store it. Application-level password length policy, rate limiting, and legacy-format verification remain the application's responsibility.
|
|
||||||
|
|
||||||
## Operational footguns
|
## Operational footguns
|
||||||
|
|
||||||
- Keep the FastCGI socket path consistent: `FCGI_SOCKET_PATH` and the web-server `fastcgi_pass` must match exactly. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
|
- Keep the FastCGI socket path consistent: `FCGI_SOCKET_PATH` and the web-server `fastcgi_pass` must match exactly. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
|
||||||
- Keep the public web root separate from the runtime source tree. Replace `/opt/uce` with your actual checkout path in local examples (for example `/opt/uce` or `/Code/uce.openfu.com/uce`), and keep public files under `/var/www/html`.
|
- Keep the public web root separate from the runtime source tree. The examples use `/opt/uce` for runtime files and `/var/www/html` for public files.
|
||||||
- Set `HTTP_DOCUMENT_ROOT` when the web root is outside the runtime working directory. The built-in HTTP/WebSocket listener resolves upgrade paths from this setting.
|
- Set `HTTP_DOCUMENT_ROOT` when the web root is outside the runtime working directory. The built-in HTTP/WebSocket listener resolves upgrade paths from this setting.
|
||||||
- Do not expose `CLI_SOCKET_PATH` or `HTTP_PORT` as public entry points. The public path should be nginx/Apache.
|
- Do not expose `CLI_SOCKET_PATH` or `HTTP_PORT` as public entry points. The public path should be nginx/Apache.
|
||||||
- Do not trust `Script-Filename` request headers from direct HTTP clients. The built-in HTTP listener resolves from `HTTP_DOCUMENT_ROOT` and rejects `..` path segments.
|
- Do not trust `Script-Filename` request headers from direct HTTP clients. The built-in HTTP listener resolves from `HTTP_DOCUMENT_ROOT` and rejects `..` path segments.
|
||||||
@ -711,18 +658,11 @@ Common compile footguns:
|
|||||||
|
|
||||||
- `WASM_COMPILE_SCRIPT` is unset or points at a removed script such as `scripts/compile`; set it to `scripts/compile_wasm_unit`.
|
- `WASM_COMPILE_SCRIPT` is unset or points at a removed script such as `scripts/compile`; set it to `scripts/compile_wasm_unit`.
|
||||||
- `scripts/check_unit_wasm.py` is missing or not executable; `scripts/compile_wasm_unit` calls it after linking each unit.
|
- `scripts/check_unit_wasm.py` is missing or not executable; `scripts/compile_wasm_unit` calls it after linking each unit.
|
||||||
- `WASI_SDK` does not point at the pinned tree with `clang++`, `wasm-ld`, `llvm-objcopy`, `llvm-nm`, and `llvm-dwarfdump`; run `scripts/install_wasi_sdk.sh --check-only`.
|
- `WASI_SDK` does not point at the pinned tree with `clang++`, `wasm-ld`, `llvm-objcopy`, and `llvm-nm`; run `scripts/install_wasi_sdk.sh --check-only`.
|
||||||
- `WASMTIME_HOME` does not point at a tree with Wasmtime headers and `libwasmtime.so`.
|
- `WASMTIME_HOME` does not point at a tree with Wasmtime headers and `libwasmtime.so`.
|
||||||
- A previous failed compile left stale `.compile.txt`, `.wasm-check.txt`, or partial `.wasm` files under `BIN_DIRECTORY`.
|
- A previous failed compile left stale `.compile.txt`, `.wasm-check.txt`, or partial `.wasm` files under `BIN_DIRECTORY`.
|
||||||
|
|
||||||
Failed compile output is persisted under the unit's ABI-generation path in
|
Failed compile output is persisted under the unit's generated path in `BIN_DIRECTORY` and may be reused until the source or compiler inputs change. First fix the source/toolchain issue and reload the page. If the cache itself is suspect, stop UCE, move only the affected unit artifact files or directory aside, and restart so the runtime recompiles from source. Avoid deleting the whole `BIN_DIRECTORY` unless you intentionally want a full rebuild.
|
||||||
`BIN_DIRECTORY` and may be reused until the source or compiler inputs change.
|
|
||||||
The managed `restart` command first runs the new binary's `--precompile` mode as
|
|
||||||
the configured service user while the old service remains live. It restarts
|
|
||||||
systemd only after every scanned unit compiles and serializes successfully.
|
|
||||||
First fix source/toolchain failures and retry; do not remove the prior generation,
|
|
||||||
which remains the rollback path. Avoid deleting the whole `BIN_DIRECTORY` unless
|
|
||||||
you intentionally want a full rebuild and have accepted losing rollback artifacts.
|
|
||||||
|
|
||||||
### CLI commands fail
|
### CLI commands fail
|
||||||
|
|
||||||
|
|||||||
@ -31,19 +31,9 @@ UCE expects these executables on each deployment host:
|
|||||||
/opt/wasi-sdk/bin/wasm-ld
|
/opt/wasi-sdk/bin/wasm-ld
|
||||||
/opt/wasi-sdk/bin/llvm-objcopy
|
/opt/wasi-sdk/bin/llvm-objcopy
|
||||||
/opt/wasi-sdk/bin/llvm-nm
|
/opt/wasi-sdk/bin/llvm-nm
|
||||||
/opt/wasi-sdk/bin/llvm-dwarfdump
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`llvm-nm` is used by `scripts/check_unit_wasm.py`, which is called by `scripts/compile_wasm_unit` after linking each unit.
|
`llvm-nm` is used by `scripts/check_unit_wasm.py`, which is called by `scripts/compile_wasm_unit` after linking each unit.
|
||||||
`llvm-dwarfdump` is used at unit compile time to extract the compact, out-of-band
|
|
||||||
source map before debug sections are stripped from the runtime artifact. The
|
|
||||||
map is consulted only on a wasm trap.
|
|
||||||
|
|
||||||
The native compiler passes the ABI-generation root to
|
|
||||||
`scripts/compile_wasm_unit`. Unless `UCE_WASM_PCH_DIR` is explicitly set, the
|
|
||||||
unit PCH lives beneath that writable generation rather than a
|
|
||||||
process-user-dependent `/tmp` path. Managed precompile therefore behaves the
|
|
||||||
same inside and outside the service unit.
|
|
||||||
|
|
||||||
## Upgrade policy
|
## Upgrade policy
|
||||||
|
|
||||||
@ -53,9 +43,7 @@ Treat WASI SDK upgrades like runtime dependency upgrades:
|
|||||||
2. Record the new release and checksum here.
|
2. Record the new release and checksum here.
|
||||||
3. Rebuild `bin/wasm/core.wasm` with `scripts/build_core_wasm.sh`.
|
3. Rebuild `bin/wasm/core.wasm` with `scripts/build_core_wasm.sh`.
|
||||||
4. Rebuild the native runtime with `scripts/build_linux.sh`.
|
4. Rebuild the native runtime with `scripts/build_linux.sh`.
|
||||||
5. Bump the compiler or core ABI constant in `src/wasm/abi.h` when required.
|
5. Clear or invalidate stale unit wasm artifacts by bumping `UCE_UNIT_ABI_VERSION` when required, or by removing affected generated artifacts under `BIN_DIRECTORY`.
|
||||||
The managed restart precompiles the resulting isolated unit generation before
|
|
||||||
switching workers; do not overwrite or delete the previous generation.
|
|
||||||
6. Run the full CLI suite including wasm kill tests:
|
6. Run the full CLI suite including wasm kill tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -26,10 +26,10 @@ gets invoked*.
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌────────────────────────────┐
|
┌────────────────────────────┐
|
||||||
nginx ──FastCGI──► worker pool (N processes) │ $FCGI_SOCKET_PATH (example `/run/uce/fastcgi.sock`)
|
nginx ──FastCGI──► worker pool (N processes) │ /run/uce.sock
|
||||||
(port 80 etc.) │ uniform unit renderers │ (FastCGI + CLI)
|
(port 80 etc.) │ uniform unit renderers │ (FastCGI + CLI)
|
||||||
└─────────────▲──────────────┘
|
└─────────────▲──────────────┘
|
||||||
│ forward render (FastCGI, FCGI_SOCKET_PATH)
|
│ forward render (FastCGI, uce.sock)
|
||||||
│
|
│
|
||||||
browser ──raw HTTP / WS──► ┌────────┴─────────┐
|
browser ──raw HTTP / WS──► ┌────────┴─────────┐
|
||||||
(HTTP_PORT 8080) │ WS broker │ owns HTTP_PORT + every
|
(HTTP_PORT 8080) │ WS broker │ owns HTTP_PORT + every
|
||||||
@ -48,29 +48,20 @@ gets invoked*.
|
|||||||
| Process | Owns | Renders units? | Source |
|
| Process | Owns | Renders units? | Source |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **Parent** | nothing; supervises children | no | `main()`, `init_base_process()` |
|
| **Parent** | nothing; supervises children | no | `main()`, `init_base_process()` |
|
||||||
| **Worker** (×`WORKER_COUNT`) | `FCGI_SOCKET_PATH` (configured socket path; example `/run/uce/fastcgi.sock`) + `CLI_SOCKET_PATH` | **yes** — the only processes that run wasm | `listen_for_connections()` |
|
| **Worker** (×`WORKER_COUNT`) | `FCGI_SOCKET_PATH` (`/run/uce.sock`) + `CLI_SOCKET_PATH` | **yes** — the only processes that run wasm | `listen_for_connections()` |
|
||||||
| **WS broker** (×1) | `HTTP_PORT` + every live WS connection + `WS_BROKER_SOCKET_PATH` | no — forwards to the pool | `run_ws_broker()` |
|
| **WS broker** (×1) | `HTTP_PORT` + every live WS connection + `WS_BROKER_SOCKET_PATH` | no — forwards to the pool | `run_ws_broker()` |
|
||||||
| **serve_http dispatcher** (×bind) | one custom-server bind address | no — forwards to the pool | `custom_server_http_dispatcher_loop()` |
|
| **serve_http dispatcher** (×bind) | one custom-server bind address | no — forwards to the pool | `custom_server_http_dispatcher_loop()` |
|
||||||
| **Proactive compiler** | nothing; pre-compiles units | no | `run_proactive_compiler()` |
|
| **Proactive compiler** | nothing; pre-compiles units | no | `run_proactive_compiler()` |
|
||||||
|
|
||||||
**only workers instantiate Wasmtime and run unit code.**
|
**only workers instantiate Wasmtime and run unit code.**
|
||||||
Every connection-owning process (broker, serve_http dispatcher) forwards the
|
Every connection-owning process (broker, serve_http dispatcher) forwards the
|
||||||
request invocation back to a worker via `FCGI_SOCKET_PATH` using the minimal
|
request invocation back to a worker over `/run/uce.sock` using the minimal
|
||||||
FastCGI client in [`src/lib/fcgi_forward.h`](../src/lib/fcgi_forward.h). This is
|
FastCGI client in [`src/lib/fcgi_forward.h`](../src/lib/fcgi_forward.h). This is
|
||||||
forced by Wasmtime: an `Engine`/`Store` cannot be safely re-created across
|
forced by Wasmtime: an `Engine`/`Store` cannot be safely re-created across
|
||||||
`fork()`, and the brokers fork from the parent that already touched the
|
`fork()`, and the brokers fork from the parent that already touched the
|
||||||
runtime. So the brokers hold the long-lived connection and units respond to
|
runtime. So the brokers hold the long-lived connection and units respond to
|
||||||
events the same way they respond to a page request — through a clean worker.
|
events the same way they respond to a page request — through a clean worker.
|
||||||
|
|
||||||
The broker-to-worker hop explicitly sets `GATEWAY_INTERFACE=CGI/1.1`. After
|
|
||||||
the Wasm core decodes request parameters it derives the guest's default status
|
|
||||||
syntax from that marker (`Status:` for FastCGI, `HTTP/1.1` for direct HTTP).
|
|
||||||
The FastCGI transport clears completed stdout/stderr as soon as their records
|
|
||||||
are queued and emits no stream records after `FCGI_END_REQUEST`. Together these
|
|
||||||
boundaries prevent a forwarded dynamic response from being parsed as body and
|
|
||||||
wrapped in a second HTTP response. The TCP and custom-server tests reject an
|
|
||||||
inner status line and require the exact dynamic body.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. The membrane and the DValue ABI
|
## 2. The membrane and the DValue ABI
|
||||||
@ -87,23 +78,13 @@ the boundary).
|
|||||||
children yielding `const DValue&`.
|
children yielding `const DValue&`.
|
||||||
- The request context (`params`/`get`/`post`/`cookies`/`session`, the raw body
|
- The request context (`params`/`get`/`post`/`cookies`/`session`, the raw body
|
||||||
`in`, and—for WS—the connection context) is marshalled into a single `ctx`
|
`in`, and—for WS—the connection context) is marshalled into a single `ctx`
|
||||||
DValue and UCEB-encoded. Immutable server configuration is UCEB-encoded once
|
DValue, UCEB-encoded, and handed to the workspace. The response (body,
|
||||||
per worker as a flat string map; both byte ranges are written into one guest
|
headers, status, and any `meta` such as `ws_commands`) comes back the same
|
||||||
buffer. The guest decodes the flat map directly into its fresh `Server` and
|
way.
|
||||||
the dynamic tree into its fresh `Request`. The response (body, headers,
|
|
||||||
status, and any `meta` such as `ws_commands`) comes back as UCEB.
|
|
||||||
|
|
||||||
See [`docs/wasm-phase1-dvalue-abi.md`](wasm-phase1-dvalue-abi.md) for the wire
|
See [`docs/wasm-phase1-dvalue-abi.md`](wasm-phase1-dvalue-abi.md) for the wire
|
||||||
format details.
|
format details.
|
||||||
|
|
||||||
Decoder robustness note: UCEB input is untrusted across the wasm membrane. The
|
|
||||||
UCEB decoder must reject malformed magic/version/varint/length/trailing-data
|
|
||||||
inputs and excessive nesting with explicit errors, not native or wasm traps. Its
|
|
||||||
current hard nesting cap is intentionally low (64 levels) to stay well below the
|
|
||||||
guest stack limit. JSON decoding follows the same rule for malformed strings and
|
|
||||||
unicode escapes: validate bounds before every read and return an empty/partial
|
|
||||||
`DValue` rather than reading beyond the input.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Units, handlers, and export naming
|
## 3. Units, handlers, and export naming
|
||||||
@ -131,164 +112,14 @@ __uce_<base>[_<sanitize(suffix)>]
|
|||||||
| `exists` | probe only — resolves the unit, loads nothing |
|
| `exists` | probe only — resolves the unit, loads nothing |
|
||||||
|
|
||||||
`sanitize_symbol_suffix()` keeps `[A-Za-z0-9_]` (mirrors `ascii_safe_name`).
|
`sanitize_symbol_suffix()` keeps `[A-Za-z0-9_]` (mirrors `ascii_safe_name`).
|
||||||
Unit code is hidden by default so the linker can discard loaded helpers that the
|
|
||||||
unit does not use. Handler macros and the explicit `EXPORT` directive are the
|
|
||||||
only application symbols promoted to default visibility; generated request
|
|
||||||
binding and constructor exports remain part of the runtime ABI. Debug sections
|
|
||||||
are removed after linking while the wasm name section is retained for runtime
|
|
||||||
traces. This keeps each side module's code and public ABI local to its actual
|
|
||||||
handler responsibility instead of duplicating every helper from a loaded app
|
|
||||||
library.
|
|
||||||
|
|
||||||
`wasm_resolve_target(unit, handler)` (`src/wasm/core.cpp`) resolves the source
|
`wasm_resolve_target(unit, handler)` (`src/wasm/core.cpp`) resolves the source
|
||||||
path and looks up the export's funcref slot; `exists` lets callers probe a unit
|
path and looks up the export's funcref slot; `exists` lets callers probe a unit
|
||||||
without instantiating it.
|
without instantiating it.
|
||||||
|
|
||||||
The request entry unit is already loaded by the host, so its selected handler
|
|
||||||
and optional `ONCE` export are placed directly into the workspace table and
|
|
||||||
passed to `uce_wasm_invoke_loaded_entry()`. Dynamic component and unit calls
|
|
||||||
continue through `wasm_resolve_target()`, where relative-path resolution is
|
|
||||||
required. Handler cache keys include the calling unit so identically named
|
|
||||||
relative targets in different directories cannot alias. Once a unit has passed
|
|
||||||
freshness checks and loaded into a request workspace, later handler lookups
|
|
||||||
reuse that immutable request-local instance; `component_loaded_reuse_count`
|
|
||||||
reports those lookups. Opt-in verbose response headers split entry invocation
|
|
||||||
into load, presence lookup, table linking, and core dispatch time.
|
|
||||||
|
|
||||||
`wasm_backend_should_handle(request, entry_unit)` checks whether the wasm
|
`wasm_backend_should_handle(request, entry_unit)` checks whether the wasm
|
||||||
backend is initialized and the requested artifact/handler is currently
|
backend is initialized and the requested artifact/handler is currently
|
||||||
available. If an artifact is cold or stale, dispatch compiles it on demand via
|
available. If an artifact is cold or stale, dispatch compiles it on demand via
|
||||||
`get_shared_unit()` and rechecks. There is no native unit-execution fallback.
|
`get_shared_unit()` and rechecks. There is no native unit-execution fallback.
|
||||||
Workers identify cached unit artifacts by nanosecond mtime, ctime, and size.
|
|
||||||
Whole-second mtime alone is insufficient because a dependency-triggered rebuild
|
|
||||||
can replace a wasm artifact within the same second as its prior build.
|
|
||||||
The worker also classifies each compiled module's immutable import descriptors
|
|
||||||
once when that artifact enters the module cache. Each request still creates its
|
|
||||||
own import vector, memory/table-base globals, GOT globals and function-table
|
|
||||||
slots, then runs relocations, constructors, and request-pointer binding. This
|
|
||||||
removes repeated Wasmtime import-type traversal without sharing request state or
|
|
||||||
changing core-first, unit-load-order symbol resolution.
|
|
||||||
Each FastCGI child initializes its process-local Wasmtime engine after fork and
|
|
||||||
before entering the accept loop. It also creates the linker, resolves the core's
|
|
||||||
host imports into a store-independent Wasmtime `InstancePre`, and births then
|
|
||||||
drops one empty workspace. This preserves Wasmtime's fork boundary while
|
|
||||||
preventing the first request assigned to each worker from paying engine, linker,
|
|
||||||
or pre-instantiation startup.
|
|
||||||
Server configuration is immutable by that point. The worker therefore retains
|
|
||||||
one native `DValue` view and one UCEB encoding of it. Each request transfers
|
|
||||||
those cached bytes and the guest decodes the flat scalar map directly into its
|
|
||||||
fresh `Server`, avoiding both repeated native encoding and an intermediate guest
|
|
||||||
`DValue` tree. Request parameters, body, cookies, session, call data, and
|
|
||||||
response state are never retained this way.
|
|
||||||
Startup duration or failure is written to the service log. The serialized core
|
|
||||||
module lives in the configured writable cache root rather than beside the
|
|
||||||
possibly root-owned deployed `core.wasm`; freshness still uses the deployed
|
|
||||||
artifact's metadata.
|
|
||||||
|
|
||||||
Packaged deployments use systemd socket activation for FastCGI. The socket unit
|
|
||||||
owns `/run/uce/fastcgi.sock`; UCE validates and adopts the single named listener
|
|
||||||
after exec. The listener therefore remains connectable and queues requests while
|
|
||||||
the service and its post-fork workers restart. Direct launches without systemd
|
|
||||||
activation retain the existing configured Unix/TCP listener behavior.
|
|
||||||
The service preserves its runtime directory across service restarts because the
|
|
||||||
socket unit, not the service unit, owns a listener path inside that directory.
|
|
||||||
On termination the parent asks render workers to close their listeners, finish
|
|
||||||
accepted connections within the bounded worker drain interval, and only then
|
|
||||||
exits. This prevents an accepted FastCGI request from being reset at handoff;
|
|
||||||
the socket unit queues later connections for the replacement workers.
|
|
||||||
The graceful signal handler belongs to the parent and render workers. Generic
|
|
||||||
`task()` children restore default termination signals after fork so
|
|
||||||
`task_kill()` and `server_stop()` retain their immediate stop contract.
|
|
||||||
|
|
||||||
Epoch interruption measures uninterrupted guest CPU segments. The common
|
|
||||||
hostcall membrane re-arms the store deadline after every native call, excluding
|
|
||||||
blocking I/O, process waits, hashing, and other host work without weakening a
|
|
||||||
guest loop that makes no hostcalls. Keeping this at the membrane also covers new
|
|
||||||
hostcalls without per-import timeout bookkeeping.
|
|
||||||
|
|
||||||
`request_perf()` reports worker module-cache hits and misses and divides a miss
|
|
||||||
into artifact lookup, wasm read, custom-section parse, serialized-module
|
|
||||||
deserialization or wasm compilation, and immutable import classification. This
|
|
||||||
includes a bounded per-request unit trace with site-relative unit names, cache
|
|
||||||
source, bytes, and phase durations so a cold outlier identifies its exact unit
|
|
||||||
and whether it used worker memory, serialized code, compilation, or failed.
|
|
||||||
Absolute source paths and source contents are not exposed. This keeps cold-worker
|
|
||||||
module latency distinguishable without exposing source paths.
|
|
||||||
The same snapshot divides pre-dispatch WASM readiness into entry normalization,
|
|
||||||
mutation freshness, artifact stat, complete dependency freshness, and worker
|
|
||||||
availability. `ready_check_count` distinguishes the warm one-check path from an
|
|
||||||
on-demand compile and recheck; repeated snapshot reads retain the initial values.
|
|
||||||
Freshness still stats every distinct source on every entry check. Exact repeated
|
|
||||||
load paths are deduplicated before canonicalization, while distinct aliases are
|
|
||||||
resolved independently so symlink retargets remain immediately visible.
|
|
||||||
When a current serialized module exists, the worker scans wasm section headers
|
|
||||||
and reads only `dylink.0`, `uce.abi`, and the tiny `uce.module` identity; it does
|
|
||||||
not fault the code and data bodies into every new worker. A missing/stale/invalid serialized module
|
|
||||||
still reads, validates, compiles, and republishes the complete wasm artifact.
|
|
||||||
The proactive compiler also creates that serialization immediately after source
|
|
||||||
compilation, keeping first-worker native compilation off the request path.
|
|
||||||
Cold module compilation and deserialization are host work, so `load_unit()`
|
|
||||||
refreshes the epoch deadline before its first guest call. Otherwise a component
|
|
||||||
whose compilation outlasted the guest CPU budget would immediately trap in the
|
|
||||||
following allocator/relocation call even though no guest loop consumed it.
|
|
||||||
|
|
||||||
Unit artifacts live beneath an ABI-generation directory such as
|
|
||||||
`BIN_DIRECTORY/units-c13-w7`: `c13` is the compiler/unit-metadata ABI and `w7`
|
|
||||||
is the runtime core ABI. Old and new service binaries therefore never publish
|
|
||||||
or read the same unit path during an ABI transition. The managed restart builds
|
|
||||||
and serializes the complete next generation before stopping the old service;
|
|
||||||
failure aborts the switch and leaves the running generation intact. Old
|
|
||||||
generation directories are retained as an explicit rollback path. Two
|
|
||||||
low-priority precompile processes are used by default so independent units do
|
|
||||||
not serialize deployment time while live workers retain scheduler priority.
|
|
||||||
`PRECOMPILE_JOBS` or `UCE_PRECOMPILE_JOBS` may select 1–16 processes. Unit
|
|
||||||
publication and the shared generation PCH use separate advisory locks, and any
|
|
||||||
worker/reporting failure rejects the candidate generation.
|
|
||||||
|
|
||||||
The proactive compiler and request workers coordinate through per-unit file
|
|
||||||
locks and a lock-protected demand-priority queue under `BIN_DIRECTORY`. A small
|
|
||||||
priority-only compiler process drains that queue independently of the full-site
|
|
||||||
scanner. This matters when the scanner is already inside a long transitive C++
|
|
||||||
compile: a requested stale component can rebuild immediately instead of waiting
|
|
||||||
for that unrelated compile to finish. Both compiler processes use the same
|
|
||||||
per-unit lock, so concurrent demand and scan discovery cannot publish duplicate
|
|
||||||
artifacts. The priority worker is idle when there is no demand and never scans
|
|
||||||
the site on its own. Unit
|
|
||||||
compilation writes and validates a process-unique temporary wasm file, then
|
|
||||||
publishes it with an atomic rename. When proactive compilation is enabled,
|
|
||||||
read-only HTTP requests keep using the last complete artifact while requesting
|
|
||||||
that stale unit at the head of the compiler queue. Non-read requests never run
|
|
||||||
a stale entry artifact: they return `503 Service Unavailable` with
|
|
||||||
`Retry-After: 1`, allowing the client to retry after the priority rebuild.
|
|
||||||
CLI and explicit compile paths remain synchronous. A failed rebuild removes
|
|
||||||
availability and surfaces the compiler error rather than serving the old unit
|
|
||||||
indefinitely. This freshness contract includes components resolved lazily from
|
|
||||||
an otherwise-current CLI entry unit: if the proactive compiler already owns a
|
|
||||||
stale child's lock, the CLI request joins that compile and waits for the fresh
|
|
||||||
artifact. Read-only HTTP may use the child's last complete artifact instead.
|
|
||||||
|
|
||||||
Before preprocessing, the compiler verifies that the worker can actually read
|
|
||||||
the source. An unreadable path is reported as a source-read failure with a
|
|
||||||
persisted diagnostic; it never becomes an apparently valid empty side module or
|
|
||||||
a generic compiler error. Source signatures mark unreadable inputs, so correcting
|
|
||||||
access invalidates that failure and permits a normal retry without changing
|
|
||||||
signatures for ordinary readable files. The private CLI harness may label its
|
|
||||||
intentional unreadable-source regression request as an expected source-read
|
|
||||||
failure, keeping production log scans focused on real runtime failures while
|
|
||||||
preserving the failing compile result and diagnostic artifacts.
|
|
||||||
|
|
||||||
Unit linking retains DWARF only long enough for
|
|
||||||
`scripts/build_unit_source_map.py` to extract a compact address/file/line table.
|
|
||||||
The published `.wasm` is debug-stripped and the table is stored beside it as
|
|
||||||
`.wasm.source-map`, keyed to the exact temporary module identity recorded in
|
|
||||||
the wasm's `uce.module` custom section. Normal module loading never reads this
|
|
||||||
sidecar. On a Wasmtime trap, the worker uses structured frame module offsets to
|
|
||||||
load only the matching map and appends source locations to the error. A missing,
|
|
||||||
stale, or malformed map is deliberately non-fatal: the ordinary named wasm
|
|
||||||
backtrace remains available. Generated C++ uses a `#line` directive naming the
|
|
||||||
original `.uce` file, so application frames resolve to application source rather
|
|
||||||
than the generated cache file. Artifact invalidation removes the wasm, serialized
|
|
||||||
module, and source map together.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -298,15 +129,7 @@ Each request gets a fresh **workspace** — a per-request wasm instance tree wit
|
|||||||
the membrane wired in. `wasm_worker_serve(worker, ctx, entry_unit, handler)`
|
the membrane wired in. `wasm_worker_serve(worker, ctx, entry_unit, handler)`
|
||||||
(`src/wasm/worker.cpp`) is the single entry point for *every* mode:
|
(`src/wasm/worker.cpp`) is the single entry point for *every* mode:
|
||||||
|
|
||||||
1. Birth a request-scoped workspace with fresh per-request state. Current
|
1. Birth a workspace (CoW-snapshot-based where available).
|
||||||
production workers create a fresh Store and instantiate the store-independent
|
|
||||||
`InstancePre`; the resulting core instance owns a fresh exported, growable
|
|
||||||
function table, memory, globals, constructors, and request tree. The worker
|
|
||||||
retains only Wasmtime's compiled module, linker, pre-instantiation plan, and
|
|
||||||
store-independent host-function definitions. Every callback resolves the one
|
|
||||||
active request workspace at invocation. Legacy cores that import their table
|
|
||||||
remain supported through the prior per-request import path. Any future
|
|
||||||
snapshot/CoW optimization must preserve this request-isolation contract.
|
|
||||||
2. Resolve `entry_unit` + `handler` to an export; components referenced at
|
2. Resolve `entry_unit` + `handler` to an export; components referenced at
|
||||||
runtime are resolved on demand via the `uce_host_component_resolve` hostcall
|
runtime are resolved on demand via the `uce_host_component_resolve` hostcall
|
||||||
(`component_resolve()` → `__uce_<...>` slot), loading dependency modules
|
(`component_resolve()` → `__uce_<...>` slot), loading dependency modules
|
||||||
@ -321,44 +144,6 @@ suspend the native SIGSEGV/SIGILL recovery handler around the wasm call so that
|
|||||||
Wasmtime's own trap signals are not escalated into a native fatal signal (see
|
Wasmtime's own trap signals are not escalated into a native fatal signal (see
|
||||||
`serve_via_wasm` in `handle_complete`).
|
`serve_via_wasm` in `handle_complete`).
|
||||||
|
|
||||||
Render workers are long-lived. The generic FastCGI transport retains a legacy
|
|
||||||
eight-connection recycle default, but `listen_for_connections()` disables it
|
|
||||||
for the UCE pool: recycling a healthy worker discards its Wasmtime engine and
|
|
||||||
module cache, adding a visible cold-start request. Request state remains bounded
|
|
||||||
by the fresh workspace and the normal per-request database/resource cleanup;
|
|
||||||
faulted workers still exit and are replaced by the parent.
|
|
||||||
|
|
||||||
A warm entry artifact is checked for compiler/source freshness once before
|
|
||||||
dispatch. A missing or stale artifact is compiled (or demand-prioritized) and
|
|
||||||
then checked again before execution. The second check belongs only to that
|
|
||||||
state-changing branch; repeating it immediately after a successful warm check
|
|
||||||
adds no freshness guarantee.
|
|
||||||
|
|
||||||
The internal request envelope carries application `context.call` as UCEB2,
|
|
||||||
each request `StringMap` as a flat UCEB2 map, scalar session/input metadata as
|
|
||||||
bounded byte segments, and optional WebSocket state as UCEB2. The guest
|
|
||||||
validates the complete envelope before moving those values into a fresh
|
|
||||||
`Request`; transport-only params, cookies, session data, and entry metadata no
|
|
||||||
longer become duplicate `context.call` children. The historical by-value
|
|
||||||
`DValue::operator=(DValue)` symbol remains exported so warmed side-module
|
|
||||||
artifacts stay ABI-compatible. `request_perf()` subdivides birth into policy,
|
|
||||||
import materialization, core instantiation, export/table lookup, and initialization,
|
|
||||||
and context transfer into bytes, host encode, guest allocation/write,
|
|
||||||
guest decode/application, and free. The byte profile separately reports the
|
|
||||||
worker-cached server-configuration portion.
|
|
||||||
|
|
||||||
### Task callbacks and workspace lifetime
|
|
||||||
|
|
||||||
`task()` and `task_repeat()` are fork-backed. The `uce_host_task_spawn` hostcall
|
|
||||||
captures the current `WasmWorkspace*`, but `src/lib/sys.cpp::task()` invokes the
|
|
||||||
captured callback only in the forked child, before the hostcall stack unwinds in
|
|
||||||
that child. The parent request may return and destroy its workspace; the child
|
|
||||||
is forked from the parent and keeps a private request-context copy of the per-request workspace. This means a delayed task callback can run after the spawning request
|
|
||||||
returns without dereferencing the parent's destroyed workspace. It is still a
|
|
||||||
callback into the inherited child workspace, not a fresh normal request
|
|
||||||
workspace; avoid adding host resources to `WasmWorkspace` that are invalid across
|
|
||||||
`fork()` unless task callback handling is changed to birth a fresh workspace.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Request dispatch (`handle_complete`)
|
## 5. Request dispatch (`handle_complete`)
|
||||||
@ -376,7 +161,7 @@ otherwise (page) → serve_via_wasm(entry_unit, "render")
|
|||||||
|
|
||||||
The `UCE_*` params are set by whichever broker forwarded the request:
|
The `UCE_*` params are set by whichever broker forwarded the request:
|
||||||
|
|
||||||
- **Page render**: FastCGI nginx → `FCGI_SOCKET_PATH` directly; no `UCE_*` flags → `render`.
|
- **Page render**: FastCGI nginx → `/run/uce.sock` directly; no `UCE_*` flags → `render`.
|
||||||
- **CLI**: the CLI socket sets `is_cli`.
|
- **CLI**: the CLI socket sets `is_cli`.
|
||||||
- **serve_http**: the custom-server dispatcher sets `UCE_SERVE_HTTP=1` plus
|
- **serve_http**: the custom-server dispatcher sets `UCE_SERVE_HTTP=1` plus
|
||||||
`UCE_SERVE_HTTP_FUNCTION` and rewrites `SCRIPT_FILENAME` to the configured
|
`UCE_SERVE_HTTP_FUNCTION` and rewrites `SCRIPT_FILENAME` to the configured
|
||||||
@ -412,15 +197,13 @@ broker loop:
|
|||||||
2. The message rides as `UCE_WS_MESSAGE` (base64) with an **empty STDIN body** —
|
2. The message rides as `UCE_WS_MESSAGE` (base64) with an **empty STDIN body** —
|
||||||
a non-empty STDIN makes the FastCGI transport flush a premature response
|
a non-empty STDIN makes the FastCGI transport flush a premature response
|
||||||
before `on_complete` ever runs.
|
before `on_complete` ever runs.
|
||||||
3. Connect to `FCGI_SOCKET_PATH` (non-blocking) and queue the encoded request in
|
3. Connect to `/run/uce.sock` (non-blocking) and queue the encoded request in
|
||||||
`ws_broker_outbound[fd]` with an enqueue timestamp.
|
`ws_broker_outbound[fd]`.
|
||||||
|
|
||||||
`ws_broker_drain_outbound()` runs after every `process(50)` tick: it finishes
|
`ws_broker_drain_outbound()` runs after every `process(50)` tick: it finishes
|
||||||
writing each queued request, then drains and discards the reply (the unit's
|
writing each queued request, then drains and discards the reply (the unit's
|
||||||
output comes back via the command socket, not this reply), closing the fd when
|
output comes back via the command socket, not this reply), closing the fd when
|
||||||
the worker closes its end. If a forward remains pending beyond
|
the worker closes its end.
|
||||||
`WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` (default `30`), the broker drops and
|
|
||||||
closes it so a wedged worker cannot pin broker fds/memory indefinitely.
|
|
||||||
|
|
||||||
### 6.2 Outbound: `ws_*` commands flushed back to the broker
|
### 6.2 Outbound: `ws_*` commands flushed back to the broker
|
||||||
|
|
||||||
@ -428,10 +211,8 @@ Any unit code — not just WebSocket handlers — may call `ws_send` / `ws_send_
|
|||||||
/ `ws_close`. In the workspace these **record dispatch commands** rather than
|
/ `ws_close`. In the workspace these **record dispatch commands** rather than
|
||||||
touching a socket (the workspace owns no connections); `wasm-core`'s `ws_*`
|
touching a socket (the workspace owns no connections); `wasm-core`'s `ws_*`
|
||||||
(`src/lib/sys.cpp`) append to `websocket_dispatch_commands`, and
|
(`src/lib/sys.cpp`) append to `websocket_dispatch_commands`, and
|
||||||
`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands`.
|
`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands` (plus
|
||||||
If the handler changed per-connection state, the core also emits
|
`ws_connection_state` if the handler mutated per-connection state).
|
||||||
`ws_connection_state` even when no commands were emitted, and the native
|
|
||||||
backend flushes this state-only batch to the broker.
|
|
||||||
|
|
||||||
`wasm_backend_serve` (`src/wasm/backend.cpp`) flushes that batch at workspace
|
`wasm_backend_serve` (`src/wasm/backend.cpp`) flushes that batch at workspace
|
||||||
teardown — in **any** scenario, not just WS handlers — to the broker's command
|
teardown — in **any** scenario, not just WS handlers — to the broker's command
|
||||||
@ -458,10 +239,9 @@ serve_http dispatcher uses, so there is no duplicated request-forwarding code.
|
|||||||
handlers (it renders nothing, so it accepts every request straight through to
|
handlers (it renders nothing, so it accepts every request straight through to
|
||||||
`on_complete`), wires `on_complete=ws_broker_complete` and
|
`on_complete`), wires `on_complete=ws_broker_complete` and
|
||||||
`on_websocket_message=ws_broker_ws_message`, listens on `HTTP_PORT` and the
|
`on_websocket_message=ws_broker_ws_message`, listens on `HTTP_PORT` and the
|
||||||
command socket, and loops `process(50)` + `drain_outbound(timeout)`. The
|
command socket, and loops `process(50)` + `drain_outbound()`. The design is
|
||||||
`timeout` comes from `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` (default `30`). The
|
**non-blocking outbound dispatch + async command-socket flush, all in the
|
||||||
design is **non-blocking outbound dispatch + async command-socket flush, all in
|
broker's single epoll loop** — the broker never blocks on a worker.
|
||||||
the broker's single epoll loop** — the broker never blocks on a worker.
|
|
||||||
|
|
||||||
The parent respawns the broker if it dies (`ws_broker_alive` / `ensure_ws_broker`
|
The parent respawns the broker if it dies (`ws_broker_alive` / `ensure_ws_broker`
|
||||||
in `main()`).
|
in `main()`).
|
||||||
@ -504,13 +284,10 @@ header free-functions are `inline`. The wasm backend exposes only declarations
|
|||||||
| Key | Default | Meaning |
|
| Key | Default | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `WASM_BACKEND_VERBOSE` | `0` | Emit `X-UCE-Wasm-*` workspace timing headers (benchmark only). |
|
| `WASM_BACKEND_VERBOSE` | `0` | Emit `X-UCE-Wasm-*` workspace timing headers (benchmark only). |
|
||||||
| `FCGI_SOCKET_PATH` | runtime-configured (`/run/uce/fastcgi.sock` in this doc) | Worker pool FastCGI socket (brokers forward here). |
|
| `FCGI_SOCKET_PATH` | `/run/uce.sock` | Worker pool FastCGI socket (brokers forward here). |
|
||||||
| `CLI_SOCKET_PATH` | `/run/uce/cli.sock` | Worker CLI/admin socket. Keep private; reference `CLI_SOCKET_MODE` is `0600`. |
|
| `CLI_SOCKET_PATH` | `/run/uce/cli.sock` | Worker CLI socket. |
|
||||||
| `FCGI_SOCKET_MODE` | `0666` | Permission mode applied to `FCGI_SOCKET_PATH` after bind; set tighter if nginx/Apache can use a trusted group. |
|
|
||||||
| `CLI_SOCKET_MODE` | `0600` | Permission mode applied to `CLI_SOCKET_PATH`; set `0660` only for a trusted admin group. |
|
|
||||||
| `HTTP_PORT` | `8080` | Raw HTTP + WebSocket port — owned by the WS broker. |
|
| `HTTP_PORT` | `8080` | Raw HTTP + WebSocket port — owned by the WS broker. |
|
||||||
| `WS_BROKER_SOCKET_PATH` | `/run/uce/ws-broker.sock` | Broker command socket for `ws_*` flushes. |
|
| `WS_BROKER_SOCKET_PATH` | `/run/uce/ws-broker.sock` | Broker command socket for `ws_*` flushes. |
|
||||||
| `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` | `30` | Max lifetime in seconds for queued WS broker forwards before drop. |
|
|
||||||
| `WORKER_COUNT` | `4` | Number of uniform worker processes. |
|
| `WORKER_COUNT` | `4` | Number of uniform worker processes. |
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -518,75 +295,11 @@ header free-functions are `inline`. The wasm backend exposes only declarations
|
|||||||
## 10. Testing
|
## 10. Testing
|
||||||
|
|
||||||
- **Regression gate**: `scripts/run_cli_tests.sh --include-wasm-kill` runs the
|
- **Regression gate**: `scripts/run_cli_tests.sh --include-wasm-kill` runs the
|
||||||
in-runtime CLI suite (`site/tests/cli_runner.uce`) plus the site test pages and
|
in-runtime CLI suite (`site/tests/cli_runner.uce`) plus the site test pages.
|
||||||
`scripts/test_dependency_invalidation.sh`. The shell helper runs the CLI suite
|
|
||||||
as named groups (`demo`, `http`, `site`, `doc-gate`, `security`,
|
|
||||||
`task-lifetime`, `starter`, `tcp`, and optional `wasm-kill`) so a slow or
|
|
||||||
failing group is visible before the rest of the gate runs. `doc-gate` can take
|
|
||||||
several minutes on a cold runtime because it compiles generated documentation
|
|
||||||
examples; `UCE_CLI_TEST_TIMEOUT` controls the per-group curl timeout and
|
|
||||||
defaults to 900 seconds. Individual doc-page checks retry transient empty
|
|
||||||
frontend reads, but still fail persistent status, marker, compile, runtime, or
|
|
||||||
placeholder-example errors. The dependency-invalidation gate changes a transitive
|
|
||||||
`#load`, then replaces a warmed worker artifact while preserving its
|
|
||||||
whole-second mtime to prove both compiler and worker caches invalidate it. It
|
|
||||||
also breaks a loaded dependency, verifies the parent compile fails, restores
|
|
||||||
the dependency byte-for-byte, and requires the parent to recover. Failed
|
|
||||||
builds persist the input signature that actually failed, so metadata from an
|
|
||||||
older successful artifact cannot indefinitely defer that rebuild.
|
|
||||||
also rejects an unreadable unit without publishing a wasm artifact, restores
|
|
||||||
its permissions and proves the next CLI request compiles it, then sends 48
|
|
||||||
requests and asserts the observed worker PID set does not exceed
|
|
||||||
`WORKER_COUNT`, guarding against accidental reintroduction of request-count
|
|
||||||
recycling. It also races a source edit against an active unit compile after
|
|
||||||
the generated C++ snapshot has been written. The compiler must either retry
|
|
||||||
and serve the post-edit source or fail closed; it must never stamp current
|
|
||||||
source metadata onto wasm produced from an older snapshot.
|
|
||||||
- **Core compatibility**: the production core owns and exports its growable
|
|
||||||
function table, enabling `InstancePre`. A separately built legacy
|
|
||||||
`--import-table` core must still pass the demo and 64-request pool-isolation
|
|
||||||
groups before the compatibility fallback changes.
|
|
||||||
`scripts/test_cold_component_deadline.sh` separately compiles a deliberately
|
|
||||||
cold component that exceeds the development epoch window and proves the
|
|
||||||
parent request still renders it. The focused shell gates create temporary
|
|
||||||
`.uce` units under `UCE_TEST_SITE_DIRECTORY`, or under the installed
|
|
||||||
`SITE_DIRECTORY` from `/etc/uce/settings.cfg` when no override is supplied.
|
|
||||||
The dependency-invalidation gate also holds parent and child compile locks
|
|
||||||
across a transitive source edit, proves a warmed HTTP request returns the
|
|
||||||
last atomically published result without waiting, proves a POST returns a
|
|
||||||
prompt retryable 503 without executing its old mutation handler, and verifies
|
|
||||||
demand-priority convergence. A CLI request still waits and returns only the
|
|
||||||
current dependency result after rebuild completion.
|
|
||||||
The component page also calls a parent once without its optional relative
|
|
||||||
child and again with that child activated. This guards the component-slot
|
|
||||||
cache invariant that a cache hit must restore both the function-table slot
|
|
||||||
and the resolved unit path; otherwise nested relative targets resolve from
|
|
||||||
the caller after a repeated parent invocation. Resolved component paths are
|
|
||||||
canonical absolute paths: equivalent spellings containing `.` or `..` must
|
|
||||||
share one source, compile, artifact, and module-cache identity. The compiler
|
|
||||||
independently enforces the same canonical identity. Within one resolution,
|
|
||||||
identical entry/site bases and identical raw/`.uce` candidate spellings are
|
|
||||||
probed only once while first-match order is preserved. Persisted failures are
|
|
||||||
reused only when their recorded source path matches the current request.
|
|
||||||
`scripts/test_nested_component_props.sh` passes a large prop tree through an
|
|
||||||
outer component, invokes 300 nested components, and verifies both inner and
|
|
||||||
caller props are restored. Its warm-request ceiling guards the request-scope
|
|
||||||
invariant that entering a nested component swaps the active prop trees in
|
|
||||||
constant time instead of deep-copying the outer tree for every child.
|
|
||||||
`scripts/test_mysql_epoch_refresh.sh` spends more than half of the configured
|
|
||||||
guest epoch on each side of a real MySQL query. It proves the blocking
|
|
||||||
database hostcall refreshes the guest deadline so cumulative request work is
|
|
||||||
allowed while a CPU loop without intervening host work still traps.
|
|
||||||
|
|
||||||
- **Log timeliness**: the base process line-buffers stdout before forking
|
|
||||||
workers, the proactive compiler, and the WebSocket broker. This keeps each
|
|
||||||
newline-terminated diagnostic attached to its actual journal time instead of
|
|
||||||
releasing an old block-buffered failure during unrelated later traffic. The
|
|
||||||
private CLI timing gate labels its deliberate invalid-source request as
|
|
||||||
`UCE expected compile error`; public and unmarked CLI compilation failures
|
|
||||||
retain the ordinary `UCE compile error` operator signal.
|
|
||||||
|
|
||||||
- **WebSocket end-to-end**: a headless client performs a raw WS handshake to
|
- **WebSocket end-to-end**: a headless client performs a raw WS handshake to
|
||||||
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
|
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
|
||||||
`SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full
|
`SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full
|
||||||
broker → worker → broker → client chain across process boundaries.
|
broker → worker → broker → client chain across process boundaries.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,32 +5,20 @@ SESSION_PATH=/var/lib/uce/sessions
|
|||||||
|
|
||||||
# LISTEN ON SOCKETS
|
# LISTEN ON SOCKETS
|
||||||
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||||
# FastCGI is normally consumed by the local web server user/group.
|
|
||||||
FCGI_SOCKET_MODE=0666
|
|
||||||
FCGI_PORT=9993
|
FCGI_PORT=9993
|
||||||
|
|
||||||
# HTTP-over-Unix command socket for local CLI/control tooling.
|
# HTTP-over-Unix command socket for local CLI/control tooling.
|
||||||
# Example: curl --unix-socket /run/uce/cli.sock http://localhost/ping
|
# Example: curl --unix-socket /run/uce/cli.sock http://localhost/ping
|
||||||
CLI_SOCKET_PATH=/run/uce/cli.sock
|
CLI_SOCKET_PATH=/run/uce/cli.sock
|
||||||
# Keep the CLI/admin socket private by default; set 0660 only for a trusted group.
|
|
||||||
CLI_SOCKET_MODE=0600
|
|
||||||
|
|
||||||
# Built-in HTTP/WebSocket listener used for WebSocket Upgrade requests.
|
# Built-in HTTP/WebSocket listener used for WebSocket Upgrade requests.
|
||||||
# Keep this behind nginx/Apache on localhost or firewall it from public access.
|
# Keep this behind nginx/Apache on localhost or firewall it from public access.
|
||||||
HTTP_PORT=8080
|
HTTP_PORT=8080
|
||||||
|
|
||||||
# After this many seconds, an in-flight WS broker render-forward that is still
|
|
||||||
# not completed (including stalled writes/reads) is dropped.
|
|
||||||
WS_BROKER_OUTBOUND_TIMEOUT_SECONDS=30
|
|
||||||
|
|
||||||
# OPTIONAL PROACTIVE COMPILE ROOT
|
# OPTIONAL PROACTIVE COMPILE ROOT
|
||||||
# Leave empty to scan SITE_DIRECTORY relative to the runtime root.
|
# Leave empty to scan SITE_DIRECTORY relative to the runtime root.
|
||||||
PRECOMPILE_FILES_IN=
|
PRECOMPILE_FILES_IN=
|
||||||
|
|
||||||
# NUMBER OF LOW-PRIORITY PROCESSES USED BY MANAGED GENERATION PRECOMPILE
|
|
||||||
# Two keeps one half of a typical four-vCPU host available to live workers.
|
|
||||||
PRECOMPILE_JOBS=2
|
|
||||||
|
|
||||||
# PUBLIC SITE DIRECTORY USED FOR STARTUP SCAN WHEN PRECOMPILE_FILES_IN IS EMPTY
|
# PUBLIC SITE DIRECTORY USED FOR STARTUP SCAN WHEN PRECOMPILE_FILES_IN IS EMPTY
|
||||||
SITE_DIRECTORY=site
|
SITE_DIRECTORY=site
|
||||||
|
|
||||||
@ -42,13 +30,10 @@ WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
|
|||||||
|
|
||||||
# WASM RUNTIME SETTINGS. Unit execution is always routed through wasm.
|
# WASM RUNTIME SETTINGS. Unit execution is always routed through wasm.
|
||||||
WASM_BACKEND_VERBOSE=0
|
WASM_BACKEND_VERBOSE=0
|
||||||
WASM_PROFILE_HOSTCALL_CPU=0
|
WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm
|
||||||
WASM_PROFILE_THREAD_RUNTIME=0
|
|
||||||
WASM_CORE_PATH=bin/wasm/core.wasm
|
|
||||||
WASM_MEMORY_LIMIT_BYTES=536870912
|
WASM_MEMORY_LIMIT_BYTES=536870912
|
||||||
WASM_EPOCH_DEADLINE_TICKS=200
|
WASM_EPOCH_DEADLINE_TICKS=200
|
||||||
WASM_EPOCH_PERIOD_MS=50
|
WASM_EPOCH_PERIOD_MS=50
|
||||||
MYSQL_PERSISTENT_POOL_SIZE=8
|
|
||||||
|
|
||||||
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
|
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
|
||||||
PROACTIVE_COMPILE_ENABLED=1
|
PROACTIVE_COMPILE_ENABLED=1
|
||||||
@ -96,14 +81,3 @@ ARCHIVE_MAX_ZIP_ENTRIES=4096
|
|||||||
|
|
||||||
# LIFETIME OF SESSION COOKIES IN SECONDS
|
# LIFETIME OF SESSION COOKIES IN SECONDS
|
||||||
SESSION_TIME=2592000
|
SESSION_TIME=2592000
|
||||||
# Set to 1 behind HTTPS-only frontends to add Secure to session cookies.
|
|
||||||
SESSION_COOKIE_SECURE=0
|
|
||||||
|
|
||||||
# DEVELOPER-DEFINED ERROR PAGES (see /doc/index.uce?p=error_pages and
|
|
||||||
# /doc/index.uce?p=3_blocked_functions). The unit receives the error details in
|
|
||||||
# context.call["error"]; comment out for the plain-text fallback instead.
|
|
||||||
page_runtime_error=site/errors/runtime-error.uce
|
|
||||||
|
|
||||||
# Comma-separated uce_host_* names to disable per server policy (empty = none).
|
|
||||||
# A unit calling a blocked hostcall fails into the error page above.
|
|
||||||
UCE_HOSTCALL_BLOCKLIST=
|
|
||||||
|
|||||||
@ -15,41 +15,35 @@ DOC_DIR = ROOT / "site" / "doc" / "pages"
|
|||||||
|
|
||||||
# name, needs_doc, status. status: public | internal | integration
|
# name, needs_doc, status. status: public | internal | integration
|
||||||
PUBLIC_APIS = [
|
PUBLIC_APIS = [
|
||||||
("http_request", True, "public"), ("http_request_async", True, "public"),
|
("shell_exec", True, "public"), ("shell_escape", True, "public"),
|
||||||
("shell_exec", True, "public"), ("shell_escape", True, "public"), ("shell_spawn", True, "public"),
|
|
||||||
("job_status", True, "public"), ("job_result", True, "public"), ("job_await", True, "public"), ("job_cancel", True, "public"),
|
|
||||||
("basename", True, "public"), ("dirname", True, "public"), ("path_join", True, "public"),
|
("basename", True, "public"), ("dirname", True, "public"), ("path_join", True, "public"),
|
||||||
("path_real", True, "public"), ("path_is_within", True, "public"),
|
("path_real", True, "public"), ("path_is_within", True, "public"),
|
||||||
("file_get_contents", True, "public"),
|
("file_open_locked", True, "public"), ("file_close_locked", True, "public"),
|
||||||
("file_put_contents", True, "public"), ("file_append", True, "public"),
|
("file_release_process_locks", True, "public"), ("file_get_contents_locked_fd", True, "public"),
|
||||||
|
("file_put_contents_locked_fd", True, "public"), ("file_get_contents", True, "public"),
|
||||||
|
("file_put_contents", True, "public"), ("file_append_contents", False, "public"),
|
||||||
("cwd_get", True, "public"), ("cwd_set", True, "public"), ("process_start_directory", True, "public"),
|
("cwd_get", True, "public"), ("cwd_set", True, "public"), ("process_start_directory", True, "public"),
|
||||||
("file_mtime", True, "public"), ("file_unlink", True, "public"), ("expand_path", True, "public"),
|
("file_mtime", True, "public"), ("file_unlink", True, "public"), ("expand_path", True, "public"),
|
||||||
("ls", True, "public"), ("to_u64", True, "public"), ("to_s64", True, "public"),
|
("ls", True, "public"), ("config_map_u64", True, "public"), ("config_map_f64", True, "public"),
|
||||||
("to_f64", True, "public"), ("to_bool", True, "public"),
|
("config_bool_value", True, "public"), ("config_map_bool", True, "public"),
|
||||||
|
("config_u64", True, "public"), ("config_f64", True, "public"), ("config_bool", True, "public"),
|
||||||
("request_perf", True, "public"), ("time_format_local", True, "public"),
|
("request_perf", True, "public"), ("time_format_local", True, "public"),
|
||||||
("time_format_relative", True, "public"), ("time_parse", True, "public"),
|
("time_format_relative", True, "public"), ("time_parse", True, "public"),
|
||||||
("backtrace_get_frames", False, "public"), ("backtrace_capture", False, "public"),
|
("backtrace_frames_string", False, "public"), ("capture_backtrace_string", False, "public"),
|
||||||
("signal_name", False, "public"), ("memcache_escape_key", True, "public"),
|
("signal_name", False, "public"), ("memcache_escape_key", True, "public"),
|
||||||
("memcache_escape_keys", True, "public"), ("memcache_command", True, "public"),
|
("memcache_escape_keys", True, "public"), ("memcache_command", True, "public"),
|
||||||
("memcache_get_multiple", True, "public"), ("runtime_safe_key", True, "public"),
|
("memcache_get_multiple", True, "public"), ("runtime_safe_key", True, "public"),
|
||||||
("float_val", True, "public"), ("nibble", True, "public"), ("json_consume_space", False, "public"),
|
("float_val", True, "public"), ("nibble", True, "public"), ("json_consume_space", False, "public"),
|
||||||
("array_merge", True, "public"), ("safe_name", True, "public"), ("ascii_safe_name", True, "public"),
|
("array_merge", True, "public"), ("safe_name", True, "public"), ("ascii_safe_name", True, "public"),
|
||||||
("to_json", False, "public"), ("remove", False, "public"), ("clear", False, "public"),
|
("to_json", False, "public"), ("remove", False, "public"), ("clear", False, "public"),
|
||||||
("gen_sha1", True, "public"), ("sha256", True, "public"), ("sha256_hex", True, "public"),
|
("gen_sha1", True, "public"), ("gen_noise32", True, "public"), ("gen_noise64", True, "public"),
|
||||||
("hmac_sha256", True, "public"), ("hmac_sha256_hex", True, "public"), ("random_bytes", True, "public"),
|
|
||||||
("crypto_equal", True, "public"), ("password_hash", True, "public"),
|
|
||||||
("password_verify", True, "public"), ("password_needs_rehash", True, "public"),
|
|
||||||
("gen_noise32", True, "public"), ("gen_noise64", True, "public"),
|
|
||||||
("gen_noise01", True, "public"), ("gen_int", True, "public"), ("gen_float", True, "public"),
|
("gen_noise01", True, "public"), ("gen_int", True, "public"), ("gen_float", True, "public"),
|
||||||
("draw_int", True, "public"), ("draw_float", True, "public"),
|
("draw_int", True, "public"), ("draw_float", True, "public"),
|
||||||
("encode_query", True, "public"), ("csrf_token", True, "public"),
|
("encode_query", True, "public"), ("request_script_url", True, "public"),
|
||||||
("csrf_valid", True, "public"), ("csrf_rotate", True, "public"),
|
|
||||||
("request_script_url", True, "public"),
|
|
||||||
("request_base_url", True, "public"), ("request_route_from_raw_path", True, "public"),
|
("request_base_url", True, "public"), ("request_route_from_raw_path", True, "public"),
|
||||||
("cli_arg", True, "public"), ("unit_compile", True, "public"),
|
("cli_arg", True, "public"), ("unit_compile", True, "public"),
|
||||||
("cleanup_sqlite_connections", False, "internal"), ("cleanup_mysql_connections", False, "internal"),
|
("cleanup_sqlite_connections", False, "internal"), ("cleanup_mysql_connections", False, "internal"),
|
||||||
("mysql_connect", True, "integration"), ("mysql_query", True, "integration"),
|
("mysql_connect", True, "integration"), ("mysql_query", True, "integration"),
|
||||||
("mysql_affected_rows", True, "integration"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
REMOVED_APIS = ["unit_load", "concat"]
|
REMOVED_APIS = ["unit_load", "concat"]
|
||||||
|
|||||||
@ -21,8 +21,7 @@ fi
|
|||||||
-Wl,--export-all \
|
-Wl,--export-all \
|
||||||
-Wl,--export=__heap_base \
|
-Wl,--export=__heap_base \
|
||||||
-Wl,--export=__stack_pointer \
|
-Wl,--export=__stack_pointer \
|
||||||
-Wl,--export-table \
|
-Wl,--import-table \
|
||||||
-Wl,--growable-table \
|
|
||||||
-Wl,-z,stack-size=8388608 \
|
-Wl,-z,stack-size=8388608 \
|
||||||
-Wl,--allow-undefined-file=src/wasm/core_hostcalls.syms \
|
-Wl,--allow-undefined-file=src/wasm/core_hostcalls.syms \
|
||||||
-Wl,--no-entry \
|
-Wl,--no-entry \
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
@ -7,14 +6,11 @@ BUILDMODE=${2:-"debug"}
|
|||||||
OPT_FLAG="O0"
|
OPT_FLAG="O0"
|
||||||
GF="uce_fastcgi"
|
GF="uce_fastcgi"
|
||||||
|
|
||||||
mkdir -p bin/tmp bin/assets bin/wasm work
|
mkdir bin > /dev/null 2>&1
|
||||||
exec 9>bin/.build.lock
|
mkdir bin/tmp > /dev/null 2>&1
|
||||||
flock 9
|
mkdir bin/assets > /dev/null 2>&1
|
||||||
build_tmp_files=()
|
mkdir bin/wasm > /dev/null 2>&1
|
||||||
cleanup_build_tmp() {
|
mkdir work > /dev/null 2>&1
|
||||||
((${#build_tmp_files[@]} == 0)) || rm -f "${build_tmp_files[@]}"
|
|
||||||
}
|
|
||||||
trap cleanup_build_tmp EXIT
|
|
||||||
|
|
||||||
COMPILER="clang++"
|
COMPILER="clang++"
|
||||||
# -rdynamic is a link-time flag; the -c compiles below do not need it.
|
# -rdynamic is a link-time flag; the -c compiles below do not need it.
|
||||||
@ -25,7 +21,7 @@ WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
|
|||||||
WASM_FLAGS="-I$WASMTIME_HOME/include"
|
WASM_FLAGS="-I$WASMTIME_HOME/include"
|
||||||
WASM_LIBS="-L$WASMTIME_HOME/lib -Wl,-rpath,$WASMTIME_HOME/lib -lwasmtime"
|
WASM_LIBS="-L$WASMTIME_HOME/lib -Wl,-rpath,$WASMTIME_HOME/lib -lwasmtime"
|
||||||
|
|
||||||
LIBS="-ldl -lm -lpthread -lpcre2-8 -lcrypto `mysql_config --cflags --libs` $WASM_LIBS"
|
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs` $WASM_LIBS"
|
||||||
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
|
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
|
||||||
|
|
||||||
# The runtime is split into separately-compiled objects so an edit to one
|
# The runtime is split into separately-compiled objects so an edit to one
|
||||||
@ -56,16 +52,13 @@ fi
|
|||||||
# SQLite: vendored C, depends only on its own source (not our headers).
|
# SQLite: vendored C, depends only on its own source (not our headers).
|
||||||
if needs_rebuild bin/sqlite3.o src/3rdparty/sqlite/sqlite3.c src/3rdparty/sqlite/sqlite3.h; then
|
if needs_rebuild bin/sqlite3.o src/3rdparty/sqlite/sqlite3.c src/3rdparty/sqlite/sqlite3.h; then
|
||||||
echo "Compiling SQLite..."
|
echo "Compiling SQLite..."
|
||||||
tmp="bin/sqlite3.o.tmp.$$"
|
|
||||||
build_tmp_files+=("$tmp")
|
|
||||||
clang -g -O2 -fPIC \
|
clang -g -O2 -fPIC \
|
||||||
-DSQLITE_THREADSAFE=1 \
|
-DSQLITE_THREADSAFE=1 \
|
||||||
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
|
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
|
||||||
-DSQLITE_DQS=0 \
|
-DSQLITE_DQS=0 \
|
||||||
-DSQLITE_DEFAULT_FOREIGN_KEYS=1 \
|
-DSQLITE_DEFAULT_FOREIGN_KEYS=1 \
|
||||||
-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \
|
-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \
|
||||||
-c src/3rdparty/sqlite/sqlite3.c -o "$tmp" 2>&1
|
-c src/3rdparty/sqlite/sqlite3.c -o bin/sqlite3.o 2>&1 || exit 1
|
||||||
mv "$tmp" bin/sqlite3.o
|
|
||||||
else
|
else
|
||||||
echo "Reusing bin/sqlite3.o"
|
echo "Reusing bin/sqlite3.o"
|
||||||
fi
|
fi
|
||||||
@ -74,10 +67,7 @@ fi
|
|||||||
# declarations (not the lib .cpp — those are compiled into main.o).
|
# declarations (not the lib .cpp — those are compiled into main.o).
|
||||||
if needs_rebuild bin/wasm.o src/wasm src/lib/*.h; then
|
if needs_rebuild bin/wasm.o src/wasm src/lib/*.h; then
|
||||||
echo "Compiling wasm backend..."
|
echo "Compiling wasm backend..."
|
||||||
tmp="bin/wasm.o.tmp.$$"
|
time -p $COMPILER -c src/wasm/wasm_module.cpp $SRCFLAGS $FLAGS $WASM_FLAGS -o bin/wasm.o 2>&1 || exit 1
|
||||||
build_tmp_files+=("$tmp")
|
|
||||||
time -p $COMPILER -c src/wasm/wasm_module.cpp $SRCFLAGS $FLAGS $WASM_FLAGS -o "$tmp" 2>&1
|
|
||||||
mv "$tmp" bin/wasm.o
|
|
||||||
else
|
else
|
||||||
echo "Reusing bin/wasm.o"
|
echo "Reusing bin/wasm.o"
|
||||||
fi
|
fi
|
||||||
@ -87,19 +77,18 @@ fi
|
|||||||
# (its only view of the wasm object) — but not the wasm .cpp sources.
|
# (its only view of the wasm object) — but not the wasm .cpp sources.
|
||||||
if needs_rebuild bin/main.o src/linux_fastcgi.cpp src/lib src/fastcgi src/wasm/backend.h; then
|
if needs_rebuild bin/main.o src/linux_fastcgi.cpp src/lib src/fastcgi src/wasm/backend.h; then
|
||||||
echo "Compiling main..."
|
echo "Compiling main..."
|
||||||
tmp="bin/main.o.tmp.$$"
|
time -p $COMPILER -c src/linux_fastcgi.cpp $SRCFLAGS $FLAGS -o bin/main.o 2>&1 || exit 1
|
||||||
build_tmp_files+=("$tmp")
|
|
||||||
time -p $COMPILER -c src/linux_fastcgi.cpp $SRCFLAGS $FLAGS -o "$tmp" 2>&1
|
|
||||||
mv "$tmp" bin/main.o
|
|
||||||
else
|
else
|
||||||
echo "Reusing bin/main.o"
|
echo "Reusing bin/main.o"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Linking..."
|
echo "Linking..."
|
||||||
binary_tmp="bin/$GF.linux.bin.tmp.$$"
|
$COMPILER -rdynamic bin/main.o bin/wasm.o bin/sqlite3.o $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
|
||||||
build_tmp_files+=("$binary_tmp")
|
|
||||||
$COMPILER -rdynamic bin/main.o bin/wasm.o bin/sqlite3.o $FLAGS $LIBS -o "$binary_tmp" 2>&1
|
if [ $? -eq 0 ]
|
||||||
test -s "$binary_tmp"
|
then
|
||||||
chmod 0755 "$binary_tmp"
|
ls -lh bin/ | grep $GF
|
||||||
mv "$binary_tmp" "bin/$GF.linux.bin"
|
exit 0
|
||||||
ls -lh "bin/$GF.linux.bin"
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Extract a compact address-to-source table before a unit's DWARF is stripped."""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import ast
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
DIRECTORY = re.compile(r'^include_directories\[\s*(\d+)\] = (".*")$')
|
|
||||||
FILE = re.compile(r'^file_names\[\s*(\d+)\]:$')
|
|
||||||
ROW = re.compile(r'^0x([0-9a-fA-F]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+')
|
|
||||||
|
|
||||||
|
|
||||||
def quoted(value: str) -> str:
|
|
||||||
return ast.literal_eval(value)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--dwarfdump", required=True)
|
|
||||||
parser.add_argument("--wasm", required=True)
|
|
||||||
parser.add_argument("--module", required=True)
|
|
||||||
parser.add_argument("--output", required=True)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
text = subprocess.run(
|
|
||||||
[args.dwarfdump, "--debug-line", args.wasm],
|
|
||||||
check=True,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
).stdout
|
|
||||||
directories: dict[int, str] = {}
|
|
||||||
files: dict[int, tuple[str, int]] = {}
|
|
||||||
rows: list[tuple[int, int, int, int]] = []
|
|
||||||
current_file = 0
|
|
||||||
for line in text.splitlines():
|
|
||||||
if match := DIRECTORY.match(line):
|
|
||||||
directories[int(match.group(1))] = quoted(match.group(2))
|
|
||||||
continue
|
|
||||||
if match := FILE.match(line):
|
|
||||||
current_file = int(match.group(1))
|
|
||||||
continue
|
|
||||||
stripped = line.strip()
|
|
||||||
if current_file and stripped.startswith("name: "):
|
|
||||||
files[current_file] = (quoted(stripped[6:]), 0)
|
|
||||||
continue
|
|
||||||
if current_file and stripped.startswith("dir_index: "):
|
|
||||||
name, _ = files[current_file]
|
|
||||||
files[current_file] = (name, int(stripped[11:]))
|
|
||||||
continue
|
|
||||||
if match := ROW.match(line):
|
|
||||||
rows.append(tuple(int(value, 16 if index == 0 else 10) for index, value in enumerate(match.groups())))
|
|
||||||
|
|
||||||
paths: dict[int, str] = {}
|
|
||||||
for file_id, (name, directory_id) in files.items():
|
|
||||||
directory = directories.get(directory_id, "")
|
|
||||||
path = name if os.path.isabs(name) else os.path.normpath(os.path.join(directory, name))
|
|
||||||
if not os.path.isabs(path):
|
|
||||||
path = os.path.abspath(path)
|
|
||||||
if "\t" in path or "\n" in path or "\r" in path:
|
|
||||||
raise ValueError(f"source-map path contains a control character: {path!r}")
|
|
||||||
paths[file_id] = path
|
|
||||||
|
|
||||||
with open(args.output, "w", encoding="utf-8") as output:
|
|
||||||
output.write(f"UCE_SOURCE_MAP_V1\t{args.module}\n")
|
|
||||||
for file_id, path in sorted(paths.items()):
|
|
||||||
output.write(f"F\t{file_id}\t{path}\n")
|
|
||||||
for address, line, column, file_id in rows:
|
|
||||||
output.write(f"L\t{address:x}\t{file_id}\t{line}\t{column}\n")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -160,13 +160,7 @@ def defined_symbols(path: Path, llvm_nm: str) -> list[str]:
|
|||||||
def main() -> int:
|
def main() -> int:
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("wasm", type=Path)
|
ap.add_argument("wasm", type=Path)
|
||||||
default_abi = "7"
|
ap.add_argument("--abi-version", default="6")
|
||||||
abi_header = Path(__file__).resolve().parents[1] / "src" / "wasm" / "abi.h"
|
|
||||||
for line in abi_header.read_text().splitlines():
|
|
||||||
if line.startswith("#define UCE_WASM_CORE_ABI_VERSION "):
|
|
||||||
default_abi = line.rsplit(" ", 1)[1]
|
|
||||||
break
|
|
||||||
ap.add_argument("--abi-version", default=default_abi)
|
|
||||||
ap.add_argument("--llvm-nm", default=None)
|
ap.add_argument("--llvm-nm", default=None)
|
||||||
ap.add_argument("--verbose", action="store_true")
|
ap.add_argument("--verbose", action="store_true")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
@ -188,9 +182,6 @@ def main() -> int:
|
|||||||
for needle in required:
|
for needle in required:
|
||||||
if needle not in abi_text:
|
if needle not in abi_text:
|
||||||
errors.append(f"uce.abi missing {needle!r}")
|
errors.append(f"uce.abi missing {needle!r}")
|
||||||
module_payloads = customs.get("uce.module", [])
|
|
||||||
if not module_payloads or not module_payloads[-1]:
|
|
||||||
errors.append("missing uce.module custom section")
|
|
||||||
export_names = {name for name, _ in exports}
|
export_names = {name for name, _ in exports}
|
||||||
forbidden_exports = {"uce_alloc", "uce_free"}
|
forbidden_exports = {"uce_alloc", "uce_free"}
|
||||||
for name in sorted(export_names & forbidden_exports):
|
for name in sorted(export_names & forbidden_exports):
|
||||||
|
|||||||
@ -9,21 +9,17 @@ DEST_DIR="$2"
|
|||||||
SRC_FN="$3"
|
SRC_FN="$3"
|
||||||
PP_FN="$4"
|
PP_FN="$4"
|
||||||
WASM_FN="$5"
|
WASM_FN="$5"
|
||||||
UNIT_CACHE_DIR="${6:-$DEST_DIR}"
|
|
||||||
|
|
||||||
SDK=${WASI_SDK:-/opt/wasi-sdk}
|
SDK=${WASI_SDK:-/opt/wasi-sdk}
|
||||||
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-$(awk '/^#define UCE_WASM_CORE_ABI_VERSION / {print $3; exit}' src/wasm/abi.h)}
|
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
|
||||||
ROOT=$(pwd)
|
ROOT=$(pwd)
|
||||||
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
|
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
|
||||||
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
|
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
|
||||||
MODULE_TMP="$DEST_DIR/$PP_FN.uce-module.txt"
|
|
||||||
WASM_TMP="$DEST_DIR/$WASM_FN.tmp.$$"
|
|
||||||
MAP_TMP="$DEST_DIR/$WASM_FN.source-map.tmp.$$"
|
|
||||||
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
|
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
|
||||||
PCH_DIR=${UCE_WASM_PCH_DIR:-$UNIT_CACHE_DIR/.pch}
|
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
|
||||||
COMMON_FLAGS=(
|
COMMON_FLAGS=(
|
||||||
--target=wasm32-wasip1
|
--target=wasm32-wasip1
|
||||||
-fPIC -fvisibility=hidden -fvisibility-inlines-hidden
|
-fPIC -fvisibility=default -fvisibility-inlines-hidden
|
||||||
-O1 -g -std=c++20
|
-O1 -g -std=c++20
|
||||||
# The server captures this script's output and treats any non-empty result as
|
# The server captures this script's output and treats any non-empty result as
|
||||||
# a compile failure (then drops the .wasm), so a successful build must be
|
# a compile failure (then drops the .wasm), so a successful build must be
|
||||||
@ -36,7 +32,7 @@ COMMON_FLAGS=(
|
|||||||
-DPLATFORM_NAME=\"wasm32-wasip1\"
|
-DPLATFORM_NAME=\"wasm32-wasip1\"
|
||||||
)
|
)
|
||||||
|
|
||||||
if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ] || [ ! -x "$SDK/bin/llvm-dwarfdump" ]; then
|
if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ]; then
|
||||||
echo "wasi-sdk tools not found; set WASI_SDK" >&2
|
echo "wasi-sdk tools not found; set WASI_SDK" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@ -46,29 +42,21 @@ HEADER_HASH=$(find src/lib -maxdepth 1 -name '*.h' -type f -print0 | sort -z | x
|
|||||||
FLAGS_HASH=$(printf '%s\0' "${COMMON_FLAGS[@]}" -Isrc/lib | sha1sum | cut -c1-16)
|
FLAGS_HASH=$(printf '%s\0' "${COMMON_FLAGS[@]}" -Isrc/lib | sha1sum | cut -c1-16)
|
||||||
PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH" "$FLAGS_HASH" | sha1sum | cut -c1-16)
|
PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH" "$FLAGS_HASH" | sha1sum | cut -c1-16)
|
||||||
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
|
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
|
||||||
PCH_TMP="$PCH_FN.tmp.$$"
|
|
||||||
|
|
||||||
mkdir -p "$DEST_DIR" >/dev/null 2>&1
|
mkdir -p "$DEST_DIR" >/dev/null 2>&1
|
||||||
trap 'rm -f "$OBJ_FN" "$ABI_TMP" "$MODULE_TMP" "$WASM_TMP" "$MAP_TMP" "$PCH_TMP"' EXIT
|
|
||||||
|
|
||||||
build_pch_if_needed() {
|
build_pch_if_needed() {
|
||||||
if [ "$PCH_ENABLED" = "0" ]; then
|
if [ "$PCH_ENABLED" = "0" ]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
mkdir -p "$PCH_DIR"
|
mkdir -p "$PCH_DIR"
|
||||||
exec 9>"$PCH_FN.lock"
|
|
||||||
flock 9
|
|
||||||
if [ -s "$PCH_FN" ] && [ -z "$(find src/lib -maxdepth 1 -name '*.h' -type f -newer "$PCH_FN" -print -quit)" ]; then
|
if [ -s "$PCH_FN" ] && [ -z "$(find src/lib -maxdepth 1 -name '*.h' -type f -newer "$PCH_FN" -print -quit)" ]; then
|
||||||
flock -u 9
|
|
||||||
exec 9>&-
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
|
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
|
||||||
-Isrc/lib \
|
-Isrc/lib \
|
||||||
-x c++-header src/lib/uce_lib.h -o "$PCH_TMP"
|
-x c++-header src/lib/uce_lib.h -o "$PCH_FN.tmp"
|
||||||
mv "$PCH_TMP" "$PCH_FN"
|
mv "$PCH_FN.tmp" "$PCH_FN"
|
||||||
flock -u 9
|
|
||||||
exec 9>&-
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cat > "$ABI_TMP" <<EOF
|
cat > "$ABI_TMP" <<EOF
|
||||||
@ -77,7 +65,6 @@ unit_abi_version=$ABI_VERSION
|
|||||||
toolchain=$TOOLCHAIN_ID
|
toolchain=$TOOLCHAIN_ID
|
||||||
source=$SRC_FN
|
source=$SRC_FN
|
||||||
EOF
|
EOF
|
||||||
printf '%s' "$(basename "$WASM_TMP")" > "$MODULE_TMP"
|
|
||||||
|
|
||||||
build_pch_if_needed
|
build_pch_if_needed
|
||||||
PCH_FLAGS=()
|
PCH_FLAGS=()
|
||||||
@ -91,10 +78,9 @@ fi
|
|||||||
-c "$DEST_DIR/$PP_FN" -o "$OBJ_FN"
|
-c "$DEST_DIR/$PP_FN" -o "$OBJ_FN"
|
||||||
|
|
||||||
"$SDK/bin/wasm-ld" -shared --experimental-pic \
|
"$SDK/bin/wasm-ld" -shared --experimental-pic \
|
||||||
--gc-sections \
|
|
||||||
--unresolved-symbols=import-dynamic \
|
--unresolved-symbols=import-dynamic \
|
||||||
--Bsymbolic \
|
--Bsymbolic \
|
||||||
"$OBJ_FN" -o "$WASM_TMP" \
|
"$OBJ_FN" -o "$DEST_DIR/$WASM_FN" \
|
||||||
--export-if-defined=__uce_set_current_request \
|
--export-if-defined=__uce_set_current_request \
|
||||||
--export-if-defined=__uce_render \
|
--export-if-defined=__uce_render \
|
||||||
--export-if-defined=__uce_component \
|
--export-if-defined=__uce_component \
|
||||||
@ -104,19 +90,8 @@ fi
|
|||||||
--export-if-defined=__uce_once \
|
--export-if-defined=__uce_once \
|
||||||
--export-if-defined=__uce_init
|
--export-if-defined=__uce_init
|
||||||
|
|
||||||
python3 scripts/build_unit_source_map.py \
|
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$DEST_DIR/$WASM_FN"
|
||||||
--dwarfdump "$SDK/bin/llvm-dwarfdump" \
|
|
||||||
--wasm "$WASM_TMP" \
|
|
||||||
--module "$(basename "$WASM_TMP")" \
|
|
||||||
--output "$MAP_TMP"
|
|
||||||
|
|
||||||
"$SDK/bin/llvm-objcopy" --strip-debug \
|
python3 scripts/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
|
||||||
--add-section=uce.abi="$ABI_TMP" \
|
|
||||||
--add-section=uce.module="$MODULE_TMP" \
|
|
||||||
"$WASM_TMP"
|
|
||||||
|
|
||||||
python3 scripts/check_unit_wasm.py "$WASM_TMP" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
|
rm -f "$OBJ_FN" "$ABI_TMP"
|
||||||
mv "$MAP_TMP" "$DEST_DIR/$WASM_FN.source-map"
|
|
||||||
mv "$WASM_TMP" "$DEST_DIR/$WASM_FN"
|
|
||||||
|
|
||||||
rm -f "$OBJ_FN" "$ABI_TMP" "$MODULE_TMP"
|
|
||||||
|
|||||||
@ -4,8 +4,7 @@ set -e
|
|||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||||
if [ "$1" = "configure" ]; then
|
if [ "$1" = "configure" ]; then
|
||||||
systemctl enable uce.socket uce.service >/dev/null 2>&1 || true
|
systemctl enable uce.service >/dev/null 2>&1 || true
|
||||||
systemctl start uce.socket >/dev/null 2>&1 || true
|
|
||||||
systemctl restart uce.service >/dev/null 2>&1 || true
|
systemctl restart uce.service >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -3,7 +3,7 @@ set -e
|
|||||||
|
|
||||||
if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then
|
if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl disable --now uce.service uce.socket >/dev/null 2>&1 || true
|
systemctl disable --now uce.service >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,21 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=UCE FastCGI Runtime
|
Description=UCE FastCGI Runtime
|
||||||
After=network-online.target mariadb.service memcached.service uce.socket
|
After=network-online.target mariadb.service memcached.service
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
Requires=uce.socket
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/usr/lib/uce
|
WorkingDirectory=/usr/lib/uce
|
||||||
RuntimeDirectory=uce
|
RuntimeDirectory=uce
|
||||||
RuntimeDirectoryPreserve=yes
|
|
||||||
StateDirectory=uce
|
StateDirectory=uce
|
||||||
CacheDirectory=uce
|
CacheDirectory=uce
|
||||||
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
|
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
|
||||||
|
ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||||
ExecStart=/usr/lib/uce/bin/uce_fastcgi.linux.bin
|
ExecStart=/usr/lib/uce/bin/uce_fastcgi.linux.bin
|
||||||
ExecStartPost=/usr/bin/bash /usr/lib/uce/scripts/systemd/wait-ready.sh
|
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
TimeoutStopSec=30
|
TimeoutStopSec=15
|
||||||
KillMode=mixed
|
KillMode=mixed
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=UCE FastCGI socket
|
|
||||||
|
|
||||||
[Socket]
|
|
||||||
ListenStream=/run/uce/fastcgi.sock
|
|
||||||
FileDescriptorName=fastcgi
|
|
||||||
SocketUser=www-data
|
|
||||||
SocketGroup=www-data
|
|
||||||
SocketMode=0660
|
|
||||||
DirectoryMode=0755
|
|
||||||
Backlog=100
|
|
||||||
Service=uce.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=sockets.target
|
|
||||||
@ -53,18 +53,13 @@ require_command() {
|
|||||||
|
|
||||||
verify_tree() {
|
verify_tree() {
|
||||||
local root="$1"
|
local root="$1"
|
||||||
for tool in clang++ wasm-ld llvm-objcopy llvm-nm llvm-dwarfdump; do
|
for tool in clang++ wasm-ld llvm-objcopy llvm-nm; do
|
||||||
if [[ ! -x "$root/bin/$tool" ]]; then
|
if [[ ! -x "$root/bin/$tool" ]]; then
|
||||||
echo "Missing WASI SDK tool: $root/bin/$tool" >&2
|
echo "Missing WASI SDK tool: $root/bin/$tool" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if ! command -v curl >/dev/null 2>&1; then
|
|
||||||
echo "Missing runtime dependency: curl (required by UCE http_request/http_request_async)" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
"$root/bin/clang++" --version | head -n 1
|
"$root/bin/clang++" --version | head -n 1
|
||||||
curl --version | head -n 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ $check_only -eq 1 ]]; then
|
if [[ $check_only -eq 1 ]]; then
|
||||||
|
|||||||
@ -205,7 +205,6 @@ bundle_wasmtime "$STAGE_DIR"
|
|||||||
|
|
||||||
write_packaged_settings "$STAGE_DIR/etc/uce/settings.cfg" "$WEBROOT"
|
write_packaged_settings "$STAGE_DIR/etc/uce/settings.cfg" "$WEBROOT"
|
||||||
install -m 0644 "$DEB_ASSET_DIR/uce.service" "$STAGE_DIR/lib/systemd/system/uce.service"
|
install -m 0644 "$DEB_ASSET_DIR/uce.service" "$STAGE_DIR/lib/systemd/system/uce.service"
|
||||||
install -m 0644 "$DEB_ASSET_DIR/uce.socket" "$STAGE_DIR/lib/systemd/system/uce.socket"
|
|
||||||
install -m 0644 "$DEB_ASSET_DIR/conffiles" "$DEBIAN_DIR/conffiles"
|
install -m 0644 "$DEB_ASSET_DIR/conffiles" "$DEBIAN_DIR/conffiles"
|
||||||
install -m 0755 "$DEB_ASSET_DIR/postinst" "$DEBIAN_DIR/postinst"
|
install -m 0755 "$DEB_ASSET_DIR/postinst" "$DEBIAN_DIR/postinst"
|
||||||
install -m 0755 "$DEB_ASSET_DIR/prerm" "$DEBIAN_DIR/prerm"
|
install -m 0755 "$DEB_ASSET_DIR/prerm" "$DEBIAN_DIR/prerm"
|
||||||
|
|||||||
@ -179,7 +179,6 @@ bundle_wasi_sdk "$STAGE_DIR"
|
|||||||
bundle_wasmtime "$STAGE_DIR"
|
bundle_wasmtime "$STAGE_DIR"
|
||||||
write_packaged_settings "$STAGE_DIR/etc/uce/settings.cfg" "$WEBROOT"
|
write_packaged_settings "$STAGE_DIR/etc/uce/settings.cfg" "$WEBROOT"
|
||||||
install -m 0644 "$REPO_ROOT/scripts/deb/uce.service" "$STAGE_DIR/usr/lib/systemd/system/uce.service"
|
install -m 0644 "$REPO_ROOT/scripts/deb/uce.service" "$STAGE_DIR/usr/lib/systemd/system/uce.service"
|
||||||
install -m 0644 "$REPO_ROOT/scripts/deb/uce.socket" "$STAGE_DIR/usr/lib/systemd/system/uce.socket"
|
|
||||||
|
|
||||||
(
|
(
|
||||||
cd "$STAGE_DIR"
|
cd "$STAGE_DIR"
|
||||||
@ -226,14 +225,13 @@ cp -a %{_builddir}/$PACKAGE_NAME-$VERSION/. %{buildroot}/
|
|||||||
%post
|
%post
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||||
systemctl enable uce.socket uce.service >/dev/null 2>&1 || true
|
systemctl enable uce.service >/dev/null 2>&1 || true
|
||||||
systemctl start uce.socket >/dev/null 2>&1 || true
|
|
||||||
systemctl restart uce.service >/dev/null 2>&1 || true
|
systemctl restart uce.service >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
%preun
|
%preun
|
||||||
if [ "\$1" = "0" ] && command -v systemctl >/dev/null 2>&1; then
|
if [ "\$1" = "0" ] && command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl disable --now uce.service uce.socket >/dev/null 2>&1 || true
|
systemctl disable --now uce.service >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
%postun
|
%postun
|
||||||
@ -247,7 +245,6 @@ fi
|
|||||||
%config(noreplace) /etc/uce/settings.cfg
|
%config(noreplace) /etc/uce/settings.cfg
|
||||||
/usr/lib/uce
|
/usr/lib/uce
|
||||||
/usr/lib/systemd/system/uce.service
|
/usr/lib/systemd/system/uce.service
|
||||||
/usr/lib/systemd/system/uce.socket
|
|
||||||
$WEBROOT
|
$WEBROOT
|
||||||
%dir /var/cache/uce
|
%dir /var/cache/uce
|
||||||
%dir /var/lib/uce
|
%dir /var/lib/uce
|
||||||
|
|||||||
@ -3,7 +3,6 @@ set -euo pipefail
|
|||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
socket_path="${UCE_CLI_SOCKET:-/run/uce/cli.sock}"
|
socket_path="${UCE_CLI_SOCKET:-/run/uce/cli.sock}"
|
||||||
curl_timeout="${UCE_CLI_TEST_TIMEOUT:-900}"
|
|
||||||
if [[ -z "${UCE_CLI_SOCKET:-}" && -r /etc/uce/settings.cfg ]]; then
|
if [[ -z "${UCE_CLI_SOCKET:-}" && -r /etc/uce/settings.cfg ]]; then
|
||||||
configured_socket=$(awk -F= '/^[[:space:]]*CLI_SOCKET_PATH[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
configured_socket=$(awk -F= '/^[[:space:]]*CLI_SOCKET_PATH[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||||
if [[ -n "${configured_socket:-}" ]]; then
|
if [[ -n "${configured_socket:-}" ]]; then
|
||||||
@ -12,7 +11,6 @@ if [[ -z "${UCE_CLI_SOCKET:-}" && -r /etc/uce/settings.cfg ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
include_kill=0
|
include_kill=0
|
||||||
skip_local_service_pages=0
|
|
||||||
action="run"
|
action="run"
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@ -20,22 +18,15 @@ while [[ $# -gt 0 ]]; do
|
|||||||
include_kill=1
|
include_kill=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--skip-local-service-pages)
|
|
||||||
skip_local_service_pages=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--list)
|
--list)
|
||||||
action="list"
|
action="list"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
cat <<'USAGE'
|
cat <<'USAGE'
|
||||||
Usage: scripts/run_cli_tests.sh [--include-wasm-kill] [--skip-local-service-pages] [--list]
|
Usage: scripts/run_cli_tests.sh [--include-wasm-kill] [--list]
|
||||||
|
|
||||||
Runs the UCE unit-based test suite through the runtime CLI socket.
|
Runs the UCE unit-based test suite through the runtime CLI socket.
|
||||||
|
|
||||||
Environment:
|
|
||||||
UCE_CLI_TEST_TIMEOUT Per CLI runner group curl timeout in seconds (default: 900).
|
|
||||||
USAGE
|
USAGE
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
@ -51,35 +42,5 @@ if [[ ! -S "$socket_path" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
base_url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}&skip_local_service_pages=${skip_local_service_pages}"
|
url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}"
|
||||||
if [[ "$action" == "list" ]]; then
|
exec curl -sS --fail-with-body --unix-socket "$socket_path" "$url"
|
||||||
curl -sS --max-time "$curl_timeout" --fail-with-body --unix-socket "$socket_path" "$base_url"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$action" == "run" ]]; then
|
|
||||||
groups=(demo http site doc-gate security task-lifetime pool-isolation starter tcp)
|
|
||||||
if [[ "$include_kill" == "1" ]]; then
|
|
||||||
groups+=(wasm-kill)
|
|
||||||
fi
|
|
||||||
for group in "${groups[@]}"; do
|
|
||||||
echo "== UCE CLI group: $group =="
|
|
||||||
curl -sS --max-time "$curl_timeout" --fail-with-body --unix-socket "$socket_path" "${base_url}&group=${group}"
|
|
||||||
done
|
|
||||||
scripts/test_dependency_invalidation.sh
|
|
||||||
scripts/test_abi_generation_rollout.sh
|
|
||||||
scripts/test_parallel_precompile.sh
|
|
||||||
scripts/test_cold_component_deadline.sh
|
|
||||||
scripts/test_nested_component_props.sh
|
|
||||||
scripts/test_component_once_prefetch.sh
|
|
||||||
scripts/test_relative_component_cache.sh
|
|
||||||
scripts/test_password_hashing.sh
|
|
||||||
scripts/test_mysql_epoch_refresh.sh
|
|
||||||
scripts/test_mysql_persistent_pool.sh
|
|
||||||
scripts/test_log_timeliness.sh
|
|
||||||
scripts/test_raw_http_request_log.sh
|
|
||||||
scripts/test_component_resolution_ttl.sh
|
|
||||||
scripts/test_unit_export_surface.sh
|
|
||||||
scripts/test_wasm_source_locations.sh
|
|
||||||
scripts/test_socket_activation.sh
|
|
||||||
fi
|
|
||||||
|
|||||||
@ -18,25 +18,11 @@ CONFIG_SOURCE="$REPO_ROOT/etc/uce/settings.cfg"
|
|||||||
CONFIG_DEST="/etc/uce/settings.cfg"
|
CONFIG_DEST="/etc/uce/settings.cfg"
|
||||||
|
|
||||||
action="${1:-setup}"
|
action="${1:-setup}"
|
||||||
precompile_timeout="${UCE_PRECOMPILE_TIMEOUT:-900s}"
|
|
||||||
|
|
||||||
render_runtime_file() {
|
|
||||||
local source="$1"
|
|
||||||
local destination="$2"
|
|
||||||
local mode="$3"
|
|
||||||
local tmp
|
|
||||||
tmp="$(mktemp)"
|
|
||||||
trap 'rm -f "$tmp"' RETURN
|
|
||||||
sed "s#/Code/uce.openfu.com/uce#$REPO_ROOT#g" "$source" > "$tmp"
|
|
||||||
install -D -m "$mode" "$tmp" "$destination"
|
|
||||||
rm -f "$tmp"
|
|
||||||
trap - RETURN
|
|
||||||
}
|
|
||||||
|
|
||||||
install_unit() {
|
install_unit() {
|
||||||
render_runtime_file "$UNIT_SOURCE" "$UNIT_DEST" 0644
|
install -D -m 0644 "$UNIT_SOURCE" "$UNIT_DEST"
|
||||||
if [[ ! -f "$CONFIG_DEST" ]]; then
|
if [[ ! -f "$CONFIG_DEST" ]]; then
|
||||||
render_runtime_file "$CONFIG_SOURCE" "$CONFIG_DEST" 0644
|
install -D -m 0644 "$CONFIG_SOURCE" "$CONFIG_DEST"
|
||||||
fi
|
fi
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
}
|
}
|
||||||
@ -53,22 +39,7 @@ case "$action" in
|
|||||||
install_unit
|
install_unit
|
||||||
systemctl enable "$UNIT_NAME"
|
systemctl enable "$UNIT_NAME"
|
||||||
;;
|
;;
|
||||||
restart)
|
start|stop|restart|status)
|
||||||
if [[ "$REPO_ROOT" != "/usr/lib/uce" && "$REPO_ROOT" != "/opt/uce" ]]; then
|
|
||||||
"$REPO_ROOT/scripts/build_linux.sh"
|
|
||||||
fi
|
|
||||||
(
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
service_user=$(systemctl show "$UNIT_NAME" -p User --value)
|
|
||||||
if [[ -n "$service_user" && "$(id -u)" == "0" ]]; then
|
|
||||||
runuser -u "$service_user" -- timeout --signal=TERM --kill-after=10s "$precompile_timeout" nice -n 10 "$REPO_ROOT/bin/uce_fastcgi.linux.bin" --precompile
|
|
||||||
else
|
|
||||||
timeout --signal=TERM --kill-after=10s "$precompile_timeout" nice -n 10 "$REPO_ROOT/bin/uce_fastcgi.linux.bin" --precompile
|
|
||||||
fi
|
|
||||||
)
|
|
||||||
systemctl restart "$UNIT_NAME"
|
|
||||||
;;
|
|
||||||
start|stop|status)
|
|
||||||
systemctl "$action" "$UNIT_NAME"
|
systemctl "$action" "$UNIT_NAME"
|
||||||
;;
|
;;
|
||||||
logs)
|
logs)
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=UCE FastCGI Runtime
|
Description=UCE FastCGI Runtime
|
||||||
After=network-online.target mariadb.service memcached.service uce.socket
|
After=network-online.target mariadb.service memcached.service
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
Requires=uce.socket
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/Code/uce.openfu.com/uce
|
WorkingDirectory=/Code/uce.openfu.com/uce
|
||||||
RuntimeDirectory=uce
|
RuntimeDirectory=uce
|
||||||
RuntimeDirectoryPreserve=yes
|
|
||||||
StateDirectory=uce
|
StateDirectory=uce
|
||||||
CacheDirectory=uce
|
CacheDirectory=uce
|
||||||
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
|
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
|
||||||
|
ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||||
|
ExecStartPre=/usr/bin/bash /Code/uce.openfu.com/uce/scripts/build_linux.sh
|
||||||
ExecStart=/Code/uce.openfu.com/uce/bin/uce_fastcgi.linux.bin
|
ExecStart=/Code/uce.openfu.com/uce/bin/uce_fastcgi.linux.bin
|
||||||
ExecStartPost=/usr/bin/bash /Code/uce.openfu.com/uce/scripts/systemd/wait-ready.sh
|
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
TimeoutStopSec=30
|
TimeoutStopSec=15
|
||||||
KillMode=mixed
|
KillMode=mixed
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=UCE FastCGI socket
|
|
||||||
|
|
||||||
[Socket]
|
|
||||||
ListenStream=/run/uce/fastcgi.sock
|
|
||||||
FileDescriptorName=fastcgi
|
|
||||||
SocketUser=www-data
|
|
||||||
SocketGroup=www-data
|
|
||||||
SocketMode=0660
|
|
||||||
DirectoryMode=0755
|
|
||||||
Backlog=100
|
|
||||||
Service=uce.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=sockets.target
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
socket_path="/run/uce/cli.sock"
|
|
||||||
if [[ -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_socket=$(awk -F= '/^[[:space:]]*CLI_SOCKET_PATH[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
if [[ -n "${configured_socket:-}" ]]; then
|
|
||||||
socket_path="$configured_socket"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
for ((attempt = 0; attempt < 200; attempt++)); do
|
|
||||||
if [[ -S "$socket_path" ]] && [[ "$(curl -sS --max-time 0.2 --unix-socket "$socket_path" http://localhost/ping 2>/dev/null || true)" == "uce-cli: ok" ]]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "UCE CLI socket did not become ready: $socket_path" >&2
|
|
||||||
exit 1
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="abi-generation-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
[[ -z "${configured_site_directory:-}" ]] || site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
|
|
||||||
body_file="/tmp/uce-abi-generation-body-$$"
|
|
||||||
ready_file="/tmp/uce-abi-generation-ready-$$"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
rm -f "$body_file" "$ready_file"
|
|
||||||
[[ -z "${artifact_dir:-}" ]] || rm -rf "$artifact_dir"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'RENDER(Request& context) { print("entry-current"); }' \
|
|
||||||
'CLI(Request& context) {' \
|
|
||||||
' if(context.get["artifact"] == "entry") print(unit_info(context.params["SCRIPT_FILENAME"])["wasm_name"].to_string());' \
|
|
||||||
' else print("entry-current");' \
|
|
||||||
'}' >"$source_dir/entry.uce"
|
|
||||||
printf '%s\n' \
|
|
||||||
'COMPONENT(Request& context) { print("component-current"); }' >"$source_dir/child.uce"
|
|
||||||
printf '%s\n' \
|
|
||||||
'RENDER(Request& context) { DValue props; print(component("child", props, context)); }' \
|
|
||||||
'CLI(Request& context) {' \
|
|
||||||
' DValue props; component("child", props, context);' \
|
|
||||||
' print(unit_info("child.uce")["wasm_name"].to_string());' \
|
|
||||||
'}' >"$source_dir/component-parent.uce"
|
|
||||||
|
|
||||||
entry_url="http://127.0.0.1/$test_name/entry.uce"
|
|
||||||
component_url="http://127.0.0.1/$test_name/component-parent.uce"
|
|
||||||
[[ "$(curl -fsS -H "Host: $http_host" "$entry_url")" == "entry-current" ]]
|
|
||||||
entry_wasm=$(scripts/uce-cli --get "/$test_name/entry.uce" artifact=entry)
|
|
||||||
component_wasm=$(scripts/uce-cli "/$test_name/component-parent.uce")
|
|
||||||
artifact_dir=$(dirname "$entry_wasm")
|
|
||||||
generation_name=$(basename "$(scripts/unit_cache_directory /tmp/uce-generation-probe)")
|
|
||||||
if [[ "$entry_wasm" != *"/$generation_name/"* || "$component_wasm" != *"/$generation_name/"* ]]; then
|
|
||||||
echo "unit artifacts are not isolated by compiler/core ABI generation: $entry_wasm $component_wasm" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
publish_incompatible_artifact_while_locked() {
|
|
||||||
local source_file="$1"
|
|
||||||
local wasm_file="$2"
|
|
||||||
local marker="$3"
|
|
||||||
local destination
|
|
||||||
destination=$(dirname "$wasm_file")
|
|
||||||
(
|
|
||||||
exec 8>"$wasm_file.lock"
|
|
||||||
flock 8
|
|
||||||
UCE_UNIT_ABI_VERSION=6 scripts/compile_wasm_unit \
|
|
||||||
"$(dirname "$source_file")" "$destination" "$source_file" \
|
|
||||||
"$(basename "$source_file").cpp" "$(basename "$wasm_file")"
|
|
||||||
sed -i \
|
|
||||||
-e 's/^unit_abi_version=.*/unit_abi_version=0/' \
|
|
||||||
-e 's/^wasm_core_abi_version=.*/wasm_core_abi_version=0/' \
|
|
||||||
"${wasm_file%.wasm}.meta.txt"
|
|
||||||
rm -f "${wasm_file%.wasm}.cwasm"
|
|
||||||
printf '%s\n' "$marker" >"$ready_file"
|
|
||||||
sleep 2
|
|
||||||
) &
|
|
||||||
lock_pid=$!
|
|
||||||
deadline=$((SECONDS + 20))
|
|
||||||
while [[ ! -e "$ready_file" && $SECONDS -lt $deadline ]]; do sleep 0.05; done
|
|
||||||
if [[ ! -e "$ready_file" || "$(cat "$ready_file")" != "$marker" ]]; then
|
|
||||||
echo "incompatible artifact was not published under lock: $marker" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
publish_incompatible_artifact_while_locked "$source_dir/entry.uce" "$entry_wasm" entry
|
|
||||||
started_at=$(date +%s%N)
|
|
||||||
entry_status=$(curl -sS --max-time 30 -o "$body_file" -w '%{http_code}' -H "Host: $http_host" "$entry_url")
|
|
||||||
entry_elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
|
||||||
wait "$lock_pid"
|
|
||||||
if [[ "$entry_status" != "200" || "$(cat "$body_file")" != "entry-current" || $entry_elapsed_ms -lt 1500 ]]; then
|
|
||||||
echo "ABI-incompatible entry artifact was served instead of joining its rebuild: status=$entry_status elapsed=${entry_elapsed_ms}ms body=$(cat "$body_file")" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$ready_file"
|
|
||||||
publish_incompatible_artifact_while_locked "$source_dir/child.uce" "$component_wasm" component
|
|
||||||
started_at=$(date +%s%N)
|
|
||||||
component_status=$(curl -sS --max-time 30 -o "$body_file" -w '%{http_code}' -H "Host: $http_host" "$component_url")
|
|
||||||
component_elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
|
||||||
wait "$lock_pid"
|
|
||||||
if [[ "$component_status" != "200" || "$(cat "$body_file")" != "component-current" || $component_elapsed_ms -lt 1500 ]]; then
|
|
||||||
echo "ABI-incompatible component artifact was served instead of joining its rebuild: status=$component_status elapsed=${component_elapsed_ms}ms body=$(cat "$body_file")" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "ABI generation rollout passed"
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="component-deadline-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
|
||||||
site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
bin_directory="${BIN_DIRECTORY:-}"
|
|
||||||
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
fi
|
|
||||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
|
||||||
cache_dir=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
if [[ -n "$cache_dir" ]]; then
|
|
||||||
rm -rf "$cache_dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'CLI(Request& context) { DValue props; print(component("child", props, context)); }' >"$source_dir/parent.uce"
|
|
||||||
for i in $(seq 1 800); do
|
|
||||||
printf 'String cold_component_pad_%s() { return("%s"); }\n' "$i" "$i"
|
|
||||||
done >"$source_dir/child.uce"
|
|
||||||
printf '%s\n' \
|
|
||||||
'COMPONENT(Request& context) { <><strong>cold-component-deadline-ok</strong></> }' >>"$source_dir/child.uce"
|
|
||||||
rm -rf "$cache_dir"
|
|
||||||
|
|
||||||
output=$(scripts/uce-cli "/$test_name/parent.uce")
|
|
||||||
if [[ "$output" != *cold-component-deadline-ok* ]]; then
|
|
||||||
echo "cold component compilation consumed the guest epoch budget: $output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "cold component deadline passed"
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="component-once-prefetch-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
|
||||||
site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'CLI(Request& context) {' \
|
|
||||||
' String first = component("child", context);' \
|
|
||||||
' String second = component("child", context);' \
|
|
||||||
' DValue perf = request_perf();' \
|
|
||||||
' print(first, ",", second, ",", perf["component_resolve_count"].to_u64(), ",", perf["component_loaded_reuse_count"].to_u64());' \
|
|
||||||
'}' >"$source_dir/entry.uce"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'ONCE(Request& context) { context.call["once"] = context.call["once"].to_u64() + 1; }' \
|
|
||||||
'COMPONENT(Request& context) { print(context.call["once"].to_u64()); }' >"$source_dir/child.uce"
|
|
||||||
|
|
||||||
output=$(scripts/uce-cli "/$test_name/entry.uce")
|
|
||||||
if [[ "$output" != "1,1,1,0" ]]; then
|
|
||||||
echo "component ONCE slot was not returned with the primary handler: $output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "component ONCE prefetch passed"
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="component-resolution-ttl-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
worker_count="${UCE_TEST_WORKER_COUNT:-4}"
|
|
||||||
if [[ -r /etc/uce/settings.cfg ]]; then
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
site_directory="${configured_site_directory:-$site_directory}"
|
|
||||||
fi
|
|
||||||
if [[ -z "${UCE_TEST_WORKER_COUNT:-}" ]]; then
|
|
||||||
configured_worker_count=$(awk -F= '/^[[:space:]]*WORKER_COUNT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
worker_count="${configured_worker_count:-$worker_count}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
|
|
||||||
printf '%s\n' 'RENDER(Request& context) { print("parent:", request_perf()["worker_pid"].to_string(), ":", component("chosen", context)); }' >"$source_dir/parent.uce"
|
|
||||||
printf '%s\n' 'COMPONENT(Request& context) { print("a"); }' >"$source_dir/a.uce"
|
|
||||||
printf '%s\n' 'COMPONENT(Request& context) { print("b"); }' >"$source_dir/b.uce"
|
|
||||||
ln -s a.uce "$source_dir/chosen.uce"
|
|
||||||
|
|
||||||
declare -A seen
|
|
||||||
collect_workers() {
|
|
||||||
local method="$1"
|
|
||||||
local marker="$2"
|
|
||||||
local path="${3:-parent.uce}"
|
|
||||||
local deadline=$((SECONDS + 30))
|
|
||||||
seen=()
|
|
||||||
while (( SECONDS < deadline && ${#seen[@]} < worker_count )); do
|
|
||||||
local response status body pid
|
|
||||||
response=$(curl -sS --max-time 10 -X "$method" -H "Host: $http_host" -w $'\n%{http_code}' "http://127.0.0.1/$test_name/$path")
|
|
||||||
status=${response##*$'\n'}
|
|
||||||
body=${response%$'\n'*}
|
|
||||||
if [[ "$status" == "503" ]]; then
|
|
||||||
sleep 0.1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [[ "$status" != "200" || "$body" != parent:*":$marker" ]]; then
|
|
||||||
echo "component resolution TTL probe failed: method=$method status=$status expected=$marker body=$body" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
pid=${body#parent:}
|
|
||||||
pid=${pid%%:*}
|
|
||||||
seen["$pid"]=1
|
|
||||||
done
|
|
||||||
if (( ${#seen[@]} != worker_count )); then
|
|
||||||
echo "component resolution TTL probe reached ${#seen[@]}/$worker_count workers" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
collect_missing_workers() {
|
|
||||||
local deadline=$((SECONDS + 30))
|
|
||||||
seen=()
|
|
||||||
while (( SECONDS < deadline && ${#seen[@]} < worker_count )); do
|
|
||||||
local response status body pid
|
|
||||||
response=$(curl -sS --max-time 10 -H "Host: $http_host" -w $'\n%{http_code}' "http://127.0.0.1/$test_name/missing.uce")
|
|
||||||
status=${response##*$'\n'}
|
|
||||||
body=${response%$'\n'*}
|
|
||||||
if [[ "$status" == "503" ]]; then
|
|
||||||
sleep 0.1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [[ "$status" != "500" || "$body" != parent:* || "$body" != *"component not found: later"* ]]; then
|
|
||||||
echo "missing component TTL probe failed: status=$status body=$body" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
pid=${body#parent:}
|
|
||||||
pid=${pid%%:*}
|
|
||||||
seen["$pid"]=1
|
|
||||||
done
|
|
||||||
if (( ${#seen[@]} != worker_count )); then
|
|
||||||
echo "missing component TTL probe reached ${#seen[@]}/$worker_count workers" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
collect_workers GET a
|
|
||||||
ln -sfn b.uce "$source_dir/chosen.uce"
|
|
||||||
collect_workers GET a
|
|
||||||
collect_workers POST b
|
|
||||||
collect_workers GET a
|
|
||||||
sleep 10.2
|
|
||||||
collect_workers GET b
|
|
||||||
|
|
||||||
printf '%s\n' 'RENDER(Request& context) { print("parent:", request_perf()["worker_pid"].to_string(), ":", component("later", context)); }' >"$source_dir/missing.uce"
|
|
||||||
collect_missing_workers
|
|
||||||
printf '%s\n' 'COMPONENT(Request& context) { print("created"); }' >"$source_dir/later.uce"
|
|
||||||
collect_workers GET created missing.uce
|
|
||||||
|
|
||||||
echo "component resolution TTL passed"
|
|
||||||
@ -1,463 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="dependency-cache-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
|
||||||
site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
bin_directory="${BIN_DIRECTORY:-}"
|
|
||||||
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
fi
|
|
||||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
|
||||||
cache_dir=""
|
|
||||||
mutation_file="/tmp/uce-dependency-mutation-$$"
|
|
||||||
component_mutation_file="/tmp/uce-component-mutation-$$"
|
|
||||||
component_post_body="/tmp/uce-component-post-body-$$"
|
|
||||||
component_post_headers="/tmp/uce-component-post-headers-$$"
|
|
||||||
component_lock_ready_file="/tmp/uce-component-lock-ready-$$"
|
|
||||||
post_body="/tmp/uce-dependency-post-body-$$"
|
|
||||||
post_headers="/tmp/uce-dependency-post-headers-$$"
|
|
||||||
lock_ready_file="/tmp/uce-dependency-lock-ready-$$"
|
|
||||||
nested_lock_ready_file="/tmp/uce-nested-lock-ready-$$"
|
|
||||||
race_output="/tmp/uce-source-race-output-$$"
|
|
||||||
race_error="/tmp/uce-source-race-error-$$"
|
|
||||||
race_status="/tmp/uce-source-race-status-$$"
|
|
||||||
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
if [[ -n "$cache_dir" ]]; then
|
|
||||||
rm -rf "$cache_dir"
|
|
||||||
fi
|
|
||||||
rm -f "$mutation_file" "$component_mutation_file" "$component_post_body" "$component_post_headers" "$component_lock_ready_file" "$post_body" "$post_headers" "$lock_ready_file" "$nested_lock_ready_file" "$race_output" "$race_error" "$race_status"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'#ifndef UCE_DEPENDENCY_CACHE_CHILD' \
|
|
||||||
'#define UCE_DEPENDENCY_CACHE_CHILD' \
|
|
||||||
'String dependency_cache_marker() { return("dependency-marker-a"); }' \
|
|
||||||
'#endif' >"$source_dir/child.uce"
|
|
||||||
printf '%s\n' \
|
|
||||||
'#load "child.uce"' \
|
|
||||||
'CLI(Request& context) { print(dependency_cache_marker(), ":", request_perf()["worker_pid"].to_string()); }' \
|
|
||||||
"RENDER(Request& context) { if(context.params[\"REQUEST_METHOD\"] == \"POST\") file_put_contents(\"$mutation_file\", dependency_cache_marker()); print(dependency_cache_marker(), \":\", request_perf()[\"worker_pid\"].to_string()); }" >"$source_dir/parent.uce"
|
|
||||||
|
|
||||||
assert_marker() {
|
|
||||||
local path="$1"
|
|
||||||
local expected="$2"
|
|
||||||
local output
|
|
||||||
output=$(scripts/uce-cli "/$test_name/$path.uce")
|
|
||||||
if [[ "$output" != *"$expected"* ]]; then
|
|
||||||
echo "$path returned stale module; expected $expected: $output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
http_marker() {
|
|
||||||
curl -fsS -H "Host: $http_host" "http://127.0.0.1/$test_name/parent.uce"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_marker parent dependency-marker-a
|
|
||||||
if [[ "$(http_marker)" != *"dependency-marker-a"* ]]; then
|
|
||||||
echo "HTTP warm-up did not return dependency-marker-a" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Compiler source canonicalization must retain symlink-target identity while
|
|
||||||
# avoiding realpath's repeated per-segment probes on ordinary source graphs.
|
|
||||||
printf '%s\n' 'String symlink_dependency_marker() { return("symlink-marker-a"); }' >"$source_dir/symlink-target-a.uce"
|
|
||||||
printf '%s\n' 'String symlink_dependency_marker() { return("symlink-marker-b"); }' >"$source_dir/symlink-target-b.uce"
|
|
||||||
ln -s "symlink-target-a.uce" "$source_dir/symlink-child.uce"
|
|
||||||
printf '%s\n' '#load "symlink-child.uce"' 'CLI(Request& context) { print(symlink_dependency_marker()); }' >"$source_dir/symlink-parent.uce"
|
|
||||||
assert_marker symlink-parent symlink-marker-a
|
|
||||||
ln -sfn "symlink-target-b.uce" "$source_dir/symlink-child.uce"
|
|
||||||
assert_marker symlink-parent symlink-marker-b
|
|
||||||
|
|
||||||
# A repeated exact transitive load is signature-deduplicated before path
|
|
||||||
# canonicalization, but the shared dependency must still invalidate the parent.
|
|
||||||
printf '%s\n' '#ifndef UCE_DIAMOND_COMMON' '#define UCE_DIAMOND_COMMON' 'String diamond_marker() { return("diamond-a"); }' '#endif' >"$source_dir/diamond-common.uce"
|
|
||||||
printf '%s\n' '#load "diamond-common.uce"' >"$source_dir/diamond-left.uce"
|
|
||||||
printf '%s\n' '#load "diamond-common.uce"' >"$source_dir/diamond-right.uce"
|
|
||||||
printf '%s\n' '#load "diamond-left.uce"' '#load "diamond-right.uce"' 'CLI(Request& context) { print(diamond_marker()); }' >"$source_dir/diamond-parent.uce"
|
|
||||||
assert_marker diamond-parent diamond-a
|
|
||||||
sed -i 's/diamond-a/diamond-b/' "$source_dir/diamond-common.uce"
|
|
||||||
assert_marker diamond-parent diamond-b
|
|
||||||
|
|
||||||
# HTTP entry units can resolve route/components dynamically. A changed dynamic
|
|
||||||
# component must enter the demand-priority queue just like a changed entry unit;
|
|
||||||
# otherwise a common dependency rebuild can leave the requested page stale for
|
|
||||||
# the duration of the full proactive queue.
|
|
||||||
printf '%s\n' "COMPONENT(Request& context) { if(context.params[\"REQUEST_METHOD\"] == \"POST\") file_put_contents(\"$component_mutation_file\", \"executed\"); print(\"http-component-marker-a\"); }" >"$source_dir/http-component-child.uce"
|
|
||||||
printf '%s\n' 'RENDER(Request& context) { DValue props; print(component("http-component-child", props, context)); }' >"$source_dir/http-component-parent.uce"
|
|
||||||
http_component_marker() {
|
|
||||||
curl -fsS -H "Host: $http_host" "http://127.0.0.1/$test_name/http-component-parent.uce"
|
|
||||||
}
|
|
||||||
if [[ "$(http_component_marker)" != *"http-component-marker-a"* ]]; then
|
|
||||||
echo "HTTP component warm-up did not return marker-a" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sed -i 's/http-component-marker-a/http-component-marker-b/' "$source_dir/http-component-child.uce"
|
|
||||||
started_at=$(date +%s%N)
|
|
||||||
http_component_output=$(http_component_marker)
|
|
||||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
|
||||||
if [[ "$http_component_output" != *"http-component-marker-a"* && "$http_component_output" != *"http-component-marker-b"* ]]; then
|
|
||||||
echo "stale HTTP component request returned neither complete artifact: $http_component_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if (( elapsed_ms >= 2000 )); then
|
|
||||||
echo "HTTP component request spent ${elapsed_ms}ms rebuilding a stale artifact" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
deadline=$((SECONDS + 15))
|
|
||||||
while [[ "$http_component_output" != *"http-component-marker-b"* && $SECONDS -lt $deadline ]]; do
|
|
||||||
sleep 0.2
|
|
||||||
http_component_output=$(http_component_marker)
|
|
||||||
done
|
|
||||||
if [[ "$http_component_output" != *"http-component-marker-b"* ]]; then
|
|
||||||
echo "requested stale HTTP component did not receive a demand-priority rebuild" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' 'String http_component_dependency_marker() { return("http-component-dependency-a"); }' >"$source_dir/http-component-dependency.uce"
|
|
||||||
printf '%s\n' '#load "http-component-dependency.uce"' 'COMPONENT(Request& context) { print(http_component_dependency_marker()); }' >"$source_dir/http-transitive-child.uce"
|
|
||||||
printf '%s\n' 'RENDER(Request& context) { DValue props; print(component("http-transitive-child", props, context)); }' >"$source_dir/http-transitive-parent.uce"
|
|
||||||
http_transitive_marker() {
|
|
||||||
curl -fsS -H "Host: $http_host" "http://127.0.0.1/$test_name/http-transitive-parent.uce"
|
|
||||||
}
|
|
||||||
if [[ "$(http_transitive_marker)" != *"http-component-dependency-a"* ]]; then
|
|
||||||
echo "HTTP transitive component warm-up did not return dependency-a" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sed -i 's/http-component-dependency-a/http-component-dependency-b/' "$source_dir/http-component-dependency.uce"
|
|
||||||
deadline=$((SECONDS + 20))
|
|
||||||
while [[ "$(http_transitive_marker)" != *"http-component-dependency-b"* && $SECONDS -lt $deadline ]]; do sleep 0.2; done
|
|
||||||
if [[ "$(http_transitive_marker)" != *"http-component-dependency-b"* ]]; then
|
|
||||||
echo "HTTP dynamic component did not converge after its loaded dependency changed" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mutations must never execute a stale dynamically resolved component, even
|
|
||||||
# inside the short read-only freshness memo window.
|
|
||||||
http_component_wasm="$cache_dir/http-component-child.uce.wasm"
|
|
||||||
(
|
|
||||||
exec 8>"$http_component_wasm.lock"
|
|
||||||
flock 8
|
|
||||||
: >"$component_lock_ready_file"
|
|
||||||
sleep 2
|
|
||||||
) &
|
|
||||||
component_lock_pid=$!
|
|
||||||
deadline=$((SECONDS + 5))
|
|
||||||
while [[ ! -e "$component_lock_ready_file" && $SECONDS -lt $deadline ]]; do sleep 0.05; done
|
|
||||||
if [[ ! -e "$component_lock_ready_file" ]]; then
|
|
||||||
echo "HTTP component mutation lock was not acquired before deadline" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sed -i 's/http-component-marker-b/http-component-marker-c/' "$source_dir/http-component-child.uce"
|
|
||||||
rm -f "$component_mutation_file"
|
|
||||||
component_post_status=$(curl -sS -o "$component_post_body" -D "$component_post_headers" -w '%{http_code}' -X POST -H "Host: $http_host" "http://127.0.0.1/$test_name/http-component-parent.uce")
|
|
||||||
if [[ "$component_post_status" != "503" ]]; then
|
|
||||||
echo "stale HTTP component mutation did not fail closed: status=$component_post_status body=$(cat "$component_post_body")" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! grep -qi '^Retry-After: 1' "$component_post_headers"; then
|
|
||||||
echo "stale HTTP component mutation omitted Retry-After" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -e "$component_mutation_file" ]]; then
|
|
||||||
echo "stale HTTP component mutation executed application code" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
wait "$component_lock_pid"
|
|
||||||
deadline=$((SECONDS + 15))
|
|
||||||
while [[ "$(http_component_marker)" != *"http-component-marker-c"* && $SECONDS -lt $deadline ]]; do sleep 0.2; done
|
|
||||||
if [[ "$(http_component_marker)" != *"http-component-marker-c"* ]]; then
|
|
||||||
echo "HTTP component did not recover after the guarded mutation rebuild" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# A failed parent build must remember the exact dependency graph that failed.
|
|
||||||
# If a dependency is restored byte-for-byte to the last working source, the
|
|
||||||
# old successful metadata must not make that transient failure permanent.
|
|
||||||
printf '%s\n' \
|
|
||||||
'#ifndef UCE_DEPENDENCY_CACHE_CHILD' \
|
|
||||||
'#define UCE_DEPENDENCY_CACHE_CHILD' \
|
|
||||||
'String dependency_cache_marker() { return(deliberate_dependency_compile_failure); }' \
|
|
||||||
'#endif' >"$source_dir/child.uce"
|
|
||||||
if restored_failure=$(scripts/uce-cli --get "/$test_name/parent.uce" __uce_expected_compile_failure=1 2>&1); then
|
|
||||||
echo "invalid dependency unexpectedly compiled: $restored_failure" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "$restored_failure" != *"deliberate_dependency_compile_failure"* ]]; then
|
|
||||||
echo "invalid dependency did not report the expected compiler failure: $restored_failure" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
printf '%s\n' \
|
|
||||||
'#ifndef UCE_DEPENDENCY_CACHE_CHILD' \
|
|
||||||
'#define UCE_DEPENDENCY_CACHE_CHILD' \
|
|
||||||
'String dependency_cache_marker() { return("dependency-marker-a"); }' \
|
|
||||||
'#endif' >"$source_dir/child.uce"
|
|
||||||
assert_marker parent dependency-marker-a
|
|
||||||
|
|
||||||
# The load-graph signature is content-addressed. A byte-identical dependency
|
|
||||||
# touch must invalidate stat caches without recompiling its parent artifact.
|
|
||||||
parent_wasm="$cache_dir/parent.uce.wasm"
|
|
||||||
parent_wasm_identity=$(stat -c '%y:%s' "$parent_wasm")
|
|
||||||
sleep 1.1
|
|
||||||
touch "$source_dir/child.uce"
|
|
||||||
assert_marker parent dependency-marker-a
|
|
||||||
if [[ "$(stat -c '%y:%s' "$parent_wasm")" != "$parent_wasm_identity" ]]; then
|
|
||||||
echo "byte-identical dependency touch recompiled the parent artifact" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' 'CLI(Request& context) { print("readable-source-marker"); }' >"$source_dir/unreadable.uce"
|
|
||||||
chmod 000 "$source_dir/unreadable.uce"
|
|
||||||
if unreadable_output=$(scripts/uce-cli --get "/$test_name/unreadable.uce" __uce_expected_source_read_failure=1 2>&1); then
|
|
||||||
echo "unreadable source unexpectedly compiled: $unreadable_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "$unreadable_output" != *"source file is not readable"* ]]; then
|
|
||||||
echo "unreadable source did not report its actual compile error: $unreadable_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -e "$cache_dir/unreadable.uce.wasm" ]]; then
|
|
||||||
echo "unreadable source published a wasm artifact" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
chmod 644 "$source_dir/unreadable.uce"
|
|
||||||
assert_marker unreadable readable-source-marker
|
|
||||||
|
|
||||||
sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce"
|
|
||||||
started_at=$(date +%s%N)
|
|
||||||
http_during_rebuild=$(http_marker)
|
|
||||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
|
||||||
if [[ "$http_during_rebuild" != *"dependency-marker-a"* && "$http_during_rebuild" != *"dependency-marker-b"* ]]; then
|
|
||||||
echo "HTTP rebuild request returned neither complete artifact: $http_during_rebuild" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if (( elapsed_ms >= 2000 )); then
|
|
||||||
echo "HTTP request spent ${elapsed_ms}ms rebuilding a stale artifact" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
deadline=$((SECONDS + 15))
|
|
||||||
while [[ "$http_during_rebuild" != *"dependency-marker-b"* && $SECONDS -lt $deadline ]]; do
|
|
||||||
sleep 0.2
|
|
||||||
http_during_rebuild=$(http_marker)
|
|
||||||
done
|
|
||||||
if [[ "$http_during_rebuild" != *"dependency-marker-b"* ]]; then
|
|
||||||
echo "requested stale HTTP unit did not receive a demand-priority rebuild" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
assert_marker parent dependency-marker-b
|
|
||||||
|
|
||||||
# A proactive rebuild owns these same per-unit locks. While it publishes fresh
|
|
||||||
# artifacts, requests must use the last complete artifacts instead of waiting
|
|
||||||
# across the transitive graph. Atomic publication keeps those artifacts safe.
|
|
||||||
child_wasm="$cache_dir/child.uce.wasm"
|
|
||||||
(
|
|
||||||
exec 8>"$parent_wasm.lock"
|
|
||||||
exec 9>"$child_wasm.lock"
|
|
||||||
flock 8
|
|
||||||
flock 9
|
|
||||||
: >"$lock_ready_file"
|
|
||||||
sleep 3
|
|
||||||
) &
|
|
||||||
rebuild_lock_pid=$!
|
|
||||||
deadline=$((SECONDS + 5))
|
|
||||||
while [[ ! -e "$lock_ready_file" && $SECONDS -lt $deadline ]]; do
|
|
||||||
if ! kill -0 "$rebuild_lock_pid" 2>/dev/null; then
|
|
||||||
echo "test rebuild lock process exited before acquiring locks" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
if [[ ! -e "$lock_ready_file" ]]; then
|
|
||||||
echo "test rebuild lock process did not acquire locks before deadline" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sed -i 's/dependency-marker-b/dependency-marker-d/' "$source_dir/child.uce"
|
|
||||||
started_at=$(date +%s%N)
|
|
||||||
http_during_lock=$(http_marker)
|
|
||||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
|
||||||
if [[ "$http_during_lock" != *"dependency-marker-b"* ]]; then
|
|
||||||
echo "HTTP request did not serve the last complete artifact during rebuild: $http_during_lock" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if (( elapsed_ms >= 2000 )); then
|
|
||||||
echo "HTTP request waited ${elapsed_ms}ms for an active transitive rebuild" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm -f "$mutation_file"
|
|
||||||
started_at=$(date +%s%N)
|
|
||||||
post_status=$(curl -sS -o "$post_body" -D "$post_headers" -w '%{http_code}' -X POST -H "Host: $http_host" "http://127.0.0.1/$test_name/parent.uce")
|
|
||||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
|
||||||
if [[ "$post_status" != "503" ]]; then
|
|
||||||
echo "stale POST executed an application artifact instead of returning 503: status=$post_status body=$(cat "$post_body")" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! grep -qi '^Retry-After: 1' "$post_headers"; then
|
|
||||||
echo "stale POST did not return a Retry-After header" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -e "$mutation_file" ]]; then
|
|
||||||
echo "stale POST executed the old mutation handler: $(cat "$mutation_file")" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if (( elapsed_ms >= 2000 )); then
|
|
||||||
echo "stale POST waited ${elapsed_ms}ms instead of failing closed promptly" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
started_at=$(date +%s%N)
|
|
||||||
assert_marker parent dependency-marker-d
|
|
||||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
|
||||||
if (( elapsed_ms < 2000 )); then
|
|
||||||
echo "CLI request returned before the locked rebuild published the current artifact (${elapsed_ms}ms)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
wait "$rebuild_lock_pid"
|
|
||||||
|
|
||||||
# CLI freshness applies to every nested component, not only the request entry
|
|
||||||
# unit. Hold the changed child's compile lock as a proactive rebuild would: the
|
|
||||||
# parent CLI request must wait and render the new child, never its stale wasm.
|
|
||||||
printf '%s\n' 'COMPONENT(Request& context) { print("nested-component-marker-a"); }' >"$source_dir/nested-child.uce"
|
|
||||||
printf '%s\n' \
|
|
||||||
'CLI(Request& context) { DValue props; print(component("nested-child", props, context)); }' \
|
|
||||||
'RENDER(Request& context) { DValue props; print(component("nested-child", props, context)); }' >"$source_dir/nested-parent.uce"
|
|
||||||
assert_marker nested-parent nested-component-marker-a
|
|
||||||
nested_child_wasm="$cache_dir/nested-child.uce.wasm"
|
|
||||||
|
|
||||||
# A proactive source rebuild must publish the native serialized module before
|
|
||||||
# any request worker has to load the changed unit.
|
|
||||||
nested_parent_cwasm="$cache_dir/nested-parent.uce.cwasm"
|
|
||||||
rm -f "$nested_parent_cwasm"
|
|
||||||
printf '%s\n' \
|
|
||||||
'CLI(Request& context) { DValue props; print(component("nested-child", props, context)); /* proactive-serialization-v2 */ }' \
|
|
||||||
'RENDER(Request& context) { DValue props; print(component("nested-child", props, context)); /* proactive-serialization-v2 */ }' >"$source_dir/nested-parent.uce"
|
|
||||||
curl -fsS -H "Host: $http_host" "http://127.0.0.1/$test_name/nested-parent.uce" >/dev/null
|
|
||||||
deadline=$((SECONDS + 10))
|
|
||||||
while [[ ! -s "$nested_parent_cwasm" && $SECONDS -lt $deadline ]]; do sleep 0.1; done
|
|
||||||
if [[ ! -s "$nested_parent_cwasm" ]]; then
|
|
||||||
echo "proactive compiler did not serialize rebuilt module" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
(
|
|
||||||
exec 8>"$nested_child_wasm.lock"
|
|
||||||
flock 8
|
|
||||||
: >"$nested_lock_ready_file"
|
|
||||||
sleep 3
|
|
||||||
) &
|
|
||||||
nested_lock_pid=$!
|
|
||||||
deadline=$((SECONDS + 5))
|
|
||||||
while [[ ! -e "$nested_lock_ready_file" && $SECONDS -lt $deadline ]]; do
|
|
||||||
if ! kill -0 "$nested_lock_pid" 2>/dev/null; then
|
|
||||||
echo "nested component lock exited before acquisition" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
if [[ ! -e "$nested_lock_ready_file" ]]; then
|
|
||||||
echo "nested component lock was not acquired before deadline" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sed -i 's/nested-component-marker-a/nested-component-marker-b/' "$source_dir/nested-child.uce"
|
|
||||||
started_at=$(date +%s%N)
|
|
||||||
nested_output=$(scripts/uce-cli "/$test_name/nested-parent.uce")
|
|
||||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
|
||||||
wait "$nested_lock_pid"
|
|
||||||
if [[ "$nested_output" != *"nested-component-marker-b"* ]]; then
|
|
||||||
echo "CLI nested component returned stale wasm: $nested_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if (( elapsed_ms < 2000 )); then
|
|
||||||
echo "CLI nested component did not wait for the active rebuild (${elapsed_ms}ms)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Warm every configured worker, then replace the artifact while retaining its
|
|
||||||
# whole-second mtime. The worker cache must notice the nanosecond/ctime change.
|
|
||||||
for _ in {1..16}; do assert_marker parent dependency-marker-d; done
|
|
||||||
sed 's/dependency-marker-d/dependency-marker-c/' "$source_dir/child.uce" >"$source_dir/alternate-child.uce"
|
|
||||||
sed 's/child.uce/alternate-child.uce/' "$source_dir/parent.uce" >"$source_dir/alternate.uce"
|
|
||||||
assert_marker alternate dependency-marker-c
|
|
||||||
alternate_wasm="$cache_dir/alternate.uce.wasm"
|
|
||||||
parent_mtime=$(stat -c %Y "$parent_wasm")
|
|
||||||
cp "$alternate_wasm" "$parent_wasm"
|
|
||||||
touch -d "@$parent_mtime" "$parent_wasm"
|
|
||||||
rm -f "$cache_dir/parent.uce.cwasm"
|
|
||||||
for _ in {1..16}; do assert_marker parent dependency-marker-c; done
|
|
||||||
|
|
||||||
race_unit="$source_dir/source-race.uce"
|
|
||||||
race_cpp="$cache_dir/source-race.uce.cpp"
|
|
||||||
{
|
|
||||||
printf '%s\n' 'String source_race_marker() { return("source-race-marker-a"); }'
|
|
||||||
for i in $(seq 1 3500); do
|
|
||||||
printf 'int source_race_padding_%s() { return(%s); }\n' "$i" "$i"
|
|
||||||
done
|
|
||||||
printf '%s\n' 'CLI(Request& context) { print(source_race_marker()); }'
|
|
||||||
} >"$race_unit"
|
|
||||||
rm -f "$cache_dir/source-race.uce.cpp" "$cache_dir/source-race.uce.wasm" "$cache_dir/source-race.uce.cwasm" "$cache_dir/source-race.uce.meta.txt"
|
|
||||||
(
|
|
||||||
set +e
|
|
||||||
scripts/uce-cli "/$test_name/source-race.uce" >"$race_output" 2>"$race_error"
|
|
||||||
echo "$?" >"$race_status"
|
|
||||||
) &
|
|
||||||
race_pid=$!
|
|
||||||
deadline=$((SECONDS + 10))
|
|
||||||
while [[ ! -e "$race_cpp" && $SECONDS -lt $deadline ]]; do
|
|
||||||
if ! kill -0 "$race_pid" 2>/dev/null; then
|
|
||||||
echo "source-race compile finished before publishing generated C++" >&2
|
|
||||||
cat "$race_output" "$race_error" >&2 || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 0.02
|
|
||||||
done
|
|
||||||
if [[ ! -e "$race_cpp" ]]; then
|
|
||||||
echo "source-race compile did not publish generated C++ before deadline" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! grep -q 'source-race-marker-a' "$race_cpp"; then
|
|
||||||
echo "source-race generated C++ did not contain the initial marker" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! kill -0 "$race_pid" 2>/dev/null; then
|
|
||||||
echo "source-race compile finished before source mutation hook" >&2
|
|
||||||
cat "$race_output" "$race_error" >&2 || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sed -i 's/source-race-marker-a/source-race-marker-b/' "$race_unit"
|
|
||||||
wait "$race_pid"
|
|
||||||
if [[ "$(cat "$race_status" 2>/dev/null || echo 1)" != "0" ]]; then
|
|
||||||
echo "source-race CLI request failed" >&2
|
|
||||||
cat "$race_output" "$race_error" >&2 || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "$(cat "$race_output")" != *"source-race-marker-b"* ]]; then
|
|
||||||
echo "source changed during compile but UCE served stale wasm: $(cat "$race_output")" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
worker_count=$(awk -F= '/^[[:space:]]*WORKER_COUNT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg 2>/dev/null || true)
|
|
||||||
worker_count="${worker_count:-4}"
|
|
||||||
worker_pids=""
|
|
||||||
for _ in {1..48}; do
|
|
||||||
output=$(scripts/uce-cli "/$test_name/parent.uce")
|
|
||||||
worker_pids+="${output##*:}"$'\n'
|
|
||||||
done
|
|
||||||
unique_workers=$(printf '%s' "$worker_pids" | sed '/^$/d' | sort -u | wc -l)
|
|
||||||
if (( unique_workers > worker_count )); then
|
|
||||||
echo "worker pool recycled during 48 requests: $unique_workers PIDs for $worker_count workers" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "dependency invalidation passed"
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="log-timeliness-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
|
||||||
site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
bin_directory="${BIN_DIRECTORY:-}"
|
|
||||||
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
fi
|
|
||||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
|
||||||
cache_dir=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
if [[ -n "$cache_dir" ]]; then
|
|
||||||
rm -rf "$cache_dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")"
|
|
||||||
|
|
||||||
printf '%s\n' 'CLI(Request& context) { deliberate_log_timeliness_compile_failure }' >"$source_dir/probe.uce"
|
|
||||||
rm -rf "$cache_dir"
|
|
||||||
started_at=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
set +e
|
|
||||||
scripts/uce-cli --get "/$test_name/probe.uce" __uce_expected_compile_failure=1 >/dev/null 2>&1
|
|
||||||
set -e
|
|
||||||
|
|
||||||
for _ in $(seq 1 20); do
|
|
||||||
journal_output=$(journalctl -u uce --since "$started_at" --no-pager)
|
|
||||||
if [[ "$journal_output" == *"UCE expected compile error"* && "$journal_output" == *"$test_name/probe.uce"* ]]; then
|
|
||||||
echo "log timeliness passed"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "compile diagnostic was not journaled promptly: $test_name/probe.uce" >&2
|
|
||||||
exit 1
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="mysql-epoch-refresh-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
settings_file="${UCE_SETTINGS_FILE:-/etc/uce/settings.cfg}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r "$settings_file" ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file")
|
|
||||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
|
||||||
site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
bin_directory="${BIN_DIRECTORY:-}"
|
|
||||||
if [[ -z "$bin_directory" && -r "$settings_file" ]]; then
|
|
||||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file")
|
|
||||||
fi
|
|
||||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
|
||||||
ticks=$(awk -F= '/^[[:space:]]*WASM_EPOCH_DEADLINE_TICKS[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true)
|
|
||||||
period_ms=$(awk -F= '/^[[:space:]]*WASM_EPOCH_PERIOD_MS[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true)
|
|
||||||
ticks="${ticks:-200}"
|
|
||||||
period_ms="${period_ms:-50}"
|
|
||||||
segment_seconds=$(awk -v ticks="$ticks" -v period="$period_ms" 'BEGIN { printf "%.6f", ticks * period * 0.00055 }')
|
|
||||||
cache_dir=""
|
|
||||||
test_user="uce_epoch_$$"
|
|
||||||
test_password=$(printf '%s' "$test_name-$(date +%s%N)" | sha256sum | cut -c1-32)
|
|
||||||
test_user_created=false
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [[ "$test_user_created" == true ]]; then
|
|
||||||
mariadb -e "DROP USER IF EXISTS '$test_user'@'127.0.0.1'" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
if [[ -n "$cache_dir" ]]; then
|
|
||||||
rm -rf "$cache_dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")"
|
|
||||||
mariadb -e "DROP USER IF EXISTS '$test_user'@'127.0.0.1'; CREATE USER '$test_user'@'127.0.0.1' IDENTIFIED BY '$test_password'"
|
|
||||||
test_user_created=true
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'void mysql_epoch_burn(f64 seconds) {' \
|
|
||||||
' f64 deadline = time_precise() + seconds;' \
|
|
||||||
' u64 spins = 0; while(time_precise() < deadline) spins++;' \
|
|
||||||
'}' \
|
|
||||||
'CLI(Request& context) {' \
|
|
||||||
" MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\");" \
|
|
||||||
' if(db == 0 || mysql_error(db) != "") { print("mysql-connect-failed:", db == 0 ? "null" : mysql_error(db)); return; }' \
|
|
||||||
" mysql_epoch_burn($segment_seconds);" \
|
|
||||||
' mysql_query(db, "SELECT 1 AS value");' \
|
|
||||||
" mysql_epoch_burn($segment_seconds);" \
|
|
||||||
' mysql_query(db, "SELECT 2 AS value");' \
|
|
||||||
' if(mysql_error(db) == "") print("mysql-epoch-refresh-ok"); else print("mysql-query-failed:", mysql_error(db));' \
|
|
||||||
' mysql_disconnect(db);' \
|
|
||||||
'}' >"$source_dir/test.uce"
|
|
||||||
|
|
||||||
output=$(scripts/uce-cli "/$test_name/test.uce" 2>&1) || {
|
|
||||||
echo "MySQL hostcall did not refresh the guest epoch deadline: $output" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if [[ "$output" != "mysql-epoch-refresh-ok" ]]; then
|
|
||||||
echo "MySQL epoch refresh failed: $output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "MySQL epoch refresh passed with ${segment_seconds}s guest segments"
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="mysql-persistent-pool-test-$$"
|
|
||||||
settings_file="${UCE_SETTINGS_FILE:-/etc/uce/settings.cfg}"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r "$settings_file" ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file")
|
|
||||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
|
||||||
site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true)
|
|
||||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
|
||||||
cache_dir=""
|
|
||||||
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
|
|
||||||
test_user="uce_pool_$$"
|
|
||||||
test_database="uce_pool_$$"
|
|
||||||
test_database_other="uce_pool_other_$$"
|
|
||||||
test_password=$(printf '%s' "$test_name-$(date +%s%N)" | sha256sum | cut -c1-32)
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
mariadb -e "DROP DATABASE IF EXISTS \`$test_database\`; DROP DATABASE IF EXISTS \`$test_database_other\`; DROP USER IF EXISTS '$test_user'@'127.0.0.1'" >/dev/null 2>&1 || true
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
if [[ -n "$cache_dir" ]]; then
|
|
||||||
rm -rf "$cache_dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")"
|
|
||||||
mariadb -e "DROP DATABASE IF EXISTS \`$test_database\`; DROP DATABASE IF EXISTS \`$test_database_other\`; DROP USER IF EXISTS '$test_user'@'127.0.0.1'; CREATE DATABASE \`$test_database\`; CREATE DATABASE \`$test_database_other\`; CREATE USER '$test_user'@'127.0.0.1' IDENTIFIED BY '$test_password'; GRANT ALL ON \`$test_database\`.* TO '$test_user'@'127.0.0.1'; GRANT ALL ON \`$test_database_other\`.* TO '$test_user'@'127.0.0.1'; CREATE TABLE \`$test_database\`.pool_identity (label VARCHAR(16) NOT NULL); INSERT INTO \`$test_database\`.pool_identity VALUES ('primary'); CREATE TABLE \`$test_database_other\`.pool_identity (label VARCHAR(16) NOT NULL); INSERT INTO \`$test_database_other\`.pool_identity VALUES ('other')"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'RENDER(Request& context)' \
|
|
||||||
'{' \
|
|
||||||
" MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\", \"$test_database\");" \
|
|
||||||
" MySQL* other = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\", \"$test_database_other\");" \
|
|
||||||
" MySQL* unselected = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\");" \
|
|
||||||
' if(db == 0 || other == 0 || unselected == 0 || !mysql_connected(db) || !mysql_connected(other) || !mysql_connected(unselected)) { context.set_status(500, "MySQL connect failed"); print("connect-failed"); return; }' \
|
|
||||||
' String primary_database, primary_label, other_database, other_label;' \
|
|
||||||
' mysql_query(db, "SELECT DATABASE() AS db, label FROM pool_identity").each([&](DValue row, String key) { primary_database = row["db"].to_string(); primary_label = row["label"].to_string(); });' \
|
|
||||||
' mysql_query(other, "SELECT DATABASE() AS db, label FROM pool_identity").each([&](DValue row, String key) { other_database = row["db"].to_string(); other_label = row["label"].to_string(); });' \
|
|
||||||
' String unselected_database;' \
|
|
||||||
' mysql_query(unselected, "SELECT DATABASE() AS db").each([&](DValue row, String key) { unselected_database = row["db"].to_string(); });' \
|
|
||||||
" bool database_clean = primary_database == \"$test_database\" && primary_label == \"primary\" && other_database == \"$test_database_other\" && other_label == \"other\" && unselected_database == \"\";" \
|
|
||||||
' DValue marker_rows = mysql_query(db, "SELECT @uce_pool_marker AS marker");' \
|
|
||||||
' String marker; marker_rows.each([&](DValue row, String key) { marker = row["marker"].to_string(); });' \
|
|
||||||
' bool marker_clean = marker == "";' \
|
|
||||||
' mysql_query(db, "SELECT id FROM uce_persistent_temp LIMIT 1");' \
|
|
||||||
' bool temp_clean = mysql_error(db) != "";' \
|
|
||||||
" mysql_query(db, \"SET @uce_pool_marker='dirty'\");" \
|
|
||||||
' mysql_query(db, "CREATE TEMPORARY TABLE uce_persistent_temp (id INT PRIMARY KEY)");' \
|
|
||||||
" mysql_query(db, \"USE \\\`$test_database_other\\\`\");" \
|
|
||||||
" mysql_query(other, \"USE \\\`$test_database\\\`\");" \
|
|
||||||
" mysql_query(unselected, \"USE \\\`$test_database\\\`\");" \
|
|
||||||
' DValue perf = request_perf();' \
|
|
||||||
' String source;' \
|
|
||||||
' perf["mysql_operations"].each([&](DValue operation, String key) { if(operation["op"].to_string() == "connect" && operation["source"].to_string() == "worker") source = "worker"; });' \
|
|
||||||
' print(perf["worker_pid"].to_string(), "|", source, "|", marker_clean ? "clean" : "dirty", "|", temp_clean ? "clean" : "dirty", "|", database_clean ? "clean" : "dirty");' \
|
|
||||||
' mysql_disconnect(unselected);' \
|
|
||||||
' mysql_disconnect(other);' \
|
|
||||||
' mysql_disconnect(db);' \
|
|
||||||
'}' >"$source_dir/test.uce"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'RENDER(Request& context)' \
|
|
||||||
'{' \
|
|
||||||
" MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\", \"${test_database}_missing\");" \
|
|
||||||
' String error = mysql_error(db);' \
|
|
||||||
' print(!mysql_connected(db) && error != "" ? "database-selection-failed" : "database-selection-was-ignored");' \
|
|
||||||
' mysql_disconnect(db);' \
|
|
||||||
'}' >"$source_dir/failure.uce"
|
|
||||||
|
|
||||||
failure=$(curl -fsS --max-time 10 -H "Host: $http_host" "http://127.0.0.1/$test_name/failure.uce")
|
|
||||||
if [[ "$failure" != "database-selection-failed" ]]; then
|
|
||||||
echo "Unknown initial database did not surface a connection failure: $failure" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
reused=0
|
|
||||||
for _ in $(seq 1 160); do
|
|
||||||
output=$(curl -fsS --max-time 10 -H "Host: $http_host" "http://127.0.0.1/$test_name/test.uce")
|
|
||||||
if [[ "$output" == *"|worker|"* ]]; then
|
|
||||||
reused=$((reused + 1))
|
|
||||||
if [[ "$output" != *"|worker|clean|clean|clean" ]]; then
|
|
||||||
echo "Persistent MySQL reuse leaked cross-request state: $output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ "$reused" -eq 0 ]]; then
|
|
||||||
echo "Persistent MySQL pool did not produce a worker reuse in 160 requests" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Persistent MySQL pool reset passed across $reused reused requests"
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="nested-component-props-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
|
||||||
site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
threshold_ms="${UCE_NESTED_COMPONENT_PROPS_MAX_MS:-1500}"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'CLI(Request& context) {' \
|
|
||||||
' context.props["sentinel"] = "caller";' \
|
|
||||||
' String payload; for(u64 i = 0; i < 4096; ++i) payload += "x";' \
|
|
||||||
' DValue props; for(u64 i = 0; i < 300; ++i) { DValue item; item = payload; props["items"].push(item); }' \
|
|
||||||
' String output = component("outer", props, context);' \
|
|
||||||
' if(context.props["sentinel"].to_string() != "caller") { print("caller props not restored"); return; }' \
|
|
||||||
' print(output);' \
|
|
||||||
'}' >"$source_dir/parent.uce"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'COMPONENT(Request& context) {' \
|
|
||||||
' if(context.props["items"]._map.size() != 300) { print("outer props missing"); return; }' \
|
|
||||||
' for(u64 i = 0; i < 300; ++i) {' \
|
|
||||||
' DValue props; props["index"] = std::to_string(i); component("leaf", props, context);' \
|
|
||||||
' if(context.props["items"]._map.size() != 300) { print("outer props not restored"); return; }' \
|
|
||||||
' }' \
|
|
||||||
' print("nested-component-props-ok");' \
|
|
||||||
'}' >"$source_dir/outer.uce"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'COMPONENT(Request& context) {' \
|
|
||||||
' if(context.props["index"].to_string() == "") print("leaf props missing");' \
|
|
||||||
'}' >"$source_dir/leaf.uce"
|
|
||||||
|
|
||||||
warm_output=$(scripts/uce-cli "/$test_name/parent.uce")
|
|
||||||
if [[ "$warm_output" != "nested-component-props-ok" ]]; then
|
|
||||||
echo "nested component props warmup failed: $warm_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
start_ns=$(date +%s%N)
|
|
||||||
output=$(scripts/uce-cli "/$test_name/parent.uce")
|
|
||||||
elapsed_ms=$(( ($(date +%s%N) - start_ns) / 1000000 ))
|
|
||||||
if [[ "$output" != "nested-component-props-ok" ]]; then
|
|
||||||
echo "nested component props failed: $output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if (( elapsed_ms > threshold_ms )); then
|
|
||||||
echo "nested component props took ${elapsed_ms}ms (limit ${threshold_ms}ms)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "nested component props passed in ${elapsed_ms}ms"
|
|
||||||
@ -1,179 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="parallel-precompile-test-$$"
|
|
||||||
test_root="/tmp/$test_name"
|
|
||||||
source_dir="$test_root/site"
|
|
||||||
bin_directory="$test_root/work"
|
|
||||||
absolute_source_dir=""
|
|
||||||
artifact_dir=""
|
|
||||||
generation_log=""
|
|
||||||
generation_pid=""
|
|
||||||
precompile_timeout="${UCE_TEST_PRECOMPILE_TIMEOUT:-120s}"
|
|
||||||
|
|
||||||
precompile() {
|
|
||||||
timeout "$precompile_timeout" env UCE_PRECOMPILE_FILES_IN="$source_dir" UCE_PRECOMPILE_BIN_DIRECTORY="$bin_directory" "$@" bin/uce_fastcgi.linux.bin --precompile
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [[ -n "$generation_pid" ]] && kill -0 "$generation_pid" 2>/dev/null; then
|
|
||||||
kill "$generation_pid" 2>/dev/null || true
|
|
||||||
cleanup_deadline=$((SECONDS + 10))
|
|
||||||
while kill -0 "$generation_pid" 2>/dev/null && (( SECONDS < cleanup_deadline )); do
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
if kill -0 "$generation_pid" 2>/dev/null; then
|
|
||||||
kill -KILL "$generation_pid" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
wait "$generation_pid" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
rm -rf "$test_root"
|
|
||||||
if [[ -n "$generation_log" ]]; then
|
|
||||||
rm -f "$generation_log"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
absolute_source_dir=$(realpath "$source_dir")
|
|
||||||
artifact_dir="$(scripts/unit_cache_directory "$bin_directory")$absolute_source_dir"
|
|
||||||
|
|
||||||
write_units() {
|
|
||||||
local version="$1"
|
|
||||||
for unit in 0 1 2 3; do
|
|
||||||
printf 'CLI(Request& context) { print("parallel-precompile-%s-%s"); }\n' "$version" "$unit" >"$source_dir/unit-$unit.uce"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
printf 'CLI(Request& context) { print("parallel-precompile-warmup"); }\n' >"$source_dir/warmup.uce"
|
|
||||||
precompile UCE_PRECOMPILE_JOBS=1 UCE_WASM_PCH_DIR="$artifact_dir/pch-shared" >/dev/null
|
|
||||||
rm "$source_dir/warmup.uce"
|
|
||||||
|
|
||||||
write_units serial
|
|
||||||
serial_start=$(date +%s%N)
|
|
||||||
serial_output=$(precompile UCE_PRECOMPILE_JOBS=1 UCE_WASM_PCH_DIR="$artifact_dir/pch-shared")
|
|
||||||
serial_ns=$(( $(date +%s%N) - serial_start ))
|
|
||||||
if ! grep -Eq 'with 1 job: .* 4 compiled, 0 failed, worker status ok' <<<"$serial_output"; then
|
|
||||||
echo "serial precompile did not compile the four controlled units" >&2
|
|
||||||
echo "$serial_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
write_units parallel
|
|
||||||
parallel_start=$(date +%s%N)
|
|
||||||
parallel_output=$(precompile UCE_PRECOMPILE_JOBS=2 UCE_WASM_PCH_DIR="$artifact_dir/pch-shared")
|
|
||||||
parallel_ns=$(( $(date +%s%N) - parallel_start ))
|
|
||||||
if [[ $(grep -Ec '^Precompile worker [12]/2:' <<<"$parallel_output") -ne 2 ]]; then
|
|
||||||
echo "parallel precompile did not report both workers" >&2
|
|
||||||
echo "$parallel_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! grep -Eq 'with 2 jobs: .* 4 compiled, 0 failed, worker status ok' <<<"$parallel_output"; then
|
|
||||||
echo "parallel precompile did not compile the four controlled units" >&2
|
|
||||||
echo "$parallel_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
for unit in 0 1 2 3; do
|
|
||||||
if [[ ! -s "$artifact_dir/unit-$unit.uce.wasm" || ! -s "$artifact_dir/unit-$unit.uce.cwasm" ]]; then
|
|
||||||
echo "parallel precompile did not publish wasm and serialized artifacts for unit $unit" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
printf 'CLI(Request& context) { print("parallel-precompile-race-0"); }\n' >"$source_dir/race-0.uce"
|
|
||||||
printf 'CLI(Request& context) { print("parallel-precompile-race-1"); }\n' >"$source_dir/race-1.uce"
|
|
||||||
race_output=$(precompile UCE_PRECOMPILE_JOBS=2 UCE_WASM_PCH_DIR="$artifact_dir/pch-race")
|
|
||||||
if ! grep -Eq 'with 2 jobs: .* 2 compiled, 0 failed, worker status ok' <<<"$race_output" || \
|
|
||||||
[[ $(find "$artifact_dir/pch-race" -maxdepth 1 -type f -name '*.pch' | wc -l) -ne 1 ]] || \
|
|
||||||
find "$artifact_dir/pch-race" -maxdepth 1 -type f -name '*.tmp.*' -print -quit | grep -q .; then
|
|
||||||
echo "parallel precompile did not publish exactly one clean shared PCH" >&2
|
|
||||||
echo "$race_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm "$source_dir/race-0.uce" "$source_dir/race-1.uce"
|
|
||||||
|
|
||||||
for unit in $(seq -w 0 9); do
|
|
||||||
printf 'CLI(Request& context) { print("parallel-precompile-generation-%s"); }\n' "$unit" >"$source_dir/generation-$unit.uce"
|
|
||||||
done
|
|
||||||
generation_log="/tmp/$test_name-generation.log"
|
|
||||||
precompile UCE_PRECOMPILE_JOBS=2 UCE_WASM_PCH_DIR="$artifact_dir/pch-shared" >"$generation_log" 2>&1 &
|
|
||||||
generation_pid=$!
|
|
||||||
generation_deadline=$((SECONDS + 60))
|
|
||||||
while [[ ! -s "$artifact_dir/generation-0.uce.meta.txt" ]]; do
|
|
||||||
if ! kill -0 "$generation_pid" 2>/dev/null; then
|
|
||||||
echo "parallel precompile exited before the controlled source mutation" >&2
|
|
||||||
wait "$generation_pid" || true
|
|
||||||
cat "$generation_log" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if (( SECONDS >= generation_deadline )); then
|
|
||||||
echo "timed out waiting for the controlled precompile mutation boundary" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
printf 'CLI(Request& context) { print("parallel-precompile-generation-mutated"); }\n' >"$source_dir/generation-0.uce"
|
|
||||||
printf 'CLI(Request& context) { print("parallel-precompile-generation-added"); }\n' >"$source_dir/generation-new.uce"
|
|
||||||
if ! kill -0 "$generation_pid" 2>/dev/null; then
|
|
||||||
echo "parallel precompile exited at the controlled source mutation boundary" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
set +e
|
|
||||||
wait "$generation_pid"
|
|
||||||
generation_rc=$?
|
|
||||||
set -e
|
|
||||||
generation_pid=""
|
|
||||||
if [[ $generation_rc -eq 0 ]] || ! grep -q 'source generation changed during precompile; candidate generation rejected' "$generation_log"; then
|
|
||||||
echo "parallel precompile accepted a source generation that changed during compilation" >&2
|
|
||||||
cat "$generation_log" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -e "$artifact_dir/generation-new.uce.wasm" || -e "$artifact_dir/generation-new.uce.cwasm" ]]; then
|
|
||||||
echo "parallel precompile published an added unit outside the rejected candidate source set" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
generation_retry_output=$(precompile UCE_PRECOMPILE_JOBS=2 UCE_WASM_PCH_DIR="$artifact_dir/pch-shared")
|
|
||||||
if ! grep -Eq 'with 2 jobs: .* 2 compiled, 0 failed, worker status ok' <<<"$generation_retry_output"; then
|
|
||||||
echo "parallel precompile did not reconcile the changed and added units on retry" >&2
|
|
||||||
echo "$generation_retry_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm -f "$generation_log"
|
|
||||||
|
|
||||||
printf 'CLI(Request& context) { print(deliberate_parallel_precompile_failure); }\n' >"$source_dir/broken.uce"
|
|
||||||
set +e
|
|
||||||
failure_output=$(precompile UCE_PRECOMPILE_JOBS=2 UCE_WASM_PCH_DIR="$artifact_dir/pch-shared" 2>&1)
|
|
||||||
failure_rc=$?
|
|
||||||
set -e
|
|
||||||
if [[ $failure_rc -eq 0 ]] || ! grep -Eq 'with 2 jobs: .* 1 failed, worker status failed' <<<"$failure_output"; then
|
|
||||||
echo "parallel precompile did not aggregate a controlled worker failure" >&2
|
|
||||||
echo "$failure_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
set +e
|
|
||||||
repeat_failure_output=$(precompile UCE_PRECOMPILE_JOBS=2 UCE_WASM_PCH_DIR="$artifact_dir/pch-shared" 2>&1)
|
|
||||||
repeat_failure_rc=$?
|
|
||||||
set -e
|
|
||||||
if [[ $repeat_failure_rc -eq 0 ]] || ! grep -Eq 'with 2 jobs: .* 1 failed, worker status failed' <<<"$repeat_failure_output"; then
|
|
||||||
echo "parallel precompile accepted a persisted current compile failure" >&2
|
|
||||||
echo "$repeat_failure_output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm "$source_dir/broken.uce"
|
|
||||||
|
|
||||||
rm -f "$source_dir"/unit-*.uce
|
|
||||||
for unit in $(seq -w 0 4); do
|
|
||||||
printf 'CLI(Request& context) { print("parallel-precompile-bounds-%s"); }\n' "$unit" >"$source_dir/bounds-$unit.uce"
|
|
||||||
done
|
|
||||||
negative_jobs_output=$(precompile UCE_PRECOMPILE_JOBS=-1)
|
|
||||||
malformed_jobs_output=$(precompile UCE_PRECOMPILE_JOBS=invalid)
|
|
||||||
oversized_jobs_output=$(precompile UCE_PRECOMPILE_JOBS=17)
|
|
||||||
if ! grep -q 'with 1 job: 16 units,' <<<"$negative_jobs_output" || ! grep -q 'with 2 jobs: 16 units,' <<<"$malformed_jobs_output" || ! grep -q 'with 16 jobs: 16 units,' <<<"$oversized_jobs_output"; then
|
|
||||||
echo "precompile job bounds did not map negative/malformed/oversized input to 1/2/16" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm -f "$source_dir"/bounds-*.uce "$source_dir"/generation-*.uce
|
|
||||||
|
|
||||||
printf 'parallel precompile passed: serial %.3fs, parallel %.3fs\n' \
|
|
||||||
"$(awk -v ns="$serial_ns" 'BEGIN { print ns / 1000000000 }')" \
|
|
||||||
"$(awk -v ns="$parallel_ns" 'BEGIN { print ns / 1000000000 }')"
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="password-hash-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
|
||||||
site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
bin_directory="${BIN_DIRECTORY:-}"
|
|
||||||
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
fi
|
|
||||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
|
||||||
cache_dir=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
if [[ -n "$cache_dir" ]]; then
|
|
||||||
rm -rf "$cache_dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'CLI(Request& context) {' \
|
|
||||||
' String encoded = password_hash("correct horse battery staple");' \
|
|
||||||
' String second = password_hash("correct horse battery staple");' \
|
|
||||||
' String weaker = "$uce$scrypt$16384$8$1$00112233445566778899aabbccddeeff$29fdfb3d991961e926a19c1136a07e252afa5fdb8d3a0fb74cdcfa5016956f34";' \
|
|
||||||
' String excessive = "$uce$scrypt$131072$8$1$00112233445566778899aabbccddeeff$0000000000000000000000000000000000000000000000000000000000000000";' \
|
|
||||||
' print(encoded, "\n", encoded != second ? "randomized" : "reused", "\n", password_verify("correct horse battery staple", encoded) ? "valid" : "invalid", "\n", password_verify("wrong password", encoded) ? "wrong-valid" : "wrong-rejected", "\n", password_needs_rehash(encoded) ? "rehash" : "current", "\n", password_verify("legacy password", weaker) && password_needs_rehash(weaker) ? "legacy-valid-rehash" : "legacy-failed", "\n", !password_verify("password", excessive) && password_needs_rehash(excessive) ? "excessive-rejected" : "excessive-accepted", "\n", !password_verify("password", "$uce$scrypt$65536$8$1$zz$00") && password_needs_rehash("malformed") ? "malformed-rejected" : "malformed-valid");' \
|
|
||||||
'}' >"$source_dir/test.uce"
|
|
||||||
|
|
||||||
output=$(scripts/uce-cli "/$test_name/test.uce")
|
|
||||||
if [[ "$output" != *'$uce$scrypt$65536$8$1$'* || "$output" != *$'\nrandomized\nvalid\nwrong-rejected\ncurrent\nlegacy-valid-rehash\nexcessive-rejected\nmalformed-rejected'* ]]; then
|
|
||||||
echo "native password hashing failed: $output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "password hashing passed"
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
http_port="${UCE_RAW_HTTP_TEST_PORT:-}"
|
|
||||||
if [[ -z "$http_port" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
http_port=$(awk -F= '/^[[:space:]]*HTTP_PORT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
fi
|
|
||||||
http_port="${http_port:-8080}"
|
|
||||||
request_path="${UCE_RAW_HTTP_TEST_PATH:-/info/index.uce}"
|
|
||||||
marker="raw-http-log-$$-$(date +%s%N)"
|
|
||||||
separator="?"
|
|
||||||
if [[ "$request_path" == *\?* ]]; then
|
|
||||||
separator="&"
|
|
||||||
fi
|
|
||||||
started_at=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
response=$(curl -fsS --max-time 15 "http://127.0.0.1:${http_port}${request_path}${separator}__uce_log_probe=${marker}")
|
|
||||||
if [[ -z "$response" ]]; then
|
|
||||||
echo "raw HTTP probe returned an empty response" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
request_logs=""
|
|
||||||
for _ in $(seq 1 30); do
|
|
||||||
request_logs=$(journalctl -u uce --since "$started_at" --no-pager | grep '(r)' | grep "$marker" || true)
|
|
||||||
if [[ $(printf '%s\n' "$request_logs" | grep -c '(r)' || true) -ge 2 ]]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $(printf '%s\n' "$request_logs" | grep -c '(r)' || true) -ne 2 ]]; then
|
|
||||||
echo "expected exactly two raw HTTP request-stage records" >&2
|
|
||||||
printf '%s\n' "$request_logs" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ $(printf '%s\n' "$request_logs" | grep -c 'transport:http' || true) -ne 1 ||
|
|
||||||
$(printf '%s\n' "$request_logs" | grep -c 'transport:fastcgi' || true) -ne 1 ]]; then
|
|
||||||
echo "raw HTTP request-stage records did not identify HTTP and FastCGI transports" >&2
|
|
||||||
printf '%s\n' "$request_logs" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mapfile -t durations < <(printf '%s\n' "$request_logs" | sed -n 's/.*[[:space:]]\([0-9][0-9.]*\)s[[:space:]]*fps:.*/\1/p')
|
|
||||||
if [[ ${#durations[@]} -ne 2 ]]; then
|
|
||||||
echo "could not parse both raw HTTP request-stage durations" >&2
|
|
||||||
printf '%s\n' "$request_logs" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
for duration in "${durations[@]}"; do
|
|
||||||
awk -v duration="$duration" 'BEGIN { exit !(duration > 0 && duration < 60) }' || {
|
|
||||||
echo "raw HTTP request-stage duration is outside (0, 60) seconds: ${duration}s" >&2
|
|
||||||
printf '%s\n' "$request_logs" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "raw HTTP request log passed"
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="relative-component-cache-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
|
||||||
site_directory="$configured_site_directory"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir/a" "$source_dir/b"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'CLI(Request& context) {' \
|
|
||||||
' print(component("a/parent", context));' \
|
|
||||||
' print("/");' \
|
|
||||||
' print(component("b/parent", context));' \
|
|
||||||
'}' >"$source_dir/entry.uce"
|
|
||||||
printf '%s\n' 'COMPONENT(Request& context) { print(component("child", context)); }' >"$source_dir/a/parent.uce"
|
|
||||||
printf '%s\n' 'COMPONENT(Request& context) { print("relative-a"); }' >"$source_dir/a/child.uce"
|
|
||||||
printf '%s\n' 'COMPONENT(Request& context) { print(component("child", context)); }' >"$source_dir/b/parent.uce"
|
|
||||||
printf '%s\n' 'COMPONENT(Request& context) { print("relative-b"); }' >"$source_dir/b/child.uce"
|
|
||||||
|
|
||||||
output=$(scripts/uce-cli "/$test_name/entry.uce")
|
|
||||||
if [[ "$output" != "relative-a/relative-b" ]]; then
|
|
||||||
echo "relative component cache crossed caller boundaries: $output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "relative component cache passed"
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
service_name="${UCE_TEST_SERVICE:-uce.service}"
|
|
||||||
socket_name="${UCE_TEST_SOCKET_SERVICE:-uce.socket}"
|
|
||||||
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
|
|
||||||
http_path="${UCE_TEST_HTTP_PATH:-/info/}"
|
|
||||||
requests="${UCE_TEST_RESTART_REQUESTS:-100}"
|
|
||||||
statuses=$(mktemp)
|
|
||||||
trap 'rm -f "$statuses"' EXIT
|
|
||||||
|
|
||||||
systemctl is-active --quiet "$socket_name"
|
|
||||||
systemctl is-active --quiet "$service_name"
|
|
||||||
socket_path=$(systemctl show -p Listen --value "$socket_name" | sed -n 's/ \+(Stream)$//p')
|
|
||||||
[[ -S "$socket_path" ]]
|
|
||||||
inode_before=$(stat -c %i "$socket_path")
|
|
||||||
|
|
||||||
(
|
|
||||||
for ((i = 0; i < requests; i++)); do
|
|
||||||
curl -sS --max-time 15 -o /dev/null -w '%{http_code}\n' \
|
|
||||||
-H "Host: $http_host" "http://127.0.0.1${http_path}"
|
|
||||||
done
|
|
||||||
) >"$statuses" &
|
|
||||||
load_pid=$!
|
|
||||||
sleep 0.05
|
|
||||||
systemctl restart "$service_name"
|
|
||||||
wait "$load_pid"
|
|
||||||
|
|
||||||
inode_after=$(stat -c %i "$socket_path")
|
|
||||||
[[ "$inode_after" = "$inode_before" ]]
|
|
||||||
[[ $(wc -l <"$statuses") -eq "$requests" ]]
|
|
||||||
if [[ $(grep -c '^200$' "$statuses") -ne "$requests" ]]; then
|
|
||||||
sort "$statuses" | uniq -c >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
systemctl is-active --quiet "$socket_name"
|
|
||||||
systemctl is-active --quiet "$service_name"
|
|
||||||
echo "socket activation restart passed across $requests HTTP requests"
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="unit-export-surface-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
bin_directory="${UCE_TEST_BIN_DIRECTORY:-/tmp/uce/work}"
|
|
||||||
if [[ -r /etc/uce/settings.cfg ]]; then
|
|
||||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*HTTP_DOCUMENT_ROOT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
site_directory="${configured_site_directory:-$site_directory}"
|
|
||||||
fi
|
|
||||||
if [[ -z "${UCE_TEST_BIN_DIRECTORY:-}" ]]; then
|
|
||||||
configured_bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
bin_directory="${configured_bin_directory:-$bin_directory}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
artifact_dir=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
if [[ -n "$artifact_dir" ]]; then
|
|
||||||
rm -rf "$artifact_dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
absolute_source_dir=$(realpath "$source_dir")
|
|
||||||
artifact_dir="$(scripts/unit_cache_directory "$bin_directory")$absolute_source_dir"
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'String visibility_used() { return("private-used"); }' \
|
|
||||||
'String visibility_unused() { return("uce-private-unused-marker-8f61d2"); }' \
|
|
||||||
'EXPORT String visibility_shared() { return("shared"); }' \
|
|
||||||
'CLI(Request& context) { print(visibility_shared(), ":", visibility_used(), ":", component("named:NAMED", context)); }' \
|
|
||||||
>"$source_dir/entry.uce"
|
|
||||||
printf '%s\n' \
|
|
||||||
'String named_used() { return("private-named-used"); }' \
|
|
||||||
'String named_unused() { return("uce-named-unused-marker-4ae973"); }' \
|
|
||||||
'EXPORT String named_shared() { return("named-export"); }' \
|
|
||||||
'COMPONENT:NAMED(Request& context) { print(named_shared(), ":", named_used()); }' \
|
|
||||||
>"$source_dir/named.uce"
|
|
||||||
|
|
||||||
output=$(scripts/uce-cli "/$test_name/entry.uce")
|
|
||||||
if [[ "$output" != "shared:private-used:named-export:private-named-used" ]]; then
|
|
||||||
echo "unit export surface runtime failed: $output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
entry_wasm="$artifact_dir/entry.uce.wasm"
|
|
||||||
named_wasm="$artifact_dir/named.uce.wasm"
|
|
||||||
python3 - "$entry_wasm" "$named_wasm" "$absolute_source_dir" <<'PY'
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from scripts.check_unit_wasm import collect
|
|
||||||
|
|
||||||
cases = [
|
|
||||||
(Path(sys.argv[1]), {"__wasm_call_ctors", "__uce_set_current_request", "__uce_cli", "visibility_shared"}, b"uce-private-unused-marker-8f61d2"),
|
|
||||||
(Path(sys.argv[2]), {"__wasm_call_ctors", "__uce_set_current_request", "__uce_component_NAMED", "named_shared"}, b"uce-named-unused-marker-4ae973"),
|
|
||||||
]
|
|
||||||
source_dir = Path(sys.argv[3])
|
|
||||||
for path, allowed, unused_marker in cases:
|
|
||||||
data = path.read_bytes()
|
|
||||||
customs, imports, exports = collect(path)
|
|
||||||
names = {name for name, _ in exports}
|
|
||||||
unexpected = names - allowed
|
|
||||||
missing = allowed - names
|
|
||||||
if unexpected or missing:
|
|
||||||
raise SystemExit(f"{path}: unexpected exports={sorted(unexpected)} missing={sorted(missing)} all={sorted(names)}")
|
|
||||||
if unused_marker in data:
|
|
||||||
raise SystemExit(f"{path}: retained unused private code marker")
|
|
||||||
if len(imports) >= 40:
|
|
||||||
raise SystemExit(f"{path}: retained {len(imports)} imports (expected fewer than 40)")
|
|
||||||
if path.stat().st_size >= 1024 * 1024:
|
|
||||||
raise SystemExit(f"{path}: artifact is {path.stat().st_size} bytes (expected under 1 MiB)")
|
|
||||||
source_map = Path(str(path) + ".source-map")
|
|
||||||
if not source_map.is_file() or source_map.stat().st_size >= 256 * 1024:
|
|
||||||
raise SystemExit(f"{path}: missing or oversized source map")
|
|
||||||
module = customs["uce.module"][-1].decode()
|
|
||||||
lines = source_map.read_text().splitlines()
|
|
||||||
if not lines or lines[0] != f"UCE_SOURCE_MAP_V1\t{module}":
|
|
||||||
raise SystemExit(f"{path}: source map does not match wasm module identity")
|
|
||||||
expected_source = str(source_dir / path.name.removesuffix(".wasm"))
|
|
||||||
if not any(line.startswith("F\t") and line.endswith("\t" + expected_source) for line in lines):
|
|
||||||
raise SystemExit(f"{path}: source map does not identify {expected_source}")
|
|
||||||
if not any(line.startswith("L\t") for line in lines):
|
|
||||||
raise SystemExit(f"{path}: source map has no address rows")
|
|
||||||
PY
|
|
||||||
|
|
||||||
echo "unit export surface passed"
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
test_name="wasm-source-location-test-$$"
|
|
||||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
|
||||||
bin_directory="${UCE_TEST_BIN_DIRECTORY:-/tmp/uce/work}"
|
|
||||||
if [[ -r /etc/uce/settings.cfg ]]; then
|
|
||||||
configured_site_directory=$(awk -F= '/^[[:space:]]*HTTP_DOCUMENT_ROOT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
configured_bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
|
||||||
[[ -n "${UCE_TEST_SITE_DIRECTORY:-}" ]] || site_directory="${configured_site_directory:-$site_directory}"
|
|
||||||
[[ -n "${UCE_TEST_BIN_DIRECTORY:-}" ]] || bin_directory="${configured_bin_directory:-$bin_directory}"
|
|
||||||
fi
|
|
||||||
source_dir="$site_directory/$test_name"
|
|
||||||
absolute_source_dir=""
|
|
||||||
artifact_dir=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$source_dir"
|
|
||||||
[[ -z "$artifact_dir" ]] || rm -rf "$artifact_dir"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
mkdir -p "$source_dir"
|
|
||||||
absolute_source_dir=$(realpath "$source_dir")
|
|
||||||
artifact_dir="$(scripts/unit_cache_directory "$bin_directory")$absolute_source_dir"
|
|
||||||
printf '%s\n' 'CLI(Request& context) { __builtin_trap(); }' >"$source_dir/entry.uce"
|
|
||||||
|
|
||||||
set +e
|
|
||||||
output=$(scripts/uce-cli "/$test_name/entry.uce" 2>&1)
|
|
||||||
status=$?
|
|
||||||
set -e
|
|
||||||
if [[ $status -eq 0 ]]; then
|
|
||||||
echo "trapping wasm unit unexpectedly succeeded" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
expected="$absolute_source_dir/entry.uce:1:25"
|
|
||||||
if [[ "$output" != *'wasm `unreachable` instruction executed'* || "$output" != *"source locations:"* || "$output" != *"$expected"* ]]; then
|
|
||||||
echo "wasm trap omitted its source location:" >&2
|
|
||||||
echo "$output" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm "$artifact_dir/entry.uce.wasm.source-map"
|
|
||||||
set +e
|
|
||||||
without_map=$(scripts/uce-cli "/$test_name/entry.uce" 2>&1)
|
|
||||||
status=$?
|
|
||||||
set -e
|
|
||||||
if [[ $status -eq 0 || "$without_map" != *'wasm `unreachable` instruction executed'* || "$without_map" == *"source locations:"* ]]; then
|
|
||||||
echo "missing-map trap fallback failed:" >&2
|
|
||||||
echo "$without_map" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "wasm source locations passed"
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
base="${1:-/tmp/uce/work}"
|
|
||||||
compiler_abi=$(awk '/^#define UCE_COMPILER_UNIT_ABI_VERSION / {print $3; exit}' src/wasm/abi.h)
|
|
||||||
core_abi=$(awk '/^#define UCE_WASM_CORE_ABI_VERSION / {print $3; exit}' src/wasm/abi.h)
|
|
||||||
printf '%s/units-c%s-w%s\n' "${base%/}" "$compiler_abi" "$core_abi"
|
|
||||||
@ -5,8 +5,3 @@ EXPORT DValue* test_func(DValue* call_param)
|
|||||||
print("HELLO FROM TEST FUNCTION");
|
print("HELLO FROM TEST FUNCTION");
|
||||||
return(0);
|
return(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER(Request& context)
|
|
||||||
{
|
|
||||||
print("HELLO FROM UNIT RENDER");
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,9 +6,9 @@ RENDER(Request& context)
|
|||||||
p.set(context.params);
|
p.set(context.params);
|
||||||
|
|
||||||
StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"};
|
StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"};
|
||||||
StringList unique_routes = routes.unique();
|
StringList unique_routes = list_unique(routes);
|
||||||
StringList sorted_routes = unique_routes.sort();
|
StringList sorted_routes = list_sort(unique_routes);
|
||||||
StringList labels = sorted_routes.map([](String route) { return(to_upper(replace(route, "/", " / "))); });
|
StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); });
|
||||||
|
|
||||||
DValue cards;
|
DValue cards;
|
||||||
DValue card;
|
DValue card;
|
||||||
@ -16,8 +16,9 @@ RENDER(Request& context)
|
|||||||
card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear();
|
card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear();
|
||||||
card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card);
|
card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card);
|
||||||
|
|
||||||
DValue app_cards = cards.filter([](DValue item, String key) { return(item["section"].to_string() == "app"); });
|
DValue app_cards = dv_filter(cards, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
|
||||||
DValue titles = app_cards.map([](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); });
|
DValue titles = dv_map(app_cards, [](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); });
|
||||||
|
DValue grouped = dv_group_by(cards, [](DValue item, String key) { return(item["section"].to_string()); });
|
||||||
|
|
||||||
<><html>
|
<><html>
|
||||||
<head>
|
<head>
|
||||||
@ -30,7 +31,7 @@ RENDER(Request& context)
|
|||||||
<p>Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.</p>
|
<p>Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.</p>
|
||||||
<div class="system-info"><h3>StringList route labels</h3><pre><?= join(labels, "\n") ?></pre></div>
|
<div class="system-info"><h3>StringList route labels</h3><pre><?= join(labels, "\n") ?></pre></div>
|
||||||
<div class="system-info"><h3>DValue app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
|
<div class="system-info"><h3>DValue app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
|
||||||
<div class="system-info"><h3>All card values</h3><pre><?= json_encode(cards.values()) ?></pre></div>
|
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
|
||||||
<details>
|
<details>
|
||||||
<summary>Request Parameters</summary>
|
<summary>Request Parameters</summary>
|
||||||
<pre><?= var_dump(p) ?></pre>
|
<pre><?= var_dump(p) ?></pre>
|
||||||
|
|||||||
@ -91,22 +91,8 @@ RENDER(Request& context)
|
|||||||
print("Accept us: ", (f64)perf["accept_us"].to_f64(), "\n");
|
print("Accept us: ", (f64)perf["accept_us"].to_f64(), "\n");
|
||||||
print("Running us: ", (f64)perf["running_us"].to_f64(), "\n");
|
print("Running us: ", (f64)perf["running_us"].to_f64(), "\n");
|
||||||
print("Total us: ", (f64)perf["total_us"].to_f64(), "\n");
|
print("Total us: ", (f64)perf["total_us"].to_f64(), "\n");
|
||||||
print("Dispatch us: ", (u64)perf["dispatch_us"].to_u64(), "\n");
|
if(perf["workspace_birth_us"].type != 'S')
|
||||||
print("Workspace setup us: ", (u64)perf["workspace_setup_us"].to_u64(), "\n");
|
print("Workspace birth us: ", (u64)perf["workspace_birth_us"].to_u64(), "\n");
|
||||||
print("Workspace birth us: ", (u64)perf["workspace_birth_us"].to_u64(), "\n");
|
|
||||||
print("Context apply us: ", (u64)perf["context_apply_us"].to_u64(), "\n");
|
|
||||||
print("Guest us: ", (f64)perf["guest_us"].to_f64(), "\n");
|
|
||||||
print("Hostcalls: ", (u64)perf["hostcall_count"].to_u64(), " / ", (u64)perf["hostcall_us"].to_u64(), " us\n");
|
|
||||||
print("MySQL hostcalls: ", (u64)perf["mysql_hostcall_count"].to_u64(), " / ", (u64)perf["mysql_hostcall_us"].to_u64(), " us\n");
|
|
||||||
print("MySQL operations: ", json_encode(perf["mysql_operations"]), " / dropped ", (u64)perf["mysql_operations_dropped"].to_u64(), "\n");
|
|
||||||
print("MySQL connections: ", (u64)perf["mysql_connection_open_count"].to_u64(), " new / ", (u64)perf["mysql_connection_reuse_count"].to_u64(), " worker reuse / ", (u64)perf["mysql_request_pool_hit_count"].to_u64(), " request reuse\n");
|
|
||||||
print("Memcache hostcalls: ", (u64)perf["memcache_hostcall_count"].to_u64(), " / ", (u64)perf["memcache_hostcall_us"].to_u64(), " us\n");
|
|
||||||
print("Component resolves: ", (u64)perf["component_resolve_count"].to_u64(), " / ", (u64)perf["component_resolve_us"].to_u64(), " us\n");
|
|
||||||
print("Component phases: path ", (u64)perf["component_path_us"].to_u64(), " / artifact ", (u64)perf["component_artifact_us"].to_u64(), " / load ", (u64)perf["component_load_us"].to_u64(), " / link ", (u64)perf["component_link_us"].to_u64(), " us\n");
|
|
||||||
print("Unit loads: ", (u64)perf["unit_load_count"].to_u64(), " / module ", (u64)perf["unit_module_us"].to_u64(), " / allocate ", (u64)perf["unit_allocate_us"].to_u64(), " / imports ", (u64)perf["unit_import_us"].to_u64(), " / instantiate ", (u64)perf["unit_instantiate_us"].to_u64(), " / initialize ", (u64)perf["unit_initialize_us"].to_u64(), " us\n");
|
|
||||||
print("Unit module cache: ", (u64)perf["unit_module_cache_hit_count"].to_u64(), " worker hits / ", (u64)perf["unit_module_cache_miss_count"].to_u64(), " misses / ", (u64)perf["unit_module_serialized_cache_hit_count"].to_u64(), " serialized hits / ", (u64)perf["unit_module_compile_count"].to_u64(), " wasm compiles\n");
|
|
||||||
print("Unit module phases: lookup ", (u64)perf["unit_module_lookup_us"].to_u64(), " / read ", (u64)perf["unit_module_read_us"].to_u64(), " us / ", (u64)perf["unit_module_read_bytes"].to_u64(), " bytes / parse ", (u64)perf["unit_module_parse_us"].to_u64(), " / deserialize/compile ", (u64)perf["unit_module_compile_us"].to_u64(), " / classify ", (u64)perf["unit_module_classify_us"].to_u64(), " us\n");
|
|
||||||
print("Unit symbol resolves: ", (u64)perf["unit_symbol_resolve_count"].to_u64(), " / ", (u64)perf["unit_symbol_resolve_us"].to_u64(), " us\n");
|
|
||||||
print("Output buffer size: ", context.ob->str().length(), "\n");
|
print("Output buffer size: ", context.ob->str().length(), "\n");
|
||||||
?></pre>
|
?></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,5 +6,3 @@ memcache_delete
|
|||||||
memcache_get
|
memcache_get
|
||||||
memcache_get_multiple
|
memcache_get_multiple
|
||||||
memcache_set
|
memcache_set
|
||||||
memcache_escape_key
|
|
||||||
memcache_escape_keys
|
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
MySQL Functions
|
MySQL Functions
|
||||||
|
|
||||||
mysql_connect
|
mysql_connect
|
||||||
mysql_connected
|
|
||||||
mysql_disconnect
|
mysql_disconnect
|
||||||
mysql_error
|
mysql_error
|
||||||
mysql_escape
|
mysql_escape
|
||||||
mysql_insert_id
|
mysql_insert_id
|
||||||
mysql_affected_rows
|
|
||||||
mysql_query
|
mysql_query
|
||||||
|
|||||||
@ -8,12 +8,3 @@ gen_noise64
|
|||||||
gen_float
|
gen_float
|
||||||
gen_int
|
gen_int
|
||||||
gen_sha1
|
gen_sha1
|
||||||
sha256
|
|
||||||
sha256_hex
|
|
||||||
hmac_sha256
|
|
||||||
hmac_sha256_hex
|
|
||||||
random_bytes
|
|
||||||
crypto_equal
|
|
||||||
password_hash
|
|
||||||
password_verify
|
|
||||||
password_needs_rehash
|
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
Runtime
|
Runtime
|
||||||
backtrace_get_frames
|
backtrace_frames_string
|
||||||
backtrace_capture
|
capture_backtrace_string
|
||||||
error_pages
|
error_pages
|
||||||
signal_name
|
signal_name
|
||||||
unit_info
|
unit_info
|
||||||
units_list
|
units_list
|
||||||
unit_compile
|
unit_compile
|
||||||
3_Documentation format
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
Socket Functions
|
Socket Functions
|
||||||
|
|
||||||
http_request
|
|
||||||
http_request_async
|
|
||||||
socket_close
|
socket_close
|
||||||
socket_connect
|
socket_connect
|
||||||
socket_read
|
socket_read
|
||||||
|
|||||||
@ -9,7 +9,12 @@ first
|
|||||||
join
|
join
|
||||||
json_consume_space
|
json_consume_space
|
||||||
json_encode
|
json_encode
|
||||||
|
list_every
|
||||||
|
list_find
|
||||||
map
|
map
|
||||||
|
list_some
|
||||||
|
list_sort
|
||||||
|
list_unique
|
||||||
nibble
|
nibble
|
||||||
print
|
print
|
||||||
strpos
|
strpos
|
||||||
@ -30,9 +35,3 @@ regex_split
|
|||||||
to_lower
|
to_lower
|
||||||
to_upper
|
to_upper
|
||||||
trim
|
trim
|
||||||
float_val
|
|
||||||
int_val
|
|
||||||
html_escape
|
|
||||||
safe_name
|
|
||||||
var_dump
|
|
||||||
json_decode
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ basename
|
|||||||
dirname
|
dirname
|
||||||
expand_path
|
expand_path
|
||||||
file_append
|
file_append
|
||||||
|
file_append_contents
|
||||||
file_exists
|
file_exists
|
||||||
file_get_contents
|
file_get_contents
|
||||||
file_mtime
|
file_mtime
|
||||||
@ -15,11 +16,6 @@ path_join
|
|||||||
cwd_set
|
cwd_set
|
||||||
shell_escape
|
shell_escape
|
||||||
shell_exec
|
shell_exec
|
||||||
shell_spawn
|
|
||||||
job_status
|
|
||||||
job_result
|
|
||||||
job_await
|
|
||||||
job_cancel
|
|
||||||
file_unlink
|
file_unlink
|
||||||
zip_create
|
zip_create
|
||||||
zip_list
|
zip_list
|
||||||
@ -29,26 +25,3 @@ gz_compress
|
|||||||
gz_uncompress
|
gz_uncompress
|
||||||
server_start_http
|
server_start_http
|
||||||
server_stop
|
server_stop
|
||||||
file_open
|
|
||||||
file_read
|
|
||||||
file_pread
|
|
||||||
file_write
|
|
||||||
file_pwrite
|
|
||||||
file_seek
|
|
||||||
file_tell
|
|
||||||
file_close
|
|
||||||
file_stat
|
|
||||||
dir_list
|
|
||||||
file_rename
|
|
||||||
file_copy
|
|
||||||
file_truncate
|
|
||||||
dir_remove
|
|
||||||
file_temp
|
|
||||||
file_chmod
|
|
||||||
file_symlink
|
|
||||||
file_fsync
|
|
||||||
path_is_within
|
|
||||||
path_real
|
|
||||||
process_start_directory
|
|
||||||
request_perf
|
|
||||||
runtime_safe_key
|
|
||||||
|
|||||||
@ -3,38 +3,26 @@ Types
|
|||||||
0_Request
|
0_Request
|
||||||
array_merge
|
array_merge
|
||||||
0_DValue
|
0_DValue
|
||||||
2_DValue_filter
|
dv_filter
|
||||||
2_DValue_keys
|
dv_group_by
|
||||||
2_DValue_map
|
dv_keys
|
||||||
2_DValue_values
|
dv_map
|
||||||
2_DValue_each
|
dv_omit
|
||||||
2_DValue_get_by_path
|
dv_pick
|
||||||
2_DValue_has
|
dv_values
|
||||||
2_DValue_is_array
|
each
|
||||||
2_DValue_is_list
|
get_by_path
|
||||||
2_Request_set_status
|
has
|
||||||
0_String
|
is_array
|
||||||
0_StringList
|
is_list
|
||||||
2_StringList_filter
|
set_status
|
||||||
2_StringList_map
|
String
|
||||||
2_StringList_keys
|
StringList
|
||||||
2_StringList_each
|
StringMap
|
||||||
2_StringList_unique
|
|
||||||
2_StringList_sort
|
|
||||||
2_StringList_some
|
|
||||||
2_StringList_every
|
|
||||||
2_StringList_find
|
|
||||||
0_StringMap
|
|
||||||
2_DValue_to_bool
|
|
||||||
2_DValue_to_f64
|
|
||||||
2_DValue_to_json
|
|
||||||
2_DValue_to_s64
|
|
||||||
2_DValue_to_string
|
|
||||||
2_DValue_to_stringmap
|
|
||||||
2_DValue_to_u64
|
|
||||||
to_bool
|
to_bool
|
||||||
to_f64
|
to_f64
|
||||||
|
to_json
|
||||||
to_s64
|
to_s64
|
||||||
|
to_string
|
||||||
|
to_stringmap
|
||||||
to_u64
|
to_u64
|
||||||
ucb_encode
|
|
||||||
ucb_decode
|
|
||||||
|
|||||||
@ -15,4 +15,3 @@ parse_query
|
|||||||
parse_uri
|
parse_uri
|
||||||
uri_decode
|
uri_decode
|
||||||
uri_encode
|
uri_encode
|
||||||
request_route_from_raw_path
|
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
// Small target unit used by the unit_*/component documentation examples.
|
|
||||||
RENDER(Request& context)
|
|
||||||
{
|
|
||||||
print("hello from the sample unit");
|
|
||||||
}
|
|
||||||
|
|
||||||
COMPONENT(Request& context)
|
|
||||||
{
|
|
||||||
print("hello from the sample component");
|
|
||||||
}
|
|
||||||
|
|
||||||
EXPORT DValue* doc_greet(DValue* call_param)
|
|
||||||
{
|
|
||||||
print("doc_greet() was called");
|
|
||||||
return(0);
|
|
||||||
}
|
|
||||||
@ -45,64 +45,6 @@ void render_doc_params(StringList param_lines)
|
|||||||
?></div></>
|
?></div></>
|
||||||
}
|
}
|
||||||
|
|
||||||
String doc_example_path(Request& context, String page, u64 idx)
|
|
||||||
{
|
|
||||||
String bin_dir = context.params["UCE_BIN_DIRECTORY"];
|
|
||||||
if(bin_dir == "")
|
|
||||||
return("");
|
|
||||||
String dir = path_join(bin_dir, "doc-examples");
|
|
||||||
mkdir(dir);
|
|
||||||
return(path_join(dir, page + "_" + std::to_string(idx + 1) + ".uce"));
|
|
||||||
}
|
|
||||||
|
|
||||||
String doc_example_unit_source(String body)
|
|
||||||
{
|
|
||||||
return("RENDER(Request& context)\n{\n" + body + "\n}\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool doc_write_if_changed(String path, String content)
|
|
||||||
{
|
|
||||||
if(file_exists(path) && file_get_contents(path) == content)
|
|
||||||
return(true);
|
|
||||||
return(file_put_contents(path, content));
|
|
||||||
}
|
|
||||||
|
|
||||||
String doc_render_example_output(Request& context, String page, u64 idx, String body)
|
|
||||||
{
|
|
||||||
String path = doc_example_path(context, page, idx);
|
|
||||||
if(path == "")
|
|
||||||
return("DOC EXAMPLE ERROR: UCE_BIN_DIRECTORY is not available");
|
|
||||||
String unit_source = doc_example_unit_source(body);
|
|
||||||
if(!doc_write_if_changed(path, unit_source))
|
|
||||||
return("DOC EXAMPLE ERROR: could not write " + path);
|
|
||||||
if(!unit_compile(path))
|
|
||||||
return("DOC EXAMPLE ERROR: unit_compile failed for " + path);
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
unit_render(path);
|
|
||||||
String out = ob_get_close();
|
|
||||||
if(out == "")
|
|
||||||
return("DOC EXAMPLE ERROR: example produced no output");
|
|
||||||
return(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
void render_doc_examples(Request& context, String page, StringList example_blocks)
|
|
||||||
{
|
|
||||||
for(u64 idx = 0; idx < example_blocks.size(); idx++)
|
|
||||||
{
|
|
||||||
String body = example_blocks[idx];
|
|
||||||
String output = doc_render_example_output(context, page, idx, body);
|
|
||||||
?><div class="doc-section example">
|
|
||||||
<h3>Example</h3>
|
|
||||||
<pre class="example-source"><?= body ?></pre>
|
|
||||||
<div class="example-output">
|
|
||||||
<div class="example-output-label">Output</div>
|
|
||||||
<pre><?= output ?></pre>
|
|
||||||
</div>
|
|
||||||
</div><?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void render_see_section(String name)
|
void render_see_section(String name)
|
||||||
{
|
{
|
||||||
StringList lines = split(file_get_contents("areas/" + name + ".txt"), "\n");
|
StringList lines = split(file_get_contents("areas/" + name + ".txt"), "\n");
|
||||||
@ -283,8 +225,6 @@ RENDER(Request& context)
|
|||||||
?><div class="doc-section content"><?: markdown_to_html(doc_page.content) ?></div><?
|
?><div class="doc-section content"><?: markdown_to_html(doc_page.content) ?></div><?
|
||||||
}
|
}
|
||||||
|
|
||||||
render_doc_examples(context, page, doc_page.example_blocks);
|
|
||||||
|
|
||||||
?></article><?
|
?></article><?
|
||||||
|
|
||||||
if(doc_page.see_lines.size() > 0)
|
if(doc_page.see_lines.size() > 0)
|
||||||
|
|||||||
@ -5,7 +5,6 @@ struct DocPage {
|
|||||||
String content;
|
String content;
|
||||||
StringList sig_lines;
|
StringList sig_lines;
|
||||||
StringList param_lines;
|
StringList param_lines;
|
||||||
StringList example_blocks;
|
|
||||||
StringList see_lines;
|
StringList see_lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -14,25 +13,12 @@ enum class DocPageKind
|
|||||||
function,
|
function,
|
||||||
struct_page,
|
struct_page,
|
||||||
directive,
|
directive,
|
||||||
method,
|
|
||||||
info
|
info
|
||||||
};
|
};
|
||||||
|
|
||||||
String doc_method_label(String page)
|
|
||||||
{
|
|
||||||
String label = page;
|
|
||||||
nibble(label, "_");
|
|
||||||
String class_name = nibble(label, "_");
|
|
||||||
if(label == "")
|
|
||||||
return(class_name);
|
|
||||||
return(class_name + "::" + label);
|
|
||||||
}
|
|
||||||
|
|
||||||
String doc_default_title(String page)
|
String doc_default_title(String page)
|
||||||
{
|
{
|
||||||
String page_title = page;
|
String page_title = page;
|
||||||
if(page.substr(0, 2) == "2_")
|
|
||||||
return(doc_method_label(page));
|
|
||||||
if(page_title.length() > 1 && page_title[1] == '_')
|
if(page_title.length() > 1 && page_title[1] == '_')
|
||||||
nibble(page_title, "_");
|
nibble(page_title, "_");
|
||||||
return(page_title);
|
return(page_title);
|
||||||
@ -74,8 +60,6 @@ DocPageKind doc_page_kind(String page)
|
|||||||
return(DocPageKind::struct_page);
|
return(DocPageKind::struct_page);
|
||||||
if(page.substr(0, 2) == "1_")
|
if(page.substr(0, 2) == "1_")
|
||||||
return(DocPageKind::directive);
|
return(DocPageKind::directive);
|
||||||
if(page.substr(0, 2) == "2_")
|
|
||||||
return(DocPageKind::method);
|
|
||||||
if(page.substr(0, 2) == "3_")
|
if(page.substr(0, 2) == "3_")
|
||||||
return(DocPageKind::info);
|
return(DocPageKind::info);
|
||||||
return(DocPageKind::function);
|
return(DocPageKind::function);
|
||||||
@ -87,8 +71,6 @@ String doc_page_kind_badge(DocPageKind kind)
|
|||||||
return("struct");
|
return("struct");
|
||||||
if(kind == DocPageKind::directive)
|
if(kind == DocPageKind::directive)
|
||||||
return("directive");
|
return("directive");
|
||||||
if(kind == DocPageKind::method)
|
|
||||||
return("method");
|
|
||||||
if(kind == DocPageKind::info)
|
if(kind == DocPageKind::info)
|
||||||
return("info");
|
return("info");
|
||||||
return("");
|
return("");
|
||||||
@ -98,76 +80,35 @@ String doc_index_label(String page)
|
|||||||
{
|
{
|
||||||
String label = page;
|
String label = page;
|
||||||
auto kind = doc_page_kind(page);
|
auto kind = doc_page_kind(page);
|
||||||
if(kind == DocPageKind::method)
|
|
||||||
return(doc_method_label(page));
|
|
||||||
if(kind == DocPageKind::struct_page || kind == DocPageKind::directive || kind == DocPageKind::info)
|
if(kind == DocPageKind::struct_page || kind == DocPageKind::directive || kind == DocPageKind::info)
|
||||||
nibble(label, "_");
|
nibble(label, "_");
|
||||||
return(label);
|
return(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
void doc_flush_section(DocPage& result, String page, String section, StringList& section_lines, StringList& content_lines)
|
|
||||||
{
|
|
||||||
if(section == "")
|
|
||||||
return;
|
|
||||||
if(section == "title")
|
|
||||||
{
|
|
||||||
String title = trim(join(section_lines, "\n"));
|
|
||||||
if(title != page)
|
|
||||||
result.title = title;
|
|
||||||
}
|
|
||||||
else if(section == "sig")
|
|
||||||
{
|
|
||||||
for(String line : section_lines)
|
|
||||||
result.sig_lines.push_back(line);
|
|
||||||
}
|
|
||||||
else if(section == "params")
|
|
||||||
{
|
|
||||||
for(String line : section_lines)
|
|
||||||
result.param_lines.push_back(line);
|
|
||||||
}
|
|
||||||
else if(section == "see")
|
|
||||||
{
|
|
||||||
for(String line : section_lines)
|
|
||||||
{
|
|
||||||
line = trim(line);
|
|
||||||
if(line != "")
|
|
||||||
result.see_lines.push_back(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(section == "example")
|
|
||||||
{
|
|
||||||
String example = join(section_lines, "\n");
|
|
||||||
if(trim(example) != "")
|
|
||||||
result.example_blocks.push_back(example);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for(String line : section_lines)
|
|
||||||
content_lines.push_back(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DocPage load_doc_page(String page)
|
DocPage load_doc_page(String page)
|
||||||
{
|
{
|
||||||
DocPage result;
|
DocPage result;
|
||||||
StringList lines = split(file_get_contents("pages/" + page + ".txt"), "\n");
|
StringList lines = split(file_get_contents("pages/" + page + ".txt"), "\n");
|
||||||
String current_section = "";
|
String current_section = "";
|
||||||
StringList current_lines;
|
bool content_mode = false;
|
||||||
StringList content_lines;
|
StringList content_lines;
|
||||||
|
|
||||||
for(auto line : lines)
|
for(auto line : lines)
|
||||||
{
|
{
|
||||||
if(line != "" && line.substr(0, 1) == ":")
|
if(!content_mode && line != "" && line.substr(0, 1) == ":")
|
||||||
{
|
{
|
||||||
doc_flush_section(result, page, current_section, current_lines, content_lines);
|
|
||||||
current_lines.clear();
|
|
||||||
|
|
||||||
String section = trim(line.substr(1));
|
String section = trim(line.substr(1));
|
||||||
if(section == "title" || section == "sig" || section == "params" || section == "content" || section == "example" || section == "see")
|
if(section == "title" || section == "sig" || section == "params" || section == "see")
|
||||||
{
|
{
|
||||||
current_section = section;
|
current_section = section;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if(section == "content")
|
||||||
|
{
|
||||||
|
content_mode = true;
|
||||||
|
current_section = "content";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
current_section = "legacy";
|
current_section = "legacy";
|
||||||
String heading = doc_legacy_heading(section);
|
String heading = doc_legacy_heading(section);
|
||||||
@ -181,10 +122,31 @@ DocPage load_doc_page(String page)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
current_lines.push_back(line);
|
if(current_section == "title")
|
||||||
|
{
|
||||||
|
if(result.title != "")
|
||||||
|
result.title += "\n";
|
||||||
|
result.title += line;
|
||||||
|
}
|
||||||
|
else if(current_section == "sig")
|
||||||
|
{
|
||||||
|
result.sig_lines.push_back(line);
|
||||||
|
}
|
||||||
|
else if(current_section == "params")
|
||||||
|
{
|
||||||
|
result.param_lines.push_back(line);
|
||||||
|
}
|
||||||
|
else if(current_section == "see")
|
||||||
|
{
|
||||||
|
if(trim(line) != "")
|
||||||
|
result.see_lines.push_back(trim(line));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
content_lines.push_back(line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doc_flush_section(result, page, current_section, current_lines, content_lines);
|
|
||||||
result.content = join(content_lines, "\n");
|
result.content = join(content_lines, "\n");
|
||||||
result.title = trim(result.title);
|
result.title = trim(result.title);
|
||||||
return(result);
|
return(result);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ DValue
|
|||||||
|
|
||||||
:see
|
:see
|
||||||
>types
|
>types
|
||||||
2_DValue_get_by_path
|
get_by_path
|
||||||
json_decode
|
json_decode
|
||||||
|
|
||||||
:content
|
:content
|
||||||
@ -47,8 +47,12 @@ You will encounter `DValue` throughout the runtime, especially in:
|
|||||||
- `.to_bool(default)` performs best-effort boolean conversion.
|
- `.to_bool(default)` performs best-effort boolean conversion.
|
||||||
- `.to_stringmap()` converts a map-shaped tree into `StringMap`.
|
- `.to_stringmap()` converts a map-shaped tree into `StringMap`.
|
||||||
|
|
||||||
All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`2_DValue_to_string`, `2_DValue_to_s64`, `2_DValue_to_u64`, `2_DValue_to_f64`, `2_DValue_to_bool`) for the exact rules:
|
All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`to_string`, `to_s64`, `to_u64`, `to_f64`, `to_bool`) for the exact rules:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
String title = context.props["title"].to_string("Untitled");
|
||||||
|
s64 page_size = context.cfg.get_by_path("app/page_size").to_s64(25);
|
||||||
|
```
|
||||||
|
|
||||||
`operator[]` creates missing entries, just like `std::map`. `.has()` and `.key()` are the non-mutating lookup helpers, and `.get_by_path()` is the non-creating traversal helper.
|
`operator[]` creates missing entries, just like `std::map`. `.has()` and `.key()` are the non-mutating lookup helpers, and `.get_by_path()` is the non-creating traversal helper.
|
||||||
|
|
||||||
@ -96,7 +100,7 @@ Useful inspection helpers include:
|
|||||||
|
|
||||||
## each()
|
## each()
|
||||||
|
|
||||||
`each(std::function<void (const DValue& t, String key)> f)` iterates over the current tree value (see `2_DValue_each` for details).
|
`each(std::function<void (const DValue& t, String key)> f)` iterates over the current tree value (see the `each` page for details).
|
||||||
|
|
||||||
For map-shaped `DValue` values, the callback runs once per child entry and receives:
|
For map-shaped `DValue` values, the callback runs once per child entry and receives:
|
||||||
|
|
||||||
@ -108,12 +112,31 @@ For non-map values, `each()` still invokes the callback once:
|
|||||||
- `t` is the current value
|
- `t` is the current value
|
||||||
- `key` is an empty string
|
- `key` is an empty string
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
context.connection["items"].each([&](const DValue& item, String key) {
|
||||||
|
print(key, ": ", item.to_string(), "\n");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
String theme = context.cfg.get_by_path("theme/key").to_string();
|
||||||
|
|
||||||
:example
|
u64 compiled_mtime = unit_info("test/hello.uce")["compiled_mtime"].to_u64();
|
||||||
DValue user;
|
|
||||||
user["name"] = "Ada";
|
bool dark_mode = context.props["dark_mode"].to_bool();
|
||||||
user["active"] = true;
|
|
||||||
print(user["name"].to_string(), "\n");
|
if(DValue* user = payload.key("user")) {
|
||||||
|
print(user->to_json());
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.get_or_create("headers")->get_or_create("Content-Type")->set("text/plain");
|
||||||
|
|
||||||
|
StringMap headers = payload["headers"].to_stringmap();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Concepts
|
||||||
|
|
||||||
|
- PHP: nested associative arrays, `stdClass`, decoded JSON trees, and helper accessors for deep array paths
|
||||||
|
- JavaScript / Node.js: plain objects, arrays, `Map`, and JSON-shaped data passed between handlers
|
||||||
|
|||||||
@ -11,7 +11,7 @@ request_script_url
|
|||||||
request_base_url
|
request_base_url
|
||||||
request_query_path
|
request_query_path
|
||||||
request_query_route
|
request_query_route
|
||||||
2_Request_set_status
|
set_status
|
||||||
component
|
component
|
||||||
component_render
|
component_render
|
||||||
unit_render
|
unit_render
|
||||||
@ -25,12 +25,19 @@ ws_connection_id
|
|||||||
ws_connections
|
ws_connections
|
||||||
ws_send
|
ws_send
|
||||||
0_DValue
|
0_DValue
|
||||||
0_StringMap
|
StringMap
|
||||||
UploadedFile
|
UploadedFile
|
||||||
|
|
||||||
:content
|
:content
|
||||||
`Request& context` is the request-local state object passed into every UCE handler:
|
`Request& context` is the request-local state object passed into every UCE handler:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
RENDER(Request& context) { ... }
|
||||||
|
COMPONENT(Request& context) { ... }
|
||||||
|
WS(Request& context) { ... }
|
||||||
|
CLI(Request& context) { ... }
|
||||||
|
SERVE_HTTP(Request* req) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
It is the main bridge between the runtime and page code. It contains incoming request data, response state, output buffers, per-request scratch trees, session/cookie state, WebSocket metadata, and runtime diagnostics.
|
It is the main bridge between the runtime and page code. It contains incoming request data, response state, output buffers, per-request scratch trees, session/cookie state, WebSocket metadata, and runtime diagnostics.
|
||||||
|
|
||||||
@ -81,6 +88,9 @@ Type: `StringMap`
|
|||||||
|
|
||||||
Parsed query-string key/value parameters. This is populated from `context.params["QUERY_STRING"]` with `parse_query()`.
|
Parsed query-string key/value parameters. This is populated from `context.params["QUERY_STRING"]` with `parse_query()`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
String theme = first(context.get["theme"], "default");
|
||||||
|
```
|
||||||
|
|
||||||
For front-controller route URLs such as `/?dashboard&theme=dark`, the keyless `dashboard` segment is represented by sanitized `ROUTE_PATH`; named parameters such as `theme=dark` are available in `context.get`.
|
For front-controller route URLs such as `/?dashboard&theme=dark`, the keyless `dashboard` segment is represented by sanitized `ROUTE_PATH`; named parameters such as `theme=dark` are available in `context.get`.
|
||||||
|
|
||||||
@ -90,6 +100,10 @@ Type: `StringMap`
|
|||||||
|
|
||||||
Parsed request body parameters for ordinary `POST` requests. URL-encoded bodies are parsed with `parse_query()`. Multipart form data is parsed with `parse_multipart()` and uploaded files are listed in `context.uploaded_files`.
|
Parsed request body parameters for ordinary `POST` requests. URL-encoded bodies are parsed with `parse_query()`. Multipart form data is parsed with `parse_multipart()` and uploaded files are listed in `context.uploaded_files`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
if(context.params["REQUEST_METHOD"] == "POST")
|
||||||
|
String email = context.post["email"];
|
||||||
|
```
|
||||||
|
|
||||||
### `context.cookies`
|
### `context.cookies`
|
||||||
|
|
||||||
@ -105,6 +119,9 @@ Raw request body. For WebSocket handlers, this is the current message payload.
|
|||||||
|
|
||||||
Use this for JSON APIs:
|
Use this for JSON APIs:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
DValue body = json_decode(context.in);
|
||||||
|
```
|
||||||
|
|
||||||
### `context.uploaded_files`
|
### `context.uploaded_files`
|
||||||
|
|
||||||
@ -126,6 +143,10 @@ Type: `StringMap`
|
|||||||
|
|
||||||
Session data loaded by `session_start()`. UCE does not load sessions automatically for every request; call `session_start()` before reading/writing session data.
|
Session data loaded by `session_start()`. UCE does not load sessions automatically for every request; call `session_start()` before reading/writing session data.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
session_start();
|
||||||
|
context.session["user_id"] = "42";
|
||||||
|
```
|
||||||
|
|
||||||
At the end of a successful request, modified session data is saved automatically if a session is active.
|
At the end of a successful request, modified session data is saved automatically if a session is active.
|
||||||
|
|
||||||
@ -150,6 +171,11 @@ General request-local scratch/configuration tree. It is shared by the page, comp
|
|||||||
|
|
||||||
Examples from front-controller style apps:
|
Examples from front-controller style apps:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
context.call["route"] = request_query_route(context);
|
||||||
|
context.call["app"]["page_type"] = "html";
|
||||||
|
context.call["fragments"]["main"] = captured_html;
|
||||||
|
```
|
||||||
|
|
||||||
Prefer clear top-level names when state is app-wide (`route`, `fragments`) and nested app names only when the state is truly owned by that app (`app/page_title`, `app/page_type`).
|
Prefer clear top-level names when state is app-wide (`route`, `fragments`) and nested app names only when the state is truly owned by that app (`app/page_title`, `app/page_type`).
|
||||||
|
|
||||||
@ -159,11 +185,17 @@ Type: `DValue`
|
|||||||
|
|
||||||
Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup:
|
Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
context.cfg = get_config();
|
||||||
|
```
|
||||||
|
|
||||||
This is separate from `context.server->config`, which is the runtime/server string config from `/etc/uce/settings.cfg`.
|
This is separate from `context.server->config`, which is the runtime/server string config from `/etc/uce/settings.cfg`.
|
||||||
|
|
||||||
Use `get_by_path()` for non-mutating deep reads:
|
Use `get_by_path()` for non-mutating deep reads:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
String site_name = context.cfg.get_by_path("site/name").to_string();
|
||||||
|
```
|
||||||
|
|
||||||
### `context.props`
|
### `context.props`
|
||||||
|
|
||||||
@ -171,6 +203,11 @@ Type: `DValue`
|
|||||||
|
|
||||||
Invocation-local props for `component()`, `component_render()`, and macro-style `unit_call()` entrypoints. During a component call, the runtime temporarily replaces `context.props` with the props passed to that component and restores the previous value after the call returns.
|
Invocation-local props for `component()`, `component_render()`, and macro-style `unit_call()` entrypoints. During a component call, the runtime temporarily replaces `context.props` with the props passed to that component and restores the previous value after the call returns.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
DValue props;
|
||||||
|
props["title"] = "Dashboard";
|
||||||
|
print(component("components/card", props, context));
|
||||||
|
```
|
||||||
|
|
||||||
### `context.connection`
|
### `context.connection`
|
||||||
|
|
||||||
@ -178,6 +215,9 @@ Type: `DValue`
|
|||||||
|
|
||||||
WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket.
|
WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
context.connection["message_count"] = context.connection["message_count"].to_u64() + 1;
|
||||||
|
```
|
||||||
|
|
||||||
Only meaningful for WebSocket handlers.
|
Only meaningful for WebSocket handlers.
|
||||||
|
|
||||||
@ -195,6 +235,10 @@ Type: `StringMap`
|
|||||||
|
|
||||||
Response headers to emit. Header names are case-sensitive as written.
|
Response headers to emit. Header names are case-sensitive as written.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
context.header["Content-Type"] = "application/json";
|
||||||
|
context.header["Location"] = "/info/";
|
||||||
|
```
|
||||||
|
|
||||||
### `context.set_cookies`
|
### `context.set_cookies`
|
||||||
|
|
||||||
@ -206,6 +250,11 @@ Queued `Set-Cookie` header lines. Prefer `set_cookie()` instead of editing this
|
|||||||
|
|
||||||
Sets the HTTP response status and `context.flags.status`.
|
Sets the HTTP response status and `context.flags.status`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
context.set_status(404, "Not Found");
|
||||||
|
context.set_status(302, "Found");
|
||||||
|
context.header["Location"] = app_link("dashboard", context);
|
||||||
|
```
|
||||||
|
|
||||||
Related helpers:
|
Related helpers:
|
||||||
|
|
||||||
@ -227,6 +276,12 @@ Internal output-buffer stack. Most code should use helpers instead of touching t
|
|||||||
|
|
||||||
Common capture pattern:
|
Common capture pattern:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ob_start();
|
||||||
|
print(component("views/dashboard", context));
|
||||||
|
String html = ob_get_close();
|
||||||
|
context.call["fragments"]["main"] = html;
|
||||||
|
```
|
||||||
|
|
||||||
### `context.out` and `context.err`
|
### `context.out` and `context.err`
|
||||||
|
|
||||||
@ -303,6 +358,9 @@ Use `context.connection` for per-socket structured state.
|
|||||||
|
|
||||||
Pointer to server state. Useful mainly for low-level/runtime code. Runtime config lives at:
|
Pointer to server state. Useful mainly for low-level/runtime code. Runtime config lives at:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
context.server->config["KEY"]
|
||||||
|
```
|
||||||
|
|
||||||
This is a `StringMap`, separate from app-owned `context.cfg`.
|
This is a `StringMap`, separate from app-owned `context.cfg`.
|
||||||
|
|
||||||
@ -323,21 +381,57 @@ Notable fields:
|
|||||||
|
|
||||||
### Minimal page
|
### Minimal page
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
RENDER(Request& context)
|
||||||
|
{
|
||||||
|
<><h1>Hello <?= context.get["name"] ?></h1></>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### JSON endpoint
|
### JSON endpoint
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
RENDER(Request& context)
|
||||||
|
{
|
||||||
|
context.header["Content-Type"] = "application/json";
|
||||||
|
DValue response;
|
||||||
|
response["ok"].set_bool(true);
|
||||||
|
print(json_encode(response));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Redirect
|
### Redirect
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
RENDER(Request& context)
|
||||||
|
{
|
||||||
|
context.set_status(302, "Found");
|
||||||
|
context.header["Location"] = "/info/";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Component props
|
### Component props
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
DValue props;
|
||||||
|
props["title"] = "Welcome";
|
||||||
|
print(component("components/card", props, context));
|
||||||
|
```
|
||||||
|
|
||||||
### Front-controller route
|
### Front-controller route
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
context.call["route"] = request_query_route(context);
|
||||||
|
String route_path = context.call["route"]["l_path"].to_string();
|
||||||
|
```
|
||||||
|
|
||||||
Or use runtime-populated params directly:
|
Or use runtime-populated params directly:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
String route_path = context.params["ROUTE_PATH"];
|
||||||
|
```
|
||||||
|
|
||||||
:example
|
## Related Concepts
|
||||||
print(context.params["REQUEST_URI"] != "" ? "request available\n" : "request available\n");
|
|
||||||
|
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, output buffering, and `http_response_code()`
|
||||||
|
- JavaScript / Node.js: Express `req`/`res`, Fetch `Request`/`Response`, route params, middleware-populated locals, and per-socket WebSocket state
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
:sig
|
|
||||||
struct StringList — ordered list of String values
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : use methods such as `filter()`, `map()`, `keys()`, `each()`, `unique()`, `sort()`, `some()`, `every()`, and `find()` to derive or inspect lists
|
|
||||||
|
|
||||||
:content
|
|
||||||
`StringList` is an ordered container of `String` values. It inherits normal `std::vector<String>` behavior and adds list helpers for filtering, mapping, indexes, iteration, deduplication, sorting, and predicate checks.
|
|
||||||
|
|
||||||
:example
|
|
||||||
StringList labels = split("a,b,a", ",").unique().map([](String item) { return(to_upper(item)); });
|
|
||||||
print(join(labels, ","), "\n");
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_String
|
|
||||||
split
|
|
||||||
join
|
|
||||||
2_StringList_filter
|
|
||||||
2_StringList_map
|
|
||||||
2_StringList_keys
|
|
||||||
2_StringList_each
|
|
||||||
2_StringList_unique
|
|
||||||
2_StringList_sort
|
|
||||||
2_StringList_some
|
|
||||||
2_StringList_every
|
|
||||||
2_StringList_find
|
|
||||||
@ -5,7 +5,6 @@ CLI
|
|||||||
CLI(Request& context)
|
CLI(Request& context)
|
||||||
|
|
||||||
:see
|
:see
|
||||||
>ob
|
|
||||||
>1_RENDER
|
>1_RENDER
|
||||||
>1_COMPONENT
|
>1_COMPONENT
|
||||||
>1_WS
|
>1_WS
|
||||||
@ -24,9 +23,17 @@ The default CLI socket path is `/run/uce/cli.sock` and is configured with `CLI_S
|
|||||||
|
|
||||||
Example convenience script usage:
|
Example convenience script usage:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/uce-cli /tests/cli.uce
|
||||||
|
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||||
|
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
|
||||||
|
```
|
||||||
|
|
||||||
Equivalent curl probe:
|
Equivalent curl probe:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --unix-socket /run/uce/cli.sock http://localhost/tests/cli.uce
|
||||||
|
```
|
||||||
|
|
||||||
For structured commands, prefer JSON POST bodies. The `scripts/uce-cli` helper sends `key=value` parameters as JSON POST by default, while still allowing `--get` for simple query-string probes.
|
For structured commands, prefer JSON POST bodies. The `scripts/uce-cli` helper sends `key=value` parameters as JSON POST by default, while still allowing `--get` for simple query-string probes.
|
||||||
|
|
||||||
@ -41,8 +48,20 @@ CLI responses default to `text/plain; charset=utf-8`, but the handler may set he
|
|||||||
|
|
||||||
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DValue`.
|
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DValue`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
CLI(Request& context)
|
||||||
|
{
|
||||||
|
DValue input = cli_input(context);
|
||||||
|
String action = first(input["action"].to_string(), "ping");
|
||||||
|
if(action == "ping")
|
||||||
|
{
|
||||||
|
print("ok\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.set_status(400, "Bad Command");
|
||||||
|
print("unknown action: ", action, "\n");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
`ONCE(Request& context)` runs before `CLI()` in the same way it runs before render and component entrypoints. `INIT(Request& context)` runs when the unit is loaded into a worker.
|
`ONCE(Request& context)` runs before `CLI()` in the same way it runs before render and component entrypoints. `INIT(Request& context)` runs when the unit is loaded into a worker.
|
||||||
|
|
||||||
:example
|
|
||||||
print("CLI is a unit directive; place it at top level in a .uce unit.\n");
|
|
||||||
|
|||||||
@ -37,6 +37,19 @@ When you call `component(":NAME", props, context)` or `component_render(":NAME",
|
|||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
COMPONENT(Request& context)
|
||||||
|
{
|
||||||
|
<><section><?: component(":BODY", context.props, context) ?></section></>
|
||||||
|
}
|
||||||
|
|
||||||
:example
|
COMPONENT:BODY(Request& context)
|
||||||
print("COMPONENT is a unit directive; place it at top level in a .uce unit.\n");
|
{
|
||||||
|
<><p><?= context.props["body"] ?></p></>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Concepts
|
||||||
|
|
||||||
|
- PHP: view partials, reusable include files, small template helpers, and server-side component-like rendering patterns
|
||||||
|
- JavaScript / Node.js: reusable component functions, React or Vue components, and server-rendered partials
|
||||||
|
|||||||
@ -27,6 +27,24 @@ Because UCE usually loads units on demand during a request, `INIT()` still recei
|
|||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::map<String, String> cached_labels;
|
||||||
|
|
||||||
:example
|
INIT(Request& context)
|
||||||
print("INIT is a unit directive; place it at top level in a .uce unit.\n");
|
{
|
||||||
|
if(cached_labels.empty())
|
||||||
|
cached_labels["ready"] = "Ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
COMPONENT(Request& context)
|
||||||
|
{
|
||||||
|
<>
|
||||||
|
<p><?= cached_labels["ready"] ?></p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Concepts
|
||||||
|
|
||||||
|
- PHP: opcode-cache preload or one-time bootstrap work per worker process
|
||||||
|
- JavaScript / Node.js: module-load initialization or lazy singleton setup
|
||||||
|
|||||||
@ -27,6 +27,23 @@ When a request first enters a given file through `RENDER(Request& context)`, `CO
|
|||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ONCE(Request& context)
|
||||||
|
{
|
||||||
|
context.call["card_defaults"]["tone"] = "info";
|
||||||
|
}
|
||||||
|
|
||||||
:example
|
COMPONENT(Request& context)
|
||||||
print("ONCE is a unit directive; place it at top level in a .uce unit.\n");
|
{
|
||||||
|
<>
|
||||||
|
<div class="card card-<?= context.call["card_defaults"]["tone"] ?>">
|
||||||
|
<?: component(":BODY", context.props, context) ?>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Concepts
|
||||||
|
|
||||||
|
- PHP: per-request bootstrap work before a template or partial first runs
|
||||||
|
- JavaScript / Node.js: request-scoped lazy initialization before a route or component render
|
||||||
|
|||||||
@ -40,5 +40,7 @@ In that case:
|
|||||||
- `WS(Request& context)` handles later WebSocket messages
|
- `WS(Request& context)` handles later WebSocket messages
|
||||||
- `COMPONENT()` remains available only through the component helpers
|
- `COMPONENT()` remains available only through the component helpers
|
||||||
|
|
||||||
:example
|
## Related Concepts
|
||||||
print("RENDER is a unit directive; place it at top level in a .uce unit.\n");
|
|
||||||
|
- PHP: front controller entrypoints, template files, `include`, `require`, and route handlers that write the HTTP response
|
||||||
|
- JavaScript / Node.js: Express or Fastify route handlers, page controller functions, and SSR entrypoints that build a response
|
||||||
|
|||||||
@ -40,5 +40,7 @@ The current message metadata is available as:
|
|||||||
|
|
||||||
Helper wrappers such as `ws_message()`, `ws_connection_id()`, `ws_scope()`, `ws_connection_count()`, `ws_opcode()`, and `ws_is_binary()` are still available when that reads better for the handler.
|
Helper wrappers such as `ws_message()`, `ws_connection_id()`, `ws_scope()`, `ws_connection_count()`, `ws_opcode()`, and `ws_is_binary()` are still available when that reads better for the handler.
|
||||||
|
|
||||||
:example
|
## Related Concepts
|
||||||
print("WS is a unit directive; place it at top level in a .uce unit.\n");
|
|
||||||
|
- PHP: Ratchet `onMessage`, Workerman WebSocket handlers, or lower-level callbacks around accepted socket connections
|
||||||
|
- JavaScript / Node.js: browser `WebSocket` `message` handlers and Node `ws` server `connection` and `message` callbacks
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
:title
|
|
||||||
DValue::operator=
|
|
||||||
|
|
||||||
:sig
|
|
||||||
void DValue::operator=(String v)
|
|
||||||
void DValue::operator=(f64 v)
|
|
||||||
void DValue::operator=(void* v)
|
|
||||||
void DValue::operator=(DValue v)
|
|
||||||
void DValue::operator=(StringMap v)
|
|
||||||
|
|
||||||
:params
|
|
||||||
v : assigned value
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_set
|
|
||||||
2_DValue_set_bool
|
|
||||||
|
|
||||||
:content
|
|
||||||
Assignment overloads forward to the matching `set(...)` overloads. They are the normal concise way to write string, number, pointer, `DValue`, and `StringMap` values. Boolean values should use `set_bool()` to avoid overload ambiguity.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue row;
|
|
||||||
row["name"] = "Ada";
|
|
||||||
row["role"] = "engineer";
|
|
||||||
print(row["name"].to_string(), " / ", row["role"].to_string(), "\n");
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
:sig
|
|
||||||
void DValue::clear()
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_set_array
|
|
||||||
|
|
||||||
:content
|
|
||||||
Clears the value into an empty map-shaped value and resets its array index. Unlike `set_array()`, this does not set list mode.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue u;
|
|
||||||
u["a"] = "1"; u["b"] = "2";
|
|
||||||
u.clear();
|
|
||||||
print("after clear, has a: ", u.has("a") ? "yes" : "no", "\n");
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
:sig
|
|
||||||
DValue& DValue::deref()
|
|
||||||
const DValue& DValue::deref() const
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : referenced target when resolvable, otherwise this value
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_is_reference
|
|
||||||
|
|
||||||
:content
|
|
||||||
Returns the value after following internal reference links. Most public accessors call this for you, so direct use is rare in unit code.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue target;
|
|
||||||
target.set("value");
|
|
||||||
DValue ref;
|
|
||||||
ref.set_reference(&target);
|
|
||||||
print("through ref: ", ref.deref().to_string(), "\n");
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
:sig
|
|
||||||
void DValue::each(std::function<void (const DValue& item, String key)> f) const
|
|
||||||
|
|
||||||
:params
|
|
||||||
f : callback invoked for each child, or once for a scalar value
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_map
|
|
||||||
2_DValue_filter
|
|
||||||
2_DValue_is_list
|
|
||||||
|
|
||||||
:content
|
|
||||||
Iterates a `DValue` without modifying it. References are dereferenced before iteration.
|
|
||||||
|
|
||||||
For map-shaped values, the callback receives each child and its key. List-shaped maps iterate in numeric index order (`0`, `1`, `2`, ...); ordinary maps iterate in `std::map` string-key order. Scalar values invoke the callback once with the current value and an empty key.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue user;
|
|
||||||
user["name"] = "Ada";
|
|
||||||
user["role"] = "admin";
|
|
||||||
StringList keys;
|
|
||||||
user.each([&](const DValue& item, String key) { keys.push_back(key); });
|
|
||||||
print(join(keys.sort(), ","), "\n");
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
:sig
|
|
||||||
DValue DValue::filter(StringList keys) const
|
|
||||||
DValue DValue::filter(std::function<bool (const DValue& item, String key)> f) const
|
|
||||||
|
|
||||||
:params
|
|
||||||
keys : child keys to copy when using the key-list overload
|
|
||||||
f : predicate returning true for children to keep
|
|
||||||
return value : filtered copy
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_map
|
|
||||||
2_DValue_keys
|
|
||||||
|
|
||||||
:content
|
|
||||||
`filter(keys)` copies existing children with the named keys into a new map-shaped `DValue`.
|
|
||||||
|
|
||||||
`filter(f)` calls the predicate for each item from `each()`. Kept children from list-shaped input are pushed into a new list and re-indexed from zero; kept children from map-shaped input keep their original keys. Scalar input is passed to the predicate once and, if accepted, is pushed into a list.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue user;
|
|
||||||
user["name"] = "Ada";
|
|
||||||
user["email"] = "ada@example.test";
|
|
||||||
DValue public_user = user.filter({"name"});
|
|
||||||
print(public_user["name"].to_string(), "\n");
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
:sig
|
|
||||||
DValue DValue::get_by_path(String path, String delim = "/") const
|
|
||||||
|
|
||||||
:params
|
|
||||||
path : delimited child path to traverse
|
|
||||||
delim : path separator string
|
|
||||||
return value : resolved child copy, or an empty DValue when traversal fails
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_has
|
|
||||||
2_DValue_to_string
|
|
||||||
|
|
||||||
:content
|
|
||||||
Traverses nested map/list children without creating missing nodes. Empty path segments are ignored. If any segment is missing or an intermediate value is not map-shaped, the method returns an empty `DValue`.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue cfg;
|
|
||||||
cfg["site"]["title"] = "UCE";
|
|
||||||
print(cfg.get_by_path("site/title").to_string(), "\n");
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
:sig
|
|
||||||
DValue* DValue::get_or_create(String s)
|
|
||||||
|
|
||||||
:params
|
|
||||||
s : child key to create or return
|
|
||||||
return value : pointer to the child node
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_key
|
|
||||||
2_DValue_has
|
|
||||||
|
|
||||||
:content
|
|
||||||
Ensures the value is map-shaped and returns the named child. If the key does not exist, an empty child is created. Creating a non-numeric key on a list-shaped value clears list mode.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue config;
|
|
||||||
config.get_or_create("host")->set("localhost");
|
|
||||||
print(config.has("host") ? config["host"].to_string() : "missing", "\n");
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
:sig
|
|
||||||
String DValue::get_type_name() const
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : one of String, f64, bool, array, pointer, reference, or unknown
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
|
|
||||||
:content
|
|
||||||
Returns a human-readable name for the dereferenced value's type tag.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue s; s.set("text");
|
|
||||||
DValue m; m["k"] = "v";
|
|
||||||
print(s.get_type_name(), " / ", m.get_type_name(), "\n");
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
:sig
|
|
||||||
bool DValue::has(String s) const
|
|
||||||
|
|
||||||
:params
|
|
||||||
s : child key to test
|
|
||||||
return value : true when the dereferenced value is map-shaped and contains the key
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_key
|
|
||||||
2_DValue_get_by_path
|
|
||||||
|
|
||||||
:content
|
|
||||||
Checks for a child key without creating it. Returns false for scalars and missing keys.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue u;
|
|
||||||
u["name"] = "Ada";
|
|
||||||
print(u.has("name") ? "has name" : "no name", " / ", u.has("age") ? "has age" : "no age", "\n");
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
:sig
|
|
||||||
bool DValue::is_array() const
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : true when the dereferenced value is map-shaped
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_is_list
|
|
||||||
|
|
||||||
:content
|
|
||||||
Returns true when the value's current type is `M`, the map/list container type. It does not require contiguous numeric keys; use `is_list()` for that.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue user;
|
|
||||||
user["name"] = "Ada";
|
|
||||||
print(user.is_array() ? "array\n" : "scalar\n");
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
:sig
|
|
||||||
bool DValue::is_list() const
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : true when the value is an array with keys 0..n-1
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_set_array
|
|
||||||
2_DValue_push
|
|
||||||
|
|
||||||
:content
|
|
||||||
Returns true for map-shaped values that represent a list. An empty map is a list only if it was explicitly put in list mode with `set_array()` or by list operations. Non-empty lists must have canonical numeric string keys from `0` through `size - 1`.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue list;
|
|
||||||
DValue item; item = "Ada";
|
|
||||||
list.push(item);
|
|
||||||
print(list.is_list() ? "list\n" : "map\n");
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
:sig
|
|
||||||
bool DValue::is_reference() const
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : true when this node itself is a reference node
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_reference_target
|
|
||||||
2_DValue_deref
|
|
||||||
|
|
||||||
:content
|
|
||||||
Reports whether the current node's direct type tag is the internal reference tag. Most read methods dereference automatically; this helper is mainly useful when handling reference-aware runtime state.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue target; target.set("x");
|
|
||||||
DValue ref; ref.set_reference(&target);
|
|
||||||
DValue plain; plain.set("y");
|
|
||||||
print(ref.is_reference() ? "ref" : "plain", " / ", plain.is_reference() ? "ref" : "plain", "\n");
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
:sig
|
|
||||||
DValue* DValue::key(String s)
|
|
||||||
const DValue* DValue::key(String s) const
|
|
||||||
|
|
||||||
:params
|
|
||||||
s : child key to find
|
|
||||||
return value : pointer to existing child, or 0
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_has
|
|
||||||
2_DValue_get_or_create
|
|
||||||
|
|
||||||
:content
|
|
||||||
Looks up one child without creating it. The non-const overload forwards through references; both overloads return `0` for scalars or missing keys.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue user;
|
|
||||||
user["email"] = "ada@example.test";
|
|
||||||
const DValue* found = user.key("email");
|
|
||||||
const DValue* missing = user.key("phone");
|
|
||||||
print(found ? found->to_string() : "none", " / ", missing ? "present" : "absent", "\n");
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
:sig
|
|
||||||
StringList DValue::keys() const
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : child keys for map/list values; empty for scalars
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_each
|
|
||||||
0_StringList
|
|
||||||
|
|
||||||
:content
|
|
||||||
Returns the keys produced by `each()`. Scalar values have no child key, so the result is empty. List-shaped values return their numeric keys as strings.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue user;
|
|
||||||
user["name"] = "Ada";
|
|
||||||
user["role"] = "admin";
|
|
||||||
print(join(user.keys().sort(), ","), "\n");
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
:sig
|
|
||||||
DValue DValue::map(std::function<DValue (const DValue& item, String key)> f) const
|
|
||||||
|
|
||||||
:params
|
|
||||||
f : mapper called for each iterated item
|
|
||||||
return value : mapped copy
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_each
|
|
||||||
2_DValue_filter
|
|
||||||
|
|
||||||
:content
|
|
||||||
Builds a new `DValue` by replacing each iterated child with the mapper's returned value. List-shaped input stays list-shaped and is re-indexed by `push()`. Map-shaped input preserves keys. Scalar input is mapped once and pushed into a list.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue names;
|
|
||||||
DValue first; first = "ada";
|
|
||||||
DValue second; second = "grace";
|
|
||||||
names.push(first);
|
|
||||||
names.push(second);
|
|
||||||
DValue upper = names.map([](const DValue& item, String key) { DValue out; out = to_upper(item.to_string()); return(out); });
|
|
||||||
print(upper["0"].to_string(), ",", upper["1"].to_string(), "\n");
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
:title
|
|
||||||
DValue::operator[]
|
|
||||||
|
|
||||||
:sig
|
|
||||||
DValue& DValue::operator[](String s)
|
|
||||||
|
|
||||||
:params
|
|
||||||
s : child key to return or create
|
|
||||||
return value : reference to the child node
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_get_or_create
|
|
||||||
2_DValue_key
|
|
||||||
2_DValue_has
|
|
||||||
|
|
||||||
:content
|
|
||||||
Returns a mutable reference to a child, creating the child when it does not already exist. If this value is an internal reference, the operation is forwarded to the target. This has the same creation behavior as `get_or_create()`, so use `has()`, `key()`, or `get_by_path()` for non-mutating reads.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue m;
|
|
||||||
m["a"]["b"] = "nested";
|
|
||||||
print(m["a"]["b"].to_string(), "\n");
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
:sig
|
|
||||||
DValue DValue::pop()
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : removed child, or an empty DValue when no child exists
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_push
|
|
||||||
|
|
||||||
:content
|
|
||||||
Ensures the value is map-shaped, removes the last entry according to `std::map` reverse key order, and returns it. For list-mode values, the next array index is reset to the new size.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue list;
|
|
||||||
list.set_array();
|
|
||||||
DValue a; a.set("first"); list.push(a);
|
|
||||||
DValue b; b.set("last"); list.push(b);
|
|
||||||
DValue popped = list.pop();
|
|
||||||
print("popped ", popped.to_string(), ", remaining ", list.keys().size(), "\n");
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
:sig
|
|
||||||
void DValue::push(const DValue& child)
|
|
||||||
|
|
||||||
:params
|
|
||||||
child : value to append/copy
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_pop
|
|
||||||
2_DValue_set_array
|
|
||||||
|
|
||||||
:content
|
|
||||||
Appends a child under the next numeric key. Empty values become list-shaped. Existing contiguous lists continue at their size; non-list maps use the next unused numeric key and remain non-list.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue list;
|
|
||||||
list.set_array();
|
|
||||||
DValue a; a.set("x"); list.push(a);
|
|
||||||
DValue b; b.set("y"); list.push(b);
|
|
||||||
print("count: ", list.keys().size(), "\n");
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
:sig
|
|
||||||
DValue* DValue::reference_target()
|
|
||||||
const DValue* DValue::reference_target() const
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : resolved target pointer, or 0 when this node is not a usable reference
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_is_reference
|
|
||||||
2_DValue_deref
|
|
||||||
|
|
||||||
:content
|
|
||||||
If this node is an internal reference, follows reference links up to the runtime safety limit and returns the final non-reference target. Returns `0` for non-references, null/self references, or chains that still resolve to a reference.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue target;
|
|
||||||
target.set("data");
|
|
||||||
DValue ref;
|
|
||||||
ref.set_reference(&target);
|
|
||||||
print(ref.reference_target() == &target ? "points to target" : "elsewhere", "\n");
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
:sig
|
|
||||||
void DValue::remove(String s)
|
|
||||||
|
|
||||||
:params
|
|
||||||
s : child key to erase
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_clear
|
|
||||||
2_DValue_has
|
|
||||||
|
|
||||||
:content
|
|
||||||
Ensures the value is map-shaped and erases the named child. Removing the last child resets the next array index to zero.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue u;
|
|
||||||
u["a"] = "1"; u["b"] = "2";
|
|
||||||
u.remove("a");
|
|
||||||
print("has a: ", u.has("a") ? "yes" : "no", ", has b: ", u.has("b") ? "yes" : "no", "\n");
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
:sig
|
|
||||||
void DValue::set(String s)
|
|
||||||
void DValue::set(void* p)
|
|
||||||
void DValue::set(f64 f)
|
|
||||||
void DValue::set(DValue source)
|
|
||||||
void DValue::set(StringMap source)
|
|
||||||
|
|
||||||
:params
|
|
||||||
s : string value for the String overload
|
|
||||||
p : pointer value for the pointer overload
|
|
||||||
f : floating point value for the f64 overload
|
|
||||||
source : DValue or StringMap to copy
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_set_bool
|
|
||||||
2_DValue_set_array
|
|
||||||
|
|
||||||
:content
|
|
||||||
Assigns a new value, forwarding through references when possible. String, pointer, and f64 overloads set scalar nodes. `set(DValue)` copies the source's current type and payload. `set(StringMap)` creates a map-shaped value with each string entry as a child.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue v;
|
|
||||||
v.set("hello");
|
|
||||||
print(v.to_string(), "\n");
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
:sig
|
|
||||||
void DValue::set_array()
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_push
|
|
||||||
2_DValue_is_list
|
|
||||||
|
|
||||||
:content
|
|
||||||
Clears the value and makes it an empty list-shaped map. Subsequent `push()` calls append numeric keys starting at `0`.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue list;
|
|
||||||
list.set_array();
|
|
||||||
DValue item; item.set("first");
|
|
||||||
list.push(item);
|
|
||||||
print("is_list=", list.is_list() ? "yes" : "no", " count=", list.keys().size(), "\n");
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
:sig
|
|
||||||
void DValue::set_bool(bool b)
|
|
||||||
|
|
||||||
:params
|
|
||||||
b : boolean value to store
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_to_bool
|
|
||||||
|
|
||||||
:content
|
|
||||||
Stores a boolean value, forwarding through references when possible.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue flag;
|
|
||||||
flag.set_bool(true);
|
|
||||||
print(flag.to_bool() ? "true" : "false", "\n");
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
:sig
|
|
||||||
void DValue::set_reference(DValue* target)
|
|
||||||
|
|
||||||
:params
|
|
||||||
target : target node pointer
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_reference_target
|
|
||||||
|
|
||||||
:content
|
|
||||||
Stores an internal reference to another `DValue`. Most unit code should not need to create references directly; normal reads and writes generally follow existing references automatically.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue target;
|
|
||||||
target.set("original");
|
|
||||||
DValue ref;
|
|
||||||
ref.set_reference(&target);
|
|
||||||
print(ref.deref().to_string(), "\n");
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
:sig
|
|
||||||
void DValue::set_type(char t)
|
|
||||||
|
|
||||||
:params
|
|
||||||
t : internal type tag such as S, F, B, M, P, or R
|
|
||||||
return value : none
|
|
||||||
|
|
||||||
:see
|
|
||||||
0_DValue
|
|
||||||
2_DValue_set
|
|
||||||
2_DValue_set_array
|
|
||||||
|
|
||||||
:content
|
|
||||||
Changes the node's internal type tag, forwarding through references when possible. Switching to map type clears children, resets list state, and resets the array index.
|
|
||||||
|
|
||||||
Prefer the typed setters (`set`, `set_bool`, `set_array`) in normal unit code.
|
|
||||||
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue v;
|
|
||||||
v.set_array();
|
|
||||||
DValue item; item.set("x"); v.push(item);
|
|
||||||
print("type after set_array: ", v.get_type_name(), "\n");
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
:sig
|
|
||||||
bool DValue::to_bool(bool default_value = false) const
|
|
||||||
|
|
||||||
:params
|
|
||||||
bool : fallback returned when conversion is not possible
|
|
||||||
return value : converted value or the fallback
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_get_by_path
|
|
||||||
|
|
||||||
:content
|
|
||||||
Converts strings, numbers, bools, pointers, and maps to bool. Text such as `true`, `yes`, `on`, `1`, `false`, `no`, `off`, `0`, and `null` is recognized; non-empty unparseable strings are truthy; empty strings use the default. Multi-child maps are true when non-empty.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue value;
|
|
||||||
value = "true";
|
|
||||||
print(value.to_bool() ? "true\n" : "false\n");
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
:sig
|
|
||||||
f64 DValue::to_f64(f64 default_value = 0) const
|
|
||||||
|
|
||||||
:params
|
|
||||||
f64 : fallback returned when conversion is not possible
|
|
||||||
return value : converted value or the fallback
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_get_by_path
|
|
||||||
|
|
||||||
:content
|
|
||||||
Converts strings, f64, bool, pointer, and single-value maps to a finite double. Invalid or empty strings return the default.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue value;
|
|
||||||
value = "12.5";
|
|
||||||
print(value.to_f64(), "\n");
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
:sig
|
|
||||||
String DValue::to_json(char quote_char = '"') const
|
|
||||||
|
|
||||||
:params
|
|
||||||
quote_char : quote character passed to string escaping
|
|
||||||
return value : JSON-ish scalar representation
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
json_encode
|
|
||||||
|
|
||||||
:content
|
|
||||||
Returns the JSON scalar representation used by this low-level method: strings are escaped and quoted, numbers are emitted with `std::to_string`, bools as `true`/`false`, maps as `"(array)"`, pointers as `"(pointer)"`, and references as `"(reference)"`. For full structured encoding, use the runtime JSON helpers.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue value;
|
|
||||||
value = "Ada";
|
|
||||||
print(value.to_json(), "\n");
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
:sig
|
|
||||||
s64 DValue::to_s64(s64 default_value = 0) const
|
|
||||||
|
|
||||||
:params
|
|
||||||
s64 : fallback returned when conversion is not possible
|
|
||||||
return value : converted value or the fallback
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_get_by_path
|
|
||||||
|
|
||||||
:content
|
|
||||||
Converts strings, f64, bool, pointer, and single-value maps to a signed integer. Invalid or empty strings return the default. Values outside the signed range are clamped.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue value;
|
|
||||||
value = "-42";
|
|
||||||
print(value.to_s64(), "\n");
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
:sig
|
|
||||||
String DValue::to_string(String default_value = "") const
|
|
||||||
|
|
||||||
:params
|
|
||||||
String : fallback returned when conversion is not possible
|
|
||||||
return value : converted value or the fallback
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_get_by_path
|
|
||||||
|
|
||||||
:content
|
|
||||||
Returns the stored string, converts f64/bool/pointer values to text, and returns the default for empty strings, maps, unresolved references, or unknown types. Bool text is `(true)` or `(false)`.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue value;
|
|
||||||
value = true;
|
|
||||||
print(value.to_string(), "\n");
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
:sig
|
|
||||||
StringMap DValue::to_stringmap() const
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : StringMap copy of the value
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
0_StringMap
|
|
||||||
2_DValue_to_string
|
|
||||||
|
|
||||||
:content
|
|
||||||
Converts a `DValue` to `StringMap`. Map children become entries converted with `to_string()`. Non-empty strings become `value=<string>`. f64, bool, and pointer values become `value=<to_string()>`. References that cannot be resolved produce an empty map.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue user;
|
|
||||||
user["name"] = "Ada";
|
|
||||||
StringMap fields = user.to_stringmap();
|
|
||||||
print(fields["name"], "\n");
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
:sig
|
|
||||||
u64 DValue::to_u64(u64 default_value = 0) const
|
|
||||||
|
|
||||||
:params
|
|
||||||
u64 : fallback returned when conversion is not possible
|
|
||||||
return value : converted value or the fallback
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_get_by_path
|
|
||||||
|
|
||||||
:content
|
|
||||||
Converts strings, f64, bool, pointer, and single-value maps to an unsigned integer. Invalid or empty strings return the default. Negative values clamp to zero and oversized values clamp to `u64` max.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue value;
|
|
||||||
value = "42";
|
|
||||||
print(value.to_u64(), "\n");
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
:sig
|
|
||||||
DValue DValue::values() const
|
|
||||||
|
|
||||||
:params
|
|
||||||
return value : a list-shaped DValue containing each iterated child value
|
|
||||||
|
|
||||||
:see
|
|
||||||
>types
|
|
||||||
0_DValue
|
|
||||||
2_DValue_each
|
|
||||||
2_DValue_push
|
|
||||||
|
|
||||||
:content
|
|
||||||
Copies the values produced by `each()` into a new list-shaped `DValue`.
|
|
||||||
|
|
||||||
:example
|
|
||||||
DValue user;
|
|
||||||
user["name"] = "Ada";
|
|
||||||
user["role"] = "admin";
|
|
||||||
DValue values = user.values();
|
|
||||||
print(values.is_list() ? "list\n" : "map\n");
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user