Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34a97e2577 | ||
|
|
6be92ef93e | ||
|
|
5ee257565c | ||
|
|
5f430155b7 | ||
|
|
762b586242 | ||
|
|
1a5c6547b9 | ||
|
|
75bccb778c | ||
|
|
ad2ba7b632 | ||
|
|
abcb66717e | ||
|
|
1743c51e46 | ||
|
|
04745f39a8 | ||
|
|
f5637bb587 | ||
|
|
b1a0df9c93 | ||
|
|
4f84ac544d | ||
|
|
83ab9e10f7 | ||
|
|
560290ca1d | ||
|
|
bfd6d33829 | ||
|
|
cf51336873 | ||
|
|
fb728d63bc | ||
|
|
2debd33804 | ||
|
|
8587fbc5aa | ||
|
|
15e8d092bc | ||
|
|
6bb4f7f0ad | ||
|
|
b1856c1725 | ||
|
|
af6500d134 | ||
|
|
d6421cb8f3 | ||
|
|
fe83c52411 | ||
|
|
c84fc86e6c | ||
|
|
5a56d4f39e | ||
|
|
afaa4dd7c0 | ||
|
|
577aae076e | ||
|
|
eb8f303f94 | ||
|
|
89b5499c8f | ||
|
|
961df2d542 | ||
|
|
80285b7fb4 | ||
|
|
7e2faf1472 | ||
|
|
7066da3cde | ||
|
|
941f5aea08 | ||
|
|
b957a2373b | ||
|
|
20db669589 | ||
|
|
7f757654b6 | ||
|
|
71ddcaf7d4 | ||
|
|
8b37e7ea1e | ||
|
|
41e9ca219f | ||
|
|
0d8b74930c | ||
|
|
d37517041d | ||
|
|
02e153a6a7 | ||
|
|
9f7625c7fd | ||
|
|
cd445f3c9b | ||
|
|
223cf4c6e1 |
+14
@@ -36,4 +36,18 @@
|
||||
tmp/*
|
||||
bin/*
|
||||
pkg/*
|
||||
dist/*
|
||||
|
||||
# Python cache artifacts
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Editor swap artifacts
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
|
||||
## Current State
|
||||
|
||||
This is in the early stages of development. Don't use this for anything important (or at all)!
|
||||
This is in the early stages of development. Don't use this for anything important!
|
||||
|
||||
## Overview
|
||||
|
||||
UCE is a PHP-inspired server-side runtime that lets you build web pages and handlers in C++ using a small `.uce` preprocessor plus a FastCGI application server.
|
||||
|
||||
- `.uce` pages compile to shared objects on demand
|
||||
- `.uce` pages compile to WebAssembly side modules on demand
|
||||
- normal HTTP pages expose `RENDER(Request& context)`
|
||||
- WebSocket pages can additionally expose `WS(Request& context)`
|
||||
- sub-rendering and components pass structured data through `context.call`
|
||||
- nginx can forward normal `.uce` requests and ordinary `.ws.uce` page loads to the FastCGI socket, while real WebSocket upgrade requests for `.ws.uce` endpoints go to the built-in HTTP/WebSocket listener
|
||||
- the nginx-published application tree now lives under `site/`
|
||||
- local CLI/admin/test entrypoints can expose `CLI(Request& context)` and are invoked through the Unix CLI socket
|
||||
- sub-rendering and components pass structured data through `context.props`
|
||||
- nginx can forward normal `.uce` requests to the FastCGI socket, while WebSocket upgrade requests for `.uce` endpoints go to the built-in HTTP/WebSocket listener
|
||||
- the example application tree lives under `site/`; deployments should publish app files to a normal web root such as `/var/www/html`
|
||||
- you can include C++ code as much as you want, but only .uce files called via API functions and entry points will be pre-processed
|
||||
- the preprocessor has two jobs:
|
||||
- allow for inline HTML within C++ and the use of templating tags inside of that HTML
|
||||
@@ -28,7 +29,7 @@ RENDER(Request& context)
|
||||
}
|
||||
```
|
||||
|
||||
The runtime is still experimental.
|
||||
*The runtime is still experimental. This is not production-ready. Use at your own risk!*
|
||||
|
||||
## Build
|
||||
|
||||
@@ -42,7 +43,12 @@ The current build expects:
|
||||
|
||||
- `clang++`
|
||||
- `mysql_config`
|
||||
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
|
||||
- standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support
|
||||
- Wasmtime C API / C++ headers, defaulting to `/opt/wasmtime` or `WASMTIME_HOME`
|
||||
- WASI SDK tools, defaulting to `/opt/wasi-sdk` or `WASI_SDK`, for `scripts/build_core_wasm.sh` and unit compilation
|
||||
|
||||
SQLite and miniz are vendored under `src/3rdparty/`; no system SQLite or zlib package is required for those helpers.
|
||||
|
||||
The binary is written to:
|
||||
|
||||
@@ -52,28 +58,37 @@ bin/uce_fastcgi.linux.bin
|
||||
|
||||
## Runtime Model
|
||||
|
||||
UCE pages now use explicit request handlers instead of implicit globals:
|
||||
UCE pages use explicit request handlers instead of implicit globals:
|
||||
|
||||
- `RENDER(Request& context)` for normal HTTP rendering
|
||||
- `WS(Request& context)` for inbound WebSocket messages
|
||||
- `CLI(Request& context)` for local command-line/admin/test invocations through `CLI_SOCKET_PATH`
|
||||
|
||||
Useful related runtime patterns:
|
||||
|
||||
- `unit_render(String file_name)` or `unit_render(String file_name, Request& context)` to invoke another page
|
||||
- `context.cfg` for request-local structured configuration
|
||||
- `context.call` for invocation or message-local structured input
|
||||
- `context.props` for invocation-local structured input such as component props
|
||||
- `context.connection` for broker-owned per-WebSocket-connection state shared across `WS(Request& context)` calls
|
||||
- `context.params["UCE_CLI"] == "1"` while handling a local CLI socket request
|
||||
- `context.in` for the current request body, including the current WebSocket message payload inside `WS(Request& context)`
|
||||
- `context.params["WS_..."]` for direct WebSocket message metadata on the request parameter map
|
||||
- `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state
|
||||
- `context.set_status(code[, reason])` to set the HTTP response status
|
||||
|
||||
Useful helpers for that data model now include:
|
||||
Useful helpers for that data model include:
|
||||
|
||||
- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
|
||||
- `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
|
||||
- `DTree::to_u64()`, `to_s64()`, `to_f64()`, `to_bool()`, and `to_stringmap()` for typed reads from structured values
|
||||
- `DValue::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
|
||||
- `DValue::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
|
||||
- `DValue::to_u64()`, `to_s64()`, `to_f64()`, `to_bool()`, and `to_stringmap()` for typed reads from structured values
|
||||
- `json_encode(String)` for emitting JavaScript-safe string literals directly
|
||||
- `ascii_safe_name(String)` for conservative ASCII identifier normalization
|
||||
- `path_join(base, child)` for filesystem-style path assembly
|
||||
- `sqlite_connect()`, `sqlite_query()`, and related helpers for embedded SQLite storage with named prepared parameters
|
||||
- `zip_create()`, `zip_list()`, `zip_read()`, and `zip_extract()` for minimal ZIP archive workflows
|
||||
- `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
|
||||
- `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:
|
||||
|
||||
@@ -81,16 +96,31 @@ Named component handlers are also supported:
|
||||
COMPONENT:BODY(Request& context)
|
||||
{
|
||||
<>
|
||||
<p><?= context.call["body"].to_string() ?></p>
|
||||
<p><?= context.props["body"].to_string() ?></p>
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
Those are intended for sub-rendering through helpers such as `component("components/card:BODY", props, context)` rather than direct page entry.
|
||||
|
||||
Additional lifecycle hooks are also available on ordinary `.uce` units:
|
||||
|
||||
- `INIT(Request& context)` runs once when a worker instantiates that unit's wasm module
|
||||
- `ONCE(Request& context)` runs once per request before the first `RENDER()`, `CLI()`, or `COMPONENT...` entrypoint from that file
|
||||
|
||||
CLI units can be invoked locally with the convenience wrapper or directly over HTTP-over-Unix:
|
||||
|
||||
```bash
|
||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
|
||||
curl --unix-socket /run/uce/cli.sock http://localhost/tests/cli.uce
|
||||
```
|
||||
|
||||
For structured CLI commands, prefer JSON POST bodies and read them with `cli_input(context)`.
|
||||
|
||||
## Template Output
|
||||
|
||||
UCE now treats template parsing as one shared code-vs-literal state machine.
|
||||
UCE treats template parsing as one shared code-vs-literal state machine.
|
||||
|
||||
- `<>` and `?>` both enter literal output mode
|
||||
- `</>` and `<?` both return to code mode
|
||||
@@ -104,9 +134,9 @@ Inside literal output, UCE supports three inline forms:
|
||||
|
||||
Use `<?= ... ?>` by default for user-visible text. Use `<?: ... ?>` only for trusted markup or content that has already been escaped.
|
||||
|
||||
The parser now treats C++ `//` and `/* ... */` comments as comments in both normal code and `<? ... ?>` islands, so quotes or delimiter markers inside comments do not confuse template parsing.
|
||||
The parser treats C++ `//` and `/* ... */` comments as comments in both normal code and `<? ... ?>` islands, so quotes or delimiter markers inside comments do not confuse template parsing.
|
||||
|
||||
The preprocessing implementation is now split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing.
|
||||
The preprocessing implementation is split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing.
|
||||
|
||||
## Components
|
||||
|
||||
@@ -117,7 +147,7 @@ UCE includes a native component layer built on top of ordinary `.uce` files:
|
||||
- `component_exists(name)`
|
||||
- `component_resolve(name)`
|
||||
|
||||
Component props are passed through `context.call`.
|
||||
Component props are passed through `context.props`.
|
||||
|
||||
Component names resolve:
|
||||
|
||||
@@ -139,6 +169,8 @@ Components expose `COMPONENT(Request& context)` as their default entrypoint and
|
||||
|
||||
The component helpers call only `COMPONENT...` handlers. A file meant purely for component use can define `COMPONENT()` without defining `RENDER()`, which keeps direct page entry and component entry cleanly separated. Inside a component file, `component(":NAME", props, context)` and `component_render(":NAME", props, context)` target another named component handler in that same file.
|
||||
|
||||
If the component file also defines `ONCE(Request& context)`, that hook runs once per request before the file's first component/render entrypoint. If it defines `INIT(Request& context)`, that hook runs once when the worker loads the unit.
|
||||
|
||||
## WebSockets
|
||||
|
||||
The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate API to page code:
|
||||
@@ -154,19 +186,21 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate
|
||||
- `ws_send_to(connection_id, message[, binary])`
|
||||
- `ws_close([connection_id])`
|
||||
|
||||
By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint.
|
||||
By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.uce` endpoint.
|
||||
|
||||
Each live WebSocket connection now owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
|
||||
Each live WebSocket connection owns a broker-side `DValue` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
|
||||
|
||||
`ws_message()` may contain either text or binary payload data. Use `ws_opcode()` / `ws_is_binary()` to inspect the current inbound message type.
|
||||
The current inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`.
|
||||
|
||||
`ws_message()` may still be used when you want the payload through a helper API. Use `ws_opcode()` / `ws_is_binary()` to inspect the current inbound message type.
|
||||
|
||||
Set `binary = true` on `ws_send()` or `ws_send_to()` to queue a binary frame instead of a text frame.
|
||||
|
||||
The runtime now accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`.
|
||||
The runtime accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`.
|
||||
|
||||
## Error Reporting
|
||||
|
||||
Unhandled exceptions and recovered fatal request signals now return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`.
|
||||
Unhandled exceptions and recovered fatal request signals return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`.
|
||||
|
||||
The demo page `site/test/error-reporting.uce` can be used to exercise:
|
||||
|
||||
@@ -178,15 +212,19 @@ The current error page includes:
|
||||
|
||||
- request URI
|
||||
- resolved script path
|
||||
- generated C++ path when available
|
||||
- high-level error summary
|
||||
- source/generated excerpts and raw compiler output paths for template/component/unit failure modes
|
||||
- signal number and name when applicable
|
||||
- a native backtrace
|
||||
|
||||
Compile failures are also formatted with the source path, generated C++ path, compile-output artifact path, a nearby source/generated excerpt when a line can be identified, and the raw compiler output.
|
||||
|
||||
This recovery path currently covers normal request handling. It is not yet the universal recovery path for every runtime subsystem.
|
||||
|
||||
## Docs And Tests
|
||||
|
||||
The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`.
|
||||
The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`. Developers coming from React, Next, or Remix should start with `site/doc/pages/coming_from_react.txt` / `/doc/index.uce?p=coming_from_react` for the concept map and starter-router notes.
|
||||
|
||||
Useful entry points:
|
||||
|
||||
@@ -212,8 +250,8 @@ Representative test pages:
|
||||
The intended production shape is:
|
||||
|
||||
- nginx serves static files directly
|
||||
- nginx forwards `.uce` requests and ordinary `.ws.uce` page loads to the UCE FastCGI Unix socket
|
||||
- nginx proxies actual WebSocket upgrade requests for `.ws.uce` endpoints to the runtime's built-in HTTP/WebSocket listener
|
||||
- nginx forwards ordinary `.uce` page loads to the UCE FastCGI Unix socket
|
||||
- nginx proxies WebSocket upgrade requests for `.uce` endpoints to the runtime's built-in HTTP/WebSocket listener
|
||||
- systemd keeps the runtime built, started, and restarted on failure
|
||||
|
||||
The repository ships the pieces used for this:
|
||||
@@ -228,7 +266,7 @@ On a Debian or Ubuntu host, start with the packages needed to build and run UCE
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt install -y nginx clang mariadb-client libmariadb-dev build-essential
|
||||
apt install -y nginx clang mariadb-client libmariadb-dev libpcre2-dev build-essential curl rsync ca-certificates
|
||||
```
|
||||
|
||||
The exact package names may vary by distro. The important requirements are:
|
||||
@@ -236,17 +274,28 @@ The exact package names may vary by distro. The important requirements are:
|
||||
- `nginx`
|
||||
- `clang++`
|
||||
- `mysql_config`
|
||||
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
|
||||
- normal Linux development headers for threads, sockets, `dl`, and backtrace support
|
||||
- Wasmtime C API / C++ headers installed at `/opt/wasmtime` or configured with `WASMTIME_HOME`
|
||||
- WASI SDK installed at `/opt/wasi-sdk` or configured with `WASI_SDK`
|
||||
|
||||
### 2. Put the repo on the server
|
||||
|
||||
This README assumes the repository lives at:
|
||||
|
||||
```bash
|
||||
/Code/uce.openfu.com/uce
|
||||
/opt/uce
|
||||
```
|
||||
|
||||
That is what the shipped `scripts/systemd/uce.service` file currently uses as its `WorkingDirectory` and build path. If you deploy somewhere else, update that unit file before enabling the service.
|
||||
The examples below use that path for the runtime. Publish public application files under the normal web root, for example:
|
||||
|
||||
```bash
|
||||
cd /opt/uce
|
||||
mkdir -p /var/www/html
|
||||
rsync -a site/ /var/www/html/
|
||||
```
|
||||
|
||||
If you deploy somewhere else, update the systemd unit's `WorkingDirectory`, build path, and `ExecStart` path before enabling the service.
|
||||
|
||||
### 3. Configure `/etc/uce/settings.cfg`
|
||||
|
||||
@@ -256,7 +305,7 @@ The runtime reads its server settings from:
|
||||
/etc/uce/settings.cfg
|
||||
```
|
||||
|
||||
The shipped example contains the important filesystem and FastCGI settings:
|
||||
The example contains the filesystem and FastCGI settings:
|
||||
|
||||
```ini
|
||||
BIN_DIRECTORY=/var/cache/uce/work
|
||||
@@ -267,7 +316,7 @@ FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||
FCGI_PORT=9993
|
||||
|
||||
PRECOMPILE_FILES_IN=
|
||||
SITE_DIRECTORY=site
|
||||
SITE_DIRECTORY=/var/www/html
|
||||
PROACTIVE_COMPILE_CHECK_INTERVAL=60
|
||||
|
||||
WORKER_COUNT=4
|
||||
@@ -291,9 +340,9 @@ HTTP_PORT=8080
|
||||
|
||||
Proactive compilation settings:
|
||||
|
||||
- `SITE_DIRECTORY=site` tells the runtime which tree to scan on startup for `.uce` files when `PRECOMPILE_FILES_IN` is left empty.
|
||||
- `SITE_DIRECTORY=/var/www/html` tells the runtime which public web tree to scan on startup for `.uce` files when `PRECOMPILE_FILES_IN` is left empty.
|
||||
- `PRECOMPILE_FILES_IN=` can override that startup scan root with a different absolute or runtime-relative directory.
|
||||
- `PROACTIVE_COMPILE_CHECK_INTERVAL=60` controls how often the low-priority background compiler rechecks known `.uce` files for stale or missing shared objects.
|
||||
- `PROACTIVE_COMPILE_CHECK_INTERVAL=60` controls how often the low-priority background compiler rechecks known `.uce` files for stale or missing wasm modules.
|
||||
|
||||
The runtime keeps a shared known-file registry under `BIN_DIRECTORY` and updates it as request handling discovers new `.uce` files, so proactive recompiles are not limited to the initial startup scan.
|
||||
|
||||
@@ -358,12 +407,14 @@ That script:
|
||||
- writes Debian maintainer scripts for systemd reload/enable handling
|
||||
- follows a more PHP-like/FHS deployment shape with immutable runtime files under `/usr/lib`, config under `/etc`, cache/state under `/var`, and the FastCGI socket under `/run/uce/`
|
||||
|
||||
### 5. Configure nginx for `.uce` and `.ws.uce`
|
||||
### 5. Configure nginx for `.uce` and WebSocket upgrades
|
||||
|
||||
You need two nginx paths for `.ws.uce` endpoints:
|
||||
Any `.uce` unit can expose `WS(Request& context)`. WebSocket upgrade requests for `.uce` paths should be routed to the runtime's HTTP/WebSocket listener.
|
||||
|
||||
- FastCGI for ordinary `.uce` requests and plain `.ws.uce` page renders
|
||||
- HTTP proxying only for actual WebSocket upgrade traffic on `.ws.uce` endpoints
|
||||
You need two transport paths for `.uce` endpoints:
|
||||
|
||||
- FastCGI for ordinary `.uce` page renders
|
||||
- HTTP proxying only for WebSocket upgrade traffic on `.uce` endpoints
|
||||
|
||||
If you use WebSockets, add this `map` in the nginx `http` block:
|
||||
|
||||
@@ -380,7 +431,7 @@ Then use a server block along these lines:
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
root /Code/uce.openfu.com/uce/site;
|
||||
root /var/www/html;
|
||||
|
||||
index index.uce index.html;
|
||||
|
||||
@@ -389,16 +440,6 @@ server {
|
||||
}
|
||||
|
||||
location ~ \.uce$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_param DOCUMENT_URI $uri;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
fastcgi_pass unix:/run/uce/fastcgi.sock;
|
||||
}
|
||||
|
||||
location ~ \.ws\.uce$ {
|
||||
error_page 418 = @uce_websocket;
|
||||
if ($http_upgrade = "websocket") {
|
||||
return 418;
|
||||
@@ -416,7 +457,6 @@ server {
|
||||
location @uce_websocket {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
@@ -428,19 +468,19 @@ server {
|
||||
|
||||
Important details:
|
||||
|
||||
- `.ws.uce` must be matched before the more general `.uce` rule
|
||||
- `fastcgi_pass` should point at the same socket path as `FCGI_SOCKET_PATH`
|
||||
- `proxy_pass` should point at the runtime's `HTTP_PORT`
|
||||
- ordinary `GET /page.ws.uce` page renders should stay on FastCGI
|
||||
- only upgrade requests for `/page.ws.uce` should go through the HTTP/WebSocket listener
|
||||
- `SCRIPT_FILENAME` should resolve to the actual `.uce` file on disk
|
||||
- ordinary `GET /page.uce` page renders should stay on FastCGI
|
||||
- only upgrade requests for `/page.uce` should go through the HTTP/WebSocket listener
|
||||
- `SCRIPT_FILENAME` should resolve to the requested `.uce` file on disk
|
||||
- `proxy_http_version 1.1` and the `Upgrade` / `Connection` headers are required for WebSockets
|
||||
- socket-capable pages are ordinary `.uce` units; route client WebSocket upgrade requests to the HTTP/WebSocket listener
|
||||
|
||||
The `location /` block above is intentionally conservative and only serves real files from `site/`. If your app uses a front-controller pattern such as routing everything through `/index.uce`, change that block accordingly.
|
||||
The `location /` block only serves files from `/var/www/html`. If your app uses a front-controller pattern such as routing everything through `/index.uce`, change that block accordingly.
|
||||
|
||||
### 6. Think about document root and private files
|
||||
|
||||
Point nginx at `site/`, not the repository root. The repo still contains source, scripts, packaging files, and operational assets that are not meant to be public.
|
||||
Point nginx at `/var/www/html`, not the runtime repository root. The repo still contains source, scripts, packaging files, and operational assets that are not meant to be public.
|
||||
|
||||
At minimum, explicitly block internal directories that should never be served directly. For example:
|
||||
|
||||
@@ -450,7 +490,7 @@ location ~ ^/(src|scripts|etc|bin|work|dist|pkg)/ {
|
||||
}
|
||||
```
|
||||
|
||||
If nginx is rooted at `site/`, most of those paths will not be reachable anyway, which is the preferred setup.
|
||||
If nginx is rooted at `/var/www/html`, most of those paths will not be reachable anyway, which is the preferred setup.
|
||||
|
||||
### 7. Reload nginx and verify the deployment
|
||||
|
||||
@@ -469,7 +509,7 @@ curl -i http://127.0.0.1/test/index.uce
|
||||
curl -i http://127.0.0.1/doc/index.uce
|
||||
```
|
||||
|
||||
If WebSockets are enabled, also verify a `.ws.uce` endpoint through nginx rather than talking to the runtime directly.
|
||||
If WebSockets are enabled, also verify a `.uce` endpoint that defines `WS(Request& context)` through nginx rather than talking to the runtime directly.
|
||||
|
||||
### 8. Troubleshooting
|
||||
|
||||
@@ -478,9 +518,9 @@ Common failure modes:
|
||||
- `502 Bad Gateway`
|
||||
Usually means `uce.service` is down, the Unix socket path does not match, or the request crashed before sending a valid response.
|
||||
- WebSocket upgrade fails
|
||||
Check that nginx is routing `.ws.uce` to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost.
|
||||
Check that nginx is routing WebSocket upgrade requests to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost.
|
||||
- Requests compile but immediately crash
|
||||
Check `journalctl -u uce.service`. Generated units now carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually.
|
||||
Check `journalctl -u uce.service`. Generated units carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually.
|
||||
- nginx serves raw source or internal files
|
||||
Tighten the server root and add explicit deny rules for non-public directories.
|
||||
|
||||
@@ -500,4 +540,4 @@ For up-to-date usage, prefer:
|
||||
|
||||
## AI Disclosure
|
||||
|
||||
This project is largely human-made, with all the typical idiosyncracies of my projects clearly visible. However, OpenAI Codex was used for code review and documentation. Claude Opus was used for UI design work, and I used VS Code's git commit message generator.
|
||||
This project is largely human-made, with all the typical idiosyncracies of my projects clearly visible. However, OpenAI Codex was used for code review, debugging, API integration work, and documentation. Claude Opus was used for UI design work, and I used VS Code's git commit message generator.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
commit ad3d65cdacdcc5aed72fe12354f66190d7be454d
|
||||
Initial commit
|
||||
|
||||
commit c047927b189860220428b42400ed18942150882c
|
||||
initial import
|
||||
|
||||
|
||||
commit 3e8f0f1fa780da7836128fdbc39bbe8c2ba97de8
|
||||
initial import
|
||||
|
||||
|
||||
commit 939009f9a11d0934226c830375057491f088d362
|
||||
shell stuff, preprocessor directive
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
commit 18ca2368bc1d4a5ef7e4c8470a9b3e32f496fc86
|
||||
VERY basic UTF8 parser
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
commit 17336fe649c52c162a4541968c8d24c0c2ab3eb9
|
||||
Unicode stuff
|
||||
|
||||
|
||||
commit 5df52146be3f5a45759459d5716aec4ef8d3c33a
|
||||
split_space
|
||||
|
||||
|
||||
commit 3a9dfba86cb7dadb1268fcfd689b14066779e6aa
|
||||
fixed bug in #load
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
commit 0a6ebc60f0b8aacf8b01e14bd9b6447c750edeb5
|
||||
precompile on startup
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
commit 6f6919f3f01686175001251e2fca215bccc4806f
|
||||
backage builder
|
||||
|
||||
|
||||
commit defc628204f95d0cdd16c95c89ea30efe922b31a
|
||||
packaging
|
||||
|
||||
|
||||
commit 9e5b7d39e3b8be9f4ad537a134b1081564ae4abc
|
||||
Basic readme
|
||||
|
||||
commit 21fdeafb6c5b8bc80a9d93ed61c36e05fbe7b267
|
||||
Basic documentation
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
commit 364d83b199afa6a341173a80791060a42c169f0c
|
||||
doc update
|
||||
|
||||
|
||||
commit 392b7224b16e41198b0425c5ae04f46e014e78c7
|
||||
Merge branch 'main' of github.com:ThingamaNet/uce
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
commit 984453f336a9b2ad47689d161c89d7020006f75a
|
||||
update sigh
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
commit cd8f07aaa78fc488eba3b0e435350e5c2823e53e
|
||||
Refactor FastCGI server and compiler, enhance HTTP header parsing, and add WebSocket support
|
||||
|
||||
- Refactored FastCGIServer class to improve socket handling and added shutdown functionality.
|
||||
- Updated compiler functions to streamline HTML and text literal processing.
|
||||
- Enhanced split_kv function to support uppercase keys and added split_http_headers for better HTTP header parsing.
|
||||
- Introduced new session management functions to validate and handle session IDs.
|
||||
- Added WebSocket frame parsing and handling in the URI module.
|
||||
- Created systemd service scripts for easier deployment and management of the UCE FastCGI runtime.
|
||||
- Added new test cases for header handling, time parsing, and WebSocket functionality.
|
||||
|
||||
|
||||
commit 86dc93864e8eab9227ac50b16f1fc7f561383f5b
|
||||
Add WebSocket support and enhance chat functionality
|
||||
|
||||
- Implement WebSocket handling in the FastCGI server, allowing for real-time communication.
|
||||
- Introduce functions for managing WebSocket connections, broadcasting messages, and sending to specific connections.
|
||||
- Create a chat interface in the `websockets.ws.uce` file, including message handling and user notifications.
|
||||
- Refactor existing code to accommodate WebSocket integration, ensuring compatibility with HTTP requests.
|
||||
- Update documentation to reflect new features and usage instructions for WebSocket functionality.
|
||||
|
||||
|
||||
commit 46d98a092fb606c87ab4b9eef32fe68eeca2b2cb
|
||||
Enhance WebSocket support: add opcode handling, binary message support, and improve connection validation
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
commit be514d63d6ed709a40441db9dc809837081d54f6
|
||||
trying to port web app starter from PHP
|
||||
|
||||
|
||||
commit 2b5586d7dfffc5c1c40cac97e6d4d0e1f86f151a
|
||||
decided in favor of dedicated COMPONENT() macro, updates to documentation
|
||||
|
||||
|
||||
commit d1167aec3b4d7870db0b289ee0ea690f8e7e5cc9
|
||||
getting closer to full port of web app starter
|
||||
|
||||
|
||||
commit af642c8167383f9acf99274d3b3c94017d860dcf
|
||||
getting closer to full port of web app starter
|
||||
|
||||
|
||||
commit dc05f9faa5eab980f1f1fd44cee06f811cc748ff
|
||||
PHP familiarity shortcuts
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
commit b53eb6e4f1b5fa4e2a4907022f591eb9a1860c33
|
||||
Enhance template parser to handle C++ comments and refactor preprocessing logic
|
||||
|
||||
- Updated parser to correctly interpret C++ `//` and `/* ... */` comments within template code.
|
||||
- Split preprocessing implementation into separate files for better organization.
|
||||
- Added regression test for comment parsing in templates.
|
||||
- Adjusted CSS styles for improved layout and readability in documentation.
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
commit 8223dcc6b3cb7f4d26c69097fac973c2671f0938
|
||||
I think I need to change the documentation format
|
||||
|
||||
|
||||
commit f7b066b374f1031d38e3d2b344f6680534c3a2b2
|
||||
changing doc format and HTML literals
|
||||
|
||||
|
||||
commit 14ebf10a229021bb5d0fb1eae42a69c042699318
|
||||
changing doc format and HTML literals
|
||||
|
||||
|
||||
commit 223cf4c6e1c7a205bf1a27bebdc6d04fd7557df0
|
||||
website with slop placeholders
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
commit cd445f3c9b6aa079a969351d6eb742b8a4d24f6b
|
||||
some cleanup
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
commit 9f7625c7fdd6a9d2a184dc2cdf9f8a719d219ec2
|
||||
working on documentation and more API functions
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
commit 02e153a6a78bf8ded1c7380713023b4f9c3b656f
|
||||
Add archive helpers and harden task runtime
|
||||
|
||||
|
||||
commit d37517041de0c5ebe4dfd232093a78ff49cff383
|
||||
Add custom server API and runtime limits
|
||||
|
||||
|
||||
commit 0d8b74930c74cc8018cb1c0cfc7cfe6740b49cf6
|
||||
Harden HTTP path headers sessions and archives
|
||||
|
||||
|
||||
commit 41e9ca219fd684c75e78a5493e20c754c7899105
|
||||
Streamline hardening helpers and expand coverage
|
||||
|
||||
|
||||
commit 8b37e7ea1e58e36c97852f79ec121947a1ffea08
|
||||
Fix direct HTTP status sanitizer fallback
|
||||
|
||||
|
||||
commit 71ddcaf7d48ffd1343e597e4715b570c922a306c
|
||||
Consolidate config and base64 helpers
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
commit 7f757654b658d049564f1725f54fde781575e304
|
||||
fix: harden UCE runtime and starter
|
||||
|
||||
|
||||
commit 20db6695890a3ac274217fd0ed9e0edf7c5eb093
|
||||
fix: stabilize runtime follow-up regressions
|
||||
|
||||
|
||||
commit b957a2373b8464ffcb7791cc6b0031c441d5b09c
|
||||
chore: ignore Python cache artifacts
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
commit 941f5aea08941327eecbc43128d8f171a4064616
|
||||
feat: add configurable UCE error pages
|
||||
|
||||
|
||||
commit 7066da3cdef08b01dda64bca34caadc7000587b2
|
||||
refactor: rename DTree to DValue
|
||||
|
||||
|
||||
commit 7e2faf1472c84f302ce5b7f9674091a1a2b3930f
|
||||
spike: validate WASM phase 0
|
||||
|
||||
|
||||
commit 80285b7fb4acd15850218ee87c72ec8272332c05
|
||||
feat: implement WASM phase 1 DValue ABI
|
||||
|
||||
|
||||
commit 961df2d542ff6554e32da9ee28e73f5fc59b5975
|
||||
phase 5
|
||||
|
||||
|
||||
commit 89b5499c8f4e1a8acc8748e66c9dba9eb9679369
|
||||
fix: harden WASM phase 5 harness
|
||||
|
||||
|
||||
commit eb8f303f94f0f43699eca7cd6474eedaef1ed711
|
||||
docs: remove unsupported second-plane language roadmap
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
commit 577aae076e3603137265332adfdded3c15cfa8f4
|
||||
W3
|
||||
|
||||
|
||||
commit afaa4dd7c0203f322d625593a0d1c96c37130005
|
||||
feat: cut over to WASM backend
|
||||
|
||||
|
||||
commit 5a56d4f39e4b30a9a4e43985d64906aa16a642e6
|
||||
W5
|
||||
|
||||
|
||||
commit c84fc86e6cdfe254a59d57b909f647e6c254ef76
|
||||
W6
|
||||
|
||||
|
||||
commit fe83c524118722c062295bc9575cb71eff64dacb
|
||||
feat: membrane remaining wasm host surfaces
|
||||
|
||||
|
||||
commit d6421cb8f3721e69e999b84335e9094ad96356e9
|
||||
feat: extend wasm backend entrypoints
|
||||
|
||||
|
||||
commit af6500d1343581abdac475dcb6855821afd8c8d0
|
||||
fix: harden wasm w7 entrypoint paths
|
||||
|
||||
|
||||
commit b1856c1725970b31027383a7ae79c2140a62a896
|
||||
chore: narrow wasm backend entrypoint API
|
||||
|
||||
|
||||
commit 6bb4f7f0ad801a0127956f49e54d2be722c6537f
|
||||
test: port network suite to uce cli runner
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
commit 15e8d092bc13c425b5c7f7a19dfaf3c9728104f5
|
||||
W7+
|
||||
|
||||
|
||||
commit 8587fbc5aa0f38739182fa50383bb94c8d821ed7
|
||||
wasm runtime: central WS broker, unified handlers, W7d holdouts, membrane completeness
|
||||
|
||||
- WS: a dedicated broker process owns HTTP_PORT + every connection; it forwards
|
||||
renders to the worker pool over uce.sock (non-blocking) and applies ws_*
|
||||
command batches flushed back at workspace teardown. Removes the now-dead
|
||||
per-worker websocket executor (-509 lines).
|
||||
- Dispatch: unify CLI / WebSocket / serve_http / page render through one
|
||||
serve_via_wasm(entry_unit, handler) path; handler string -> __uce_<handler>
|
||||
export symbol.
|
||||
- W7d: rewrite zip.uce to the membrane return-value error contract (no C++
|
||||
try/catch), error-reporting.uce to genuine wasm traps instead of throw, and
|
||||
sharedunit.uce to unit_info(); empty the native-only token gate.
|
||||
- Membrane: wire ls / mkdir / file_mtime through new uce_host_file_list /
|
||||
uce_host_file_mkdir / uce_host_file_mtime hostcalls (resolve_guest_file gains
|
||||
directory support). Fixes /doc/index.uce listing nothing; adds a regression
|
||||
assertion that the index enumerates items.
|
||||
- Docs: add docs/wasm-runtime-architecture.md; record the W7e staged native-
|
||||
deletion plan in WASM-PROPOSAL.md.
|
||||
|
||||
|
||||
commit 2debd338040cf97e82bb8ccbb5ec09caee089c17
|
||||
W7e: wasm-preferred dispatch + compile-on-demand; harden unit ABI validator
|
||||
|
||||
- Dispatch (linux_fastcgi.cpp): route every request through wasm. On a
|
||||
cold/stale artifact, compile the unit on demand (get_shared_unit, forced) and
|
||||
serve wasm; native compiler_invoke* remains only as a fallback when wasm
|
||||
cannot be made ready (compile failure / backend disabled). Applies to the 4
|
||||
handle_complete branches and the CLI socket path.
|
||||
- backend.cpp: delete the now-vestigial native-only fallback token gate
|
||||
(wasm_backend_native_fallback_*), empty since W7d; should_handle now gates on
|
||||
config + current artifact + healthy worker only.
|
||||
- check_unit_wasm.py: skip the defense-in-depth llvm-nm allocator scan when
|
||||
llvm-nm SIGSEGVs on a degenerate-but-valid module (e.g. a unit with no
|
||||
exported handlers). Fixes site/demo/empty.uce, the last unit that could not
|
||||
produce a .wasm; forbidden allocator *exports* are still rejected.
|
||||
|
||||
A pi-assisted review caught that the compile-freshness check keys off the .so
|
||||
mtime only, so force_recompile is required to rebuild a missing/stale .wasm; a
|
||||
unit already cached in-process whose .wasm later vanished still uses native
|
||||
fallback (closed in W7e stage B). Native execution is otherwise bypassed for
|
||||
all real traffic.
|
||||
|
||||
|
||||
commit fb728d63bcd0d80bd5161537e81c30d27eb4df7b
|
||||
W7e stage B: factor the .wasm artifact into unit compile-freshness
|
||||
|
||||
inspect_shared_unit_filesystem() tracked only the .so mtime, so a missing or
|
||||
stale .wasm with a current .so never triggered a rebuild and the unit fell to
|
||||
native indefinitely (the in-process cache also never invalidated). Account for
|
||||
su->wasm_name when wasm unit compilation is enabled: a unit counts as compiled
|
||||
only as of the OLDER of the two artifacts; a missing .wasm forces a recompile.
|
||||
Closes the cached-vanished-.wasm gap noted in the stage A review.
|
||||
|
||||
|
||||
commit cf513368730e5f337e64a9c9f1456e6a64f49d9c
|
||||
W7e: delete the native unit pipeline (.so compile + dlopen execution)
|
||||
|
||||
Units now run exclusively on wasm; the native generated-C++ -> clang -> .so ->
|
||||
dlopen path and its request-time fallback are removed.
|
||||
|
||||
- Dispatch (linux_fastcgi.cpp): the 4 handle_complete branches + the CLI-socket
|
||||
path route every request through wasm (wasm_ready compiles cold/stale on
|
||||
demand); a wasm-unavailable unit now yields a clean error page
|
||||
(fail_wasm_unavailable / render_request_failure) instead of native execution.
|
||||
compiler_invoke / _cli / _websocket / _serve_http deleted.
|
||||
- compiler.cpp (-1274): removed the native .so compile (COMPILE_SCRIPT),
|
||||
load_shared_unit, dlopen/dlsym/dlclose, compiler_load_shared_unit, and the
|
||||
SharedUnit .so function-pointer fields (on_setup/on_render/on_component/
|
||||
on_websocket/on_cli/on_once/on_init) in types.h/types.cpp. compile_shared_unit
|
||||
now builds only the .wasm side-module; the .uce preprocessor/parser front-end
|
||||
is kept (it emits the C++ the wasm compile consumes).
|
||||
- unit_call()/component()/once/init now resolve across units through the wasm
|
||||
host component resolver (uce_host_component_resolve) instead of native dlsym;
|
||||
configured runtime error pages render through the wasm backend.
|
||||
- Dropped the WASM_BACKEND_ENABLED feature flag and dead COMPILE_SCRIPT /
|
||||
COMPILE_WASM_UNITS config; unit ABI freshness tied to UCE_UNIT_ABI_VERSION;
|
||||
guard against serving a stale .wasm for a deleted source; retired the obsolete
|
||||
W5 native-vs-wasm toggle script. Docs updated.
|
||||
|
||||
|
||||
commit bfd6d338299dd9001fe2d4a66f82bbc2b3b07ed9
|
||||
W7f: sweep dead/legacy/fallback leftovers after native-pipeline removal
|
||||
|
||||
Post-deletion cleanup (units run only on wasm):
|
||||
- types.h/compiler.cpp: drop the native-era SharedUnit fields so_name,
|
||||
bin_file_name, and the opt_so_optional cache-mode plumbing (no native
|
||||
optional .so path remains). The per-unit compile lock is re-keyed from
|
||||
so_name+.lock to wasm_name+.lock (still per-unit).
|
||||
- unit_info() and to_string(SharedUnit*) no longer expose .so artifact fields.
|
||||
- backend.h: drop the stale "+ fallback-token gate" comment.
|
||||
- Docs/comments corrected to wasm-only reality: README, tests/README,
|
||||
site/doc C++ preprocessor + error_pages + unit_info pages, site/info intro,
|
||||
site/demo/unit-browser artifact card; the Phase-5 native-vs-wasm benchmark
|
||||
harness (tests/wasm_benchmark.py) reframed for the wasm-only backend.
|
||||
|
||||
Audit confirmed no live references remain to so_handle, load_shared_unit,
|
||||
compiler_load_shared_unit, compiler_invoke*/_cli/_websocket/_serve_http,
|
||||
COMPILE_SCRIPT/COMPILE_WASM_UNITS, or the native export-symbol constants;
|
||||
request_ref_handler/dv_call_handler are kept (live wasm funcref casts).
|
||||
|
||||
Swept via the pi agent (delegated to a gpt-5.3-codex-spark sub-model);
|
||||
independently re-verified on the host: run_cli_tests --include-wasm-kill =>
|
||||
87 passed, 0 failed, 0 skipped.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
commit 560290ca1d18d4d5a0d204bb187d1aa68c3b5ac7
|
||||
perf: cache the compiled core module so fresh workers deserialize, not recompile
|
||||
|
||||
Workers recycle every 8 requests (calls_until_termination=8). Each fresh worker
|
||||
ran wasmtime::Module::compile() on the 6.8MB bin/wasm/core.wasm — a ~1.3s
|
||||
Cranelift JIT — on its first request, so every ~8th request spiked to ~1.3s and
|
||||
dominated suite wall-clock.
|
||||
|
||||
Cache the compiled artifact: on worker core-module load, if bin/wasm/core.cwasm
|
||||
exists and is newer than core.wasm, load it via Module::deserialize_file() (mmap,
|
||||
~ms); otherwise Module::compile() as before and atomically (temp+rename) write
|
||||
the serialized artifact for the next worker. Deserialize failure / stale cache
|
||||
falls back to a normal compile, so it is self-healing; rebuilding core.wasm
|
||||
(newer mtime) invalidates the cache. Engine config (epoch_interruption,
|
||||
signals_based_traps(false)) is unchanged, which the serialized format requires.
|
||||
|
||||
|
||||
commit 83ab9e10f7c0f17805e40a9a3d2b4a3357e5280f
|
||||
perf: extend the compiled-module disk cache to per-unit modules
|
||||
|
||||
Follow-up to 560290c. Per-unit .wasm modules were still Cranelift-compiled per
|
||||
worker on first use (~40-70ms each), so with 8-call worker recycling fresh
|
||||
workers re-JIT every unit they touch.
|
||||
|
||||
Refactor the core cache logic into a shared WasmWorker helper:
|
||||
- cached_wasm_path(p): maps <...>.wasm -> <...>.cwasm.
|
||||
- load_or_compile_cached_module(engine, cached, wasm, bytes, err): deserialize_file
|
||||
the .cwasm when it is newer than the .wasm; otherwise Module::compile + serialize,
|
||||
written atomically (temp+rename); deserialize failure falls back to compile.
|
||||
Both the core module load and unit_module() now go through this helper, so unit
|
||||
artifacts get the same <unit>.uce.cwasm cache the core got.
|
||||
|
||||
|
||||
commit 4f84ac544d9c91a66d068d372055eec8e1723def
|
||||
feat: request_perf() worker-side timing hostcall; restore demo System Info
|
||||
|
||||
Units run in the wasm sandbox, so my_pid/parent_pid/context.server->request_count
|
||||
read as sandbox stubs — the demo System Info counters were broken, and there was
|
||||
no authoritative server-side request timing available to unit code (client-side
|
||||
measurement cannot see queue/dispatch latency).
|
||||
|
||||
Add a request_perf() unit API backed by a new uce_host_request_perf hostcall.
|
||||
The native worker answers it live, returning a DValue:
|
||||
worker_pid, parent_pid, request_count,
|
||||
accept_us = (time_start - time_init)*1e6 (entry -> dispatch wait),
|
||||
running_us = (now - time_start)*1e6 (since dispatch, live),
|
||||
total_us = (now - time_init)*1e6 (since the request entered UCE),
|
||||
workspace_birth_us.
|
||||
time_init is captured at request entry (handle_request, with a handle_complete
|
||||
fallback); a RequestPerfSnapshot {pids, request_count, time_init, time_start} is
|
||||
threaded from wasm_backend_serve through wasm_worker_serve onto the workspace,
|
||||
and the hostcall computes the live deltas at call time. Wired like uce_host_units
|
||||
(sized DValue hostcall): core_hostcalls.syms + sys.cpp/sys.h request_perf().
|
||||
|
||||
site/demo/index.uce System Info now uses request_perf() and shows the real worker
|
||||
PID, an incrementing per-worker request count, and the timing counters.
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
diff --git a/src/3rdparty/miniz/miniz_tdef.c b/src/3rdparty/miniz/miniz_tdef.c
|
||||
--- a/src/3rdparty/miniz/miniz_tdef.c
|
||||
+++ b/src/3rdparty/miniz/miniz_tdef.c
|
||||
@@
|
||||
-static const mz_uint s_tdefl_num_probes[11];
|
||||
+static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 };
|
||||
|
||||
static int tdefl_flush_block(tdefl_compressor *d, int flush)
|
||||
@@
|
||||
-static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 };
|
||||
-
|
||||
/* level may actually range from [0,10] (10 is a "hidden" max level, where we want a bit more compression and it's fine if throughput to fall off a cliff on some files). */
|
||||
@@ -0,0 +1,34 @@
|
||||
# SQLite 3.46.1 Vendor Import
|
||||
|
||||
Imported the official SQLite amalgamation without local source modifications.
|
||||
|
||||
- Version: SQLite 3.46.1
|
||||
- Upstream archive: https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip
|
||||
- Import date: 2026-05-29
|
||||
- Vendored files:
|
||||
- `src/3rdparty/sqlite/sqlite3.c`
|
||||
- `src/3rdparty/sqlite/sqlite3.h`
|
||||
- `src/3rdparty/sqlite/sqlite3ext.h`
|
||||
|
||||
## Checksums
|
||||
|
||||
```text
|
||||
77823cb110929c2bcb0f5d48e4833b5c59a8a6e40cdea3936b99e199dbbe5784 sqlite-amalgamation-3460100.zip
|
||||
6c35bc5f7f85eac9c49928bacbb02bb694b547aabf69197e058cca245ad80e83 sqlite3.c
|
||||
89b62c671c5964e137409ce034941b7b05a3af2c9875aba41f47f9483d0c2515 sqlite3.h
|
||||
b184dd1586d935133d37ad76fa353faf0a1021ff2fdedeedcc3498fff74bbb94 sqlite3ext.h
|
||||
```
|
||||
|
||||
## Runtime compile flags
|
||||
|
||||
The amalgamation is included by `src/lib/uce_lib.cpp` with:
|
||||
|
||||
```cpp
|
||||
#define SQLITE_THREADSAFE 1
|
||||
#define SQLITE_OMIT_LOAD_EXTENSION 1
|
||||
#define SQLITE_DQS 0
|
||||
#define SQLITE_DEFAULT_FOREIGN_KEYS 1
|
||||
#define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1
|
||||
```
|
||||
|
||||
No patch diff is needed because the vendored SQLite files are unmodified.
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
# UCE Runtime Setup
|
||||
|
||||
This guide describes how to run UCE behind nginx or Apache. UCE is a FastCGI application server for `.uce` units; the web server should serve static files directly and forward dynamic `.uce` requests to the UCE runtime.
|
||||
|
||||
## Deployment shape
|
||||
|
||||
A typical deployment has four pieces:
|
||||
|
||||
1. A checked-out or packaged UCE runtime tree.
|
||||
2. `/etc/uce/settings.cfg`, read by the UCE runtime at startup.
|
||||
3. `uce.service`, a systemd service that builds/starts/restarts the runtime.
|
||||
4. nginx or Apache as the public HTTP server.
|
||||
|
||||
Recommended filesystem layout for a source checkout:
|
||||
|
||||
```text
|
||||
/opt/uce/ UCE repository/runtime root
|
||||
/var/www/html/ public web root served by nginx/Apache
|
||||
/etc/uce/settings.cfg runtime configuration
|
||||
/run/uce/fastcgi.sock FastCGI socket used by nginx/Apache
|
||||
/run/uce/cli.sock local CLI/admin/test socket
|
||||
/var/cache/uce/work generated source, wasm modules, caches
|
||||
/var/lib/uce/uploads multipart upload scratch space
|
||||
/var/lib/uce/sessions session files
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
On Debian/Ubuntu-like systems, install the distro packages first:
|
||||
|
||||
```bash
|
||||
apt update
|
||||
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.
|
||||
|
||||
- **Wasmtime C API / C++ headers** at `/opt/wasmtime` by default. `scripts/build_linux.sh` expects:
|
||||
- `/opt/wasmtime/include/wasmtime.hh`
|
||||
- `/opt/wasmtime/include/wasmtime/*.h`
|
||||
- `/opt/wasmtime/lib/libwasmtime.so`
|
||||
|
||||
- **Pinned WASI SDK** at `/opt/wasi-sdk` by default. `scripts/build_core_wasm.sh` and request-time `scripts/compile_wasm_unit` expect:
|
||||
- `/opt/wasi-sdk/bin/clang++`
|
||||
- `/opt/wasi-sdk/bin/wasm-ld`
|
||||
- `/opt/wasi-sdk/bin/llvm-objcopy`
|
||||
|
||||
You can use different install locations by setting environment variables before building and in the systemd service environment:
|
||||
|
||||
```bash
|
||||
export WASMTIME_HOME=/path/to/wasmtime
|
||||
export WASI_SDK=/path/to/wasi-sdk
|
||||
```
|
||||
|
||||
Install one web server:
|
||||
|
||||
```bash
|
||||
apt install -y nginx
|
||||
# or
|
||||
apt install -y apache2
|
||||
```
|
||||
|
||||
Build UCE from the repository root:
|
||||
|
||||
```bash
|
||||
cd /opt/uce
|
||||
bash scripts/build_core_wasm.sh
|
||||
bash scripts/build_linux.sh
|
||||
```
|
||||
|
||||
Publish the starter site or your application files into the web root:
|
||||
|
||||
```bash
|
||||
mkdir -p /var/www/html
|
||||
rsync -a site/ /var/www/html/
|
||||
```
|
||||
|
||||
The main binary is written to:
|
||||
|
||||
```text
|
||||
bin/uce_fastcgi.linux.bin
|
||||
```
|
||||
|
||||
### Installing Wasmtime and WASI SDK
|
||||
|
||||
The UCE build does not download these dependencies for you. Install Wasmtime through a compatible distro package or a pinned upstream C API archive. Install WASI SDK with UCE's pinned installer, or unpack the same pinned archive under `/opt/wasi-sdk`.
|
||||
|
||||
Do not use `curl | sh` installers in production setup scripts. Download archives from the upstream release pages, verify checksums/signatures when available, and record the exact versions in your deployment notes. Avoid installing a release published in the last few days unless you have reviewed it separately.
|
||||
|
||||
The expected directories are:
|
||||
|
||||
```text
|
||||
/opt/wasmtime/include/wasmtime.hh
|
||||
/opt/wasmtime/include/wasmtime/store.h
|
||||
/opt/wasmtime/lib/libwasmtime.so
|
||||
|
||||
/opt/wasi-sdk/bin/clang++
|
||||
/opt/wasi-sdk/bin/wasm-ld
|
||||
/opt/wasi-sdk/bin/llvm-objcopy
|
||||
```
|
||||
|
||||
Install the WASI SDK:
|
||||
|
||||
```bash
|
||||
cd /opt/uce
|
||||
scripts/install_wasi_sdk.sh
|
||||
scripts/install_wasi_sdk.sh --check-only
|
||||
```
|
||||
|
||||
The current pin is documented in `docs/wasi-sdk-toolchain.md`. The script verifies the archive SHA256 before installing and updates `/opt/wasi-sdk` to point at the pinned versioned directory.
|
||||
|
||||
For Wasmtime, use a compatible distro package or an upstream C API archive. Example flow using an archive you have already chosen and verified:
|
||||
|
||||
```bash
|
||||
mkdir -p /opt /tmp/uce-deps
|
||||
cd /tmp/uce-deps
|
||||
|
||||
# Download the Wasmtime C API archive for your architecture from the upstream
|
||||
# release page, then verify its checksum before unpacking. The archive name
|
||||
# normally contains "c-api".
|
||||
sha256sum -c wasmtime-c-api.sha256
|
||||
mkdir -p /opt/wasmtime
|
||||
tar -xf wasmtime-*-c-api*.tar.* -C /opt/wasmtime --strip-components=1
|
||||
```
|
||||
|
||||
After unpacking, verify the tools UCE needs. Also record the exact Wasmtime and WASI SDK versions used. The native build embeds an rpath for `$WASMTIME_HOME/lib`, so the service environment should use the same `WASMTIME_HOME` value used during build.
|
||||
|
||||
```bash
|
||||
test -f /opt/wasmtime/include/wasmtime.hh
|
||||
test -f /opt/wasmtime/lib/libwasmtime.so
|
||||
/opt/wasi-sdk/bin/clang++ --version
|
||||
/opt/wasi-sdk/bin/wasm-ld --version
|
||||
/opt/wasi-sdk/bin/llvm-objcopy --version
|
||||
```
|
||||
|
||||
If your paths differ, export the variables for manual builds:
|
||||
|
||||
```bash
|
||||
WASMTIME_HOME=/usr/local/wasmtime WASI_SDK=/usr/local/wasi-sdk bash scripts/build_core_wasm.sh
|
||||
WASMTIME_HOME=/usr/local/wasmtime WASI_SDK=/usr/local/wasi-sdk bash scripts/build_linux.sh
|
||||
```
|
||||
|
||||
For systemd, add an override:
|
||||
|
||||
```bash
|
||||
systemctl edit uce.service
|
||||
```
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
Environment=WASMTIME_HOME=/usr/local/wasmtime
|
||||
Environment=WASI_SDK=/usr/local/wasi-sdk
|
||||
```
|
||||
|
||||
Then reload and restart:
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl restart uce.service
|
||||
```
|
||||
|
||||
## Runtime configuration
|
||||
|
||||
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:
|
||||
|
||||
```ini
|
||||
BIN_DIRECTORY=/var/cache/uce/work
|
||||
TMP_UPLOAD_PATH=/var/lib/uce/uploads
|
||||
SESSION_PATH=/var/lib/uce/sessions
|
||||
|
||||
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||
CLI_SOCKET_PATH=/run/uce/cli.sock
|
||||
|
||||
SITE_DIRECTORY=/var/www/html
|
||||
HTTP_DOCUMENT_ROOT=/var/www/html
|
||||
JIT_COMPILE_ON_REQUEST=1
|
||||
PROACTIVE_COMPILE_ENABLED=1
|
||||
PROACTIVE_COMPILE_CHECK_INTERVAL=60
|
||||
|
||||
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
|
||||
WASM_BACKEND_VERBOSE=0
|
||||
WASM_CORE_PATH=/opt/uce/bin/wasm/core.wasm
|
||||
WASM_MEMORY_LIMIT_BYTES=536870912
|
||||
WASM_EPOCH_DEADLINE_TICKS=200
|
||||
WASM_EPOCH_PERIOD_MS=50
|
||||
|
||||
WORKER_COUNT=4
|
||||
MAX_MEMORY=16777216
|
||||
SESSION_TIME=2592000
|
||||
|
||||
HTTP_PORT=8080
|
||||
```
|
||||
|
||||
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.
|
||||
- `CLI_SOCKET_PATH` is a local HTTP-over-Unix socket used by `scripts/uce-cli` and test/admin units.
|
||||
- `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.
|
||||
- `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 generated C++, wasm artifacts, compile output, and runtime caches.
|
||||
- `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime.
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
After editing settings, restart UCE:
|
||||
|
||||
```bash
|
||||
systemctl restart uce.service
|
||||
```
|
||||
|
||||
## systemd service
|
||||
|
||||
For source-checkout deployments, install the provided service helper:
|
||||
|
||||
```bash
|
||||
cd /opt/uce
|
||||
scripts/systemd/manage-uce-service.sh setup
|
||||
```
|
||||
|
||||
That helper installs `scripts/systemd/uce.service`, creates runtime directories, enables the service, and starts it.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
scripts/systemd/manage-uce-service.sh status
|
||||
scripts/systemd/manage-uce-service.sh restart
|
||||
scripts/systemd/manage-uce-service.sh logs 200
|
||||
```
|
||||
|
||||
Equivalent manual systemd service for a source checkout:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=UCE FastCGI Runtime
|
||||
After=network-online.target mariadb.service memcached.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/uce
|
||||
RuntimeDirectory=uce
|
||||
StateDirectory=uce
|
||||
CacheDirectory=uce
|
||||
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 /opt/uce/scripts/build_linux.sh
|
||||
ExecStart=/opt/uce/bin/uce_fastcgi.linux.bin
|
||||
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
TimeoutStopSec=15
|
||||
KillMode=mixed
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Install it as `/etc/systemd/system/uce.service` and run:
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now uce.service
|
||||
```
|
||||
|
||||
### Debian package build
|
||||
|
||||
To build a Debian package from the repository root:
|
||||
|
||||
```bash
|
||||
bash scripts/make_deb.sh 0.1.2
|
||||
```
|
||||
|
||||
The Debian package creator bundles WASI SDK and Wasmtime by default when `/opt/wasi-sdk` and `/opt/wasmtime` are present. Verify the pinned SDK before building:
|
||||
|
||||
```bash
|
||||
scripts/install_wasi_sdk.sh --check-only
|
||||
bash scripts/make_deb.sh 0.1.2
|
||||
```
|
||||
|
||||
This includes the resolved `/opt/wasi-sdk-...` tree, `/opt/wasi-sdk` symlink, resolved `/opt/wasmtime-...` tree, and `/opt/wasmtime` symlink in the package. It makes the package large, but keeps request-time unit compilation and runtime linking tied to the toolchain versions that passed the test suite. Set `UCE_DEB_BUNDLE_WASI_SDK=0` or `UCE_DEB_BUNDLE_WASMTIME=0` only if your deployment provides those exact dependencies separately.
|
||||
|
||||
### RPM package build
|
||||
|
||||
To build an RPM package from the repository root, install `rpmbuild` on the packaging host, verify the pinned SDK, then run:
|
||||
|
||||
```bash
|
||||
scripts/install_wasi_sdk.sh --check-only
|
||||
bash scripts/make_rpm.sh 0.1.2
|
||||
```
|
||||
|
||||
The RPM creator mirrors the Debian package layout: runtime files under `/usr/lib/uce`, public files under `/var/www/html`, config under `/etc/uce/settings.cfg`, systemd unit under `/usr/lib/systemd/system/uce.service`, and bundled `/opt/wasi-sdk` plus `/opt/wasmtime` trees by default. Set `UCE_RPM_BUNDLE_WASI_SDK=0` or `UCE_RPM_BUNDLE_WASMTIME=0` only if your deployment provides those exact dependencies separately.
|
||||
|
||||
## How request routing works
|
||||
|
||||
### Static files
|
||||
|
||||
The web server should serve ordinary static files directly from the public web root, for example `/var/www/html`.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
/style.css
|
||||
/images/logo.png
|
||||
/examples/uce-starter/js/site.js
|
||||
```
|
||||
|
||||
These should not touch the UCE runtime.
|
||||
|
||||
### Normal `.uce` page requests
|
||||
|
||||
For a request such as:
|
||||
|
||||
```text
|
||||
GET /doc/index.uce?p=component
|
||||
```
|
||||
|
||||
nginx/Apache forwards the request to `FCGI_SOCKET_PATH` as FastCGI. The web server must provide CGI/FastCGI variables including:
|
||||
|
||||
- `SCRIPT_FILENAME` — full filesystem path to the `.uce` file.
|
||||
- `DOCUMENT_ROOT` — public web root, normally `/var/www/html` or whatever nginx/Apache uses as `root`/`DocumentRoot`.
|
||||
- `SCRIPT_NAME` — URL path to the script, such as `/doc/index.uce`.
|
||||
- `DOCUMENT_URI` — normalized URI path without query string.
|
||||
- `REQUEST_URI` — original request URI including query string.
|
||||
- standard request variables such as method, query string, content type, body length, cookies, and headers.
|
||||
|
||||
UCE resolves the unit, compiles it to wasm if needed, creates a request workspace, and calls:
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
```
|
||||
|
||||
The unit writes output with template literals or `print()`. Response headers and status are set through `context.header` and `context.set_status()`.
|
||||
|
||||
### Component and sub-render calls
|
||||
|
||||
Inside a request, UCE code can call other units:
|
||||
|
||||
```cpp
|
||||
component("components/card", props, context);
|
||||
unit_render("other-page.uce", context);
|
||||
```
|
||||
|
||||
These calls stay inside the UCE runtime. They are not new HTTP requests and do not go back through nginx or Apache.
|
||||
|
||||
### WebSocket pages
|
||||
|
||||
Any `.uce` unit can provide both an ordinary page render and WebSocket message handling:
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context) { ... } // normal page load
|
||||
WS(Request& context) { ... } // later WebSocket messages
|
||||
```
|
||||
|
||||
The nginx and Apache examples below split traffic by checking for a WebSocket upgrade request on `.uce` paths. A file such as `chat.uce` or `events.uce` can expose `WS(Request& context)`.
|
||||
|
||||
Routing split:
|
||||
|
||||
- Plain `GET /demo/chat.uce` should use FastCGI, just like any other page render.
|
||||
- WebSocket upgrade requests for `/demo/chat.uce` should proxy to the UCE built-in HTTP/WebSocket listener at `HTTP_PORT`.
|
||||
|
||||
The built-in listener owns the socket lifecycle. When a message arrives, the broker forwards a render-style invocation back to the worker pool so `WS(Request& context)` runs inside the same wasm runtime model as normal pages.
|
||||
|
||||
### CLI requests
|
||||
|
||||
`CLI(Request& context)` handlers are not public web endpoints. They are invoked over `CLI_SOCKET_PATH`:
|
||||
|
||||
```bash
|
||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||
curl --unix-socket /run/uce/cli.sock http://localhost/tests/cli.uce
|
||||
```
|
||||
|
||||
Use CLI units for local tests, admin commands, and maintenance tools. Do not expose the CLI socket through nginx or Apache.
|
||||
|
||||
### Custom runtime HTTP servers
|
||||
|
||||
UCE code can start local custom HTTP listeners with `server_start_http()`. Those are runtime-managed listeners for app-specific local services. They are separate from the public nginx/Apache entry point and should be firewalled or bound locally unless you explicitly want them reachable.
|
||||
|
||||
## nginx configuration
|
||||
|
||||
### Required modules
|
||||
|
||||
A normal nginx build includes the needed FastCGI and proxy modules. Confirm nginx is installed and can load your config:
|
||||
|
||||
```bash
|
||||
nginx -t
|
||||
```
|
||||
|
||||
### WebSocket upgrade map
|
||||
|
||||
Put this in the nginx `http` block if using WebSockets:
|
||||
|
||||
```nginx
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
```
|
||||
|
||||
### Server block
|
||||
|
||||
Example site config:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
|
||||
root /var/www/html;
|
||||
index index.uce index.html;
|
||||
|
||||
# Serve static files directly. Directory requests use index.uce when present.
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# UCE page requests use FastCGI. If the client asks to upgrade a .uce
|
||||
# request to WebSocket, send that connection to the built-in listener.
|
||||
location ~ \.uce$ {
|
||||
error_page 418 = @uce_websocket;
|
||||
if ($http_upgrade = "websocket") {
|
||||
return 418;
|
||||
}
|
||||
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_param DOCUMENT_URI $uri;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
fastcgi_pass unix:/run/uce/fastcgi.sock;
|
||||
}
|
||||
|
||||
location @uce_websocket {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
}
|
||||
|
||||
# Defense in depth if the root is changed later.
|
||||
location ~ ^/(src|scripts|etc|bin|work|dist|pkg|docs|changelog)/ {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `fastcgi_pass` must match `FCGI_SOCKET_PATH`.
|
||||
- `proxy_pass` must match `HTTP_PORT`.
|
||||
- The example routes WebSocket upgrades for `.uce` paths to the HTTP/WebSocket listener.
|
||||
- The built-in HTTP/WebSocket listener resolves scripts from `HTTP_DOCUMENT_ROOT`; do not depend on client-supplied or proxied `Script-Filename` headers for routing.
|
||||
- Ordinary `.uce` page loads continue to use FastCGI.
|
||||
- Keep `root` pointed at `/var/www/html`, not the runtime repository root.
|
||||
- If your app uses a front controller, replace `location /` with a `try_files` rule that ends at `/index.uce`.
|
||||
|
||||
Front-controller variant:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri $uri/ /index.uce?$query_string;
|
||||
}
|
||||
```
|
||||
|
||||
Reload nginx:
|
||||
|
||||
```bash
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
```
|
||||
|
||||
## Apache configuration
|
||||
|
||||
Apache can run UCE through `mod_proxy_fcgi` for FastCGI and `mod_proxy_wstunnel` or `mod_proxy_http` for WebSocket upgrades.
|
||||
|
||||
### Enable modules
|
||||
|
||||
On Debian/Ubuntu:
|
||||
|
||||
```bash
|
||||
a2enmod proxy proxy_fcgi proxy_http proxy_wstunnel rewrite headers setenvif
|
||||
systemctl restart apache2
|
||||
```
|
||||
|
||||
### VirtualHost example
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName example.com
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
<Directory /var/www/html>
|
||||
Require all granted
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
DirectoryIndex index.uce index.html
|
||||
</Directory>
|
||||
|
||||
# Do not expose repository internals if DocumentRoot changes later.
|
||||
<LocationMatch "^/(src|scripts|etc|bin|work|dist|pkg|docs|changelog)/">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# WebSocket upgrade traffic for any .uce unit goes to UCE's built-in HTTP listener.
|
||||
RewriteCond %{HTTP:Upgrade} =websocket [NC]
|
||||
RewriteCond %{REQUEST_URI} \.uce(?:\?|$) [NC]
|
||||
RewriteRule ^/(.*)$ ws://127.0.0.1:8080/$1 [P,L]
|
||||
|
||||
# Normal .uce page loads go to FastCGI.
|
||||
<FilesMatch "\.uce$">
|
||||
SetHandler "proxy:unix:/run/uce/fastcgi.sock|fcgi://localhost/"
|
||||
</FilesMatch>
|
||||
|
||||
# Optional: make the key CGI variables explicit for UCE.
|
||||
ProxyFCGISetEnvIf "true" DOCUMENT_ROOT "/var/www/html"
|
||||
ProxyFCGISetEnvIf "true" DOCUMENT_URI "%{REQUEST_URI}"
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
Apache notes:
|
||||
|
||||
- `SetHandler "proxy:unix:/run/uce/fastcgi.sock|fcgi://localhost/"` must use the same socket path as `FCGI_SOCKET_PATH`.
|
||||
- The WebSocket rewrite rule must run before the FastCGI handler.
|
||||
- Plain `.uce` page loads should not be proxied as WebSockets unless the client sends `Upgrade: websocket`.
|
||||
- Apache's FastCGI environment differs by version and module configuration. If UCE cannot resolve a page, inspect the request environment and make sure `SCRIPT_FILENAME` points to the target file under the web root.
|
||||
|
||||
If your Apache version does not populate `SCRIPT_FILENAME` correctly through `SetHandler`, use `ProxyPassMatch` for `.uce` files instead:
|
||||
|
||||
```apache
|
||||
ProxyPassMatch ^/(.*\.uce)$ unix:/run/uce/fastcgi.sock|fcgi://localhost/var/www/html/$1
|
||||
```
|
||||
|
||||
Use only one FastCGI mapping style at a time (`SetHandler` or `ProxyPassMatch`) to avoid duplicate routing.
|
||||
|
||||
## Permissions
|
||||
|
||||
The web server needs permission to connect to `/run/uce/fastcgi.sock`. Common approaches:
|
||||
|
||||
- run UCE and the web server under compatible groups;
|
||||
- add the web server user (`www-data` on Debian/Ubuntu) to the socket's group;
|
||||
- adjust the service or runtime socket mode if needed.
|
||||
|
||||
The runtime creates the FastCGI socket and CLI socket under `/run/uce`. The CLI socket should remain local-only and should not be reachable from the public web server.
|
||||
|
||||
Writable paths for the runtime:
|
||||
|
||||
```text
|
||||
/var/cache/uce/work
|
||||
/var/lib/uce/uploads
|
||||
/var/lib/uce/sessions
|
||||
/run/uce
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Check service state:
|
||||
|
||||
```bash
|
||||
systemctl status uce.service
|
||||
journalctl -u uce.service -n 100 --no-pager
|
||||
```
|
||||
|
||||
Check the FastCGI/web-server path:
|
||||
|
||||
```bash
|
||||
curl -i http://127.0.0.1/doc/index.uce -H 'Host: example.com'
|
||||
curl -i http://127.0.0.1/examples/uce-starter/ -H 'Host: example.com'
|
||||
```
|
||||
|
||||
Check the local CLI path:
|
||||
|
||||
```bash
|
||||
cd /opt/uce
|
||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||
scripts/run_cli_tests.sh
|
||||
```
|
||||
|
||||
Check WebSocket routing with a WebSocket client against a `.uce` endpoint that defines `WS(Request& context)` through nginx/Apache, not directly against `HTTP_PORT`:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import base64, os, socket
|
||||
host = "example.com"
|
||||
path = "/chat.uce"
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
request = (
|
||||
f"GET {path} HTTP/1.1\r\n"
|
||||
f"Host: {host}\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n\r\n"
|
||||
).encode()
|
||||
sock = socket.create_connection(("127.0.0.1", 80), timeout=5)
|
||||
sock.sendall(request)
|
||||
print(sock.recv(4096).decode("latin1", "replace").split("\r\n", 1)[0])
|
||||
sock.close()
|
||||
PY
|
||||
```
|
||||
|
||||
## 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 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.
|
||||
- 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.
|
||||
- WASI SDK is a deployment/runtime dependency, not just a developer build tool. UCE compiles units to wasm on demand during requests and during proactive startup scans, so each host must use the pinned SDK version documented in `docs/wasi-sdk-toolchain.md`.
|
||||
- After toolchain or compile-script fixes, clear stale failed artifacts under `BIN_DIRECTORY`; otherwise a later request may report an old compile failure.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 502 Bad Gateway
|
||||
|
||||
Check:
|
||||
|
||||
- `systemctl status uce.service`
|
||||
- `journalctl -u uce.service -n 200 --no-pager`
|
||||
- socket path in web server config equals `FCGI_SOCKET_PATH`
|
||||
- web server user can connect to the Unix socket
|
||||
- `SCRIPT_FILENAME` resolves to an existing `.uce` file
|
||||
|
||||
### Raw `.uce` source is downloaded or displayed
|
||||
|
||||
The `.uce` request did not match the FastCGI rule. Check location/order rules and confirm the public root is `/var/www/html` or your chosen web-root path.
|
||||
|
||||
### Static files 404
|
||||
|
||||
Confirm the web server `root`/`DocumentRoot` is `/var/www/html` or your chosen web-root path and that `location /` or Apache directory rules allow static file reads.
|
||||
|
||||
### WebSocket page renders but upgrade fails
|
||||
|
||||
Check:
|
||||
|
||||
- the client sends `Upgrade: websocket`
|
||||
- `.uce` upgrade traffic reaches `HTTP_PORT`
|
||||
- nginx/Apache preserves `Upgrade` and `Connection` headers
|
||||
- `HTTP_DOCUMENT_ROOT` matches the web-server root; if it points at the runtime tree while files live in `/var/www/html`, the built-in listener will return `404 script not found`
|
||||
- firewall/network policy allows localhost access to `HTTP_PORT`
|
||||
|
||||
### Page compiles fail
|
||||
|
||||
Check the compile artifact paths shown in the UCE error response and service logs. Generated files and compile output live under `BIN_DIRECTORY`.
|
||||
|
||||
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`.
|
||||
- `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`, 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`.
|
||||
- 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 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.
|
||||
|
||||
### CLI commands fail
|
||||
|
||||
Check:
|
||||
|
||||
- `CLI_SOCKET_PATH` in `/etc/uce/settings.cfg`
|
||||
- `/run/uce/cli.sock` exists
|
||||
- `scripts/uce-cli --socket /run/uce/cli.sock /ping` works
|
||||
- the target unit defines `CLI(Request& context)`
|
||||
@@ -0,0 +1,55 @@
|
||||
# WASI SDK Toolchain Pin
|
||||
|
||||
UCE treats WASI SDK as a deployment/runtime dependency, not just a developer build tool.
|
||||
|
||||
The runtime compiles `.uce` units to wasm on demand during requests and during the proactive compiler scan. That means every deployment host must have the same compiler/linker toolchain available, and the generated `.wasm`/`.cwasm` artifacts are tied to that toolchain version and UCE unit ABI version.
|
||||
|
||||
## Current pin
|
||||
|
||||
- Upstream: <https://github.com/WebAssembly/wasi-sdk>
|
||||
- Release tag: `wasi-sdk-33`
|
||||
- Version: `33.0`
|
||||
- Linux x86_64 archive: `wasi-sdk-33.0-x86_64-linux.tar.gz`
|
||||
- URL: `https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-x86_64-linux.tar.gz`
|
||||
- SHA256: `0ba8b5bfaeb2adf3f29bab5841d76cf5318ab8e1642ea195f88baba1abd47bce`
|
||||
- Expected install symlink: `/opt/wasi-sdk`
|
||||
- Expected resolved path: `/opt/wasi-sdk-33.0-x86_64-linux`
|
||||
|
||||
Install or verify with:
|
||||
|
||||
```bash
|
||||
scripts/install_wasi_sdk.sh
|
||||
scripts/install_wasi_sdk.sh --check-only
|
||||
```
|
||||
|
||||
## Required tools
|
||||
|
||||
UCE expects these executables on each deployment host:
|
||||
|
||||
```text
|
||||
/opt/wasi-sdk/bin/clang++
|
||||
/opt/wasi-sdk/bin/wasm-ld
|
||||
/opt/wasi-sdk/bin/llvm-objcopy
|
||||
/opt/wasi-sdk/bin/llvm-nm
|
||||
```
|
||||
|
||||
`llvm-nm` is used by `scripts/check_unit_wasm.py`, which is called by `scripts/compile_wasm_unit` after linking each unit.
|
||||
|
||||
## Upgrade policy
|
||||
|
||||
Treat WASI SDK upgrades like runtime dependency upgrades:
|
||||
|
||||
1. Update `scripts/install_wasi_sdk.sh` version, URL, and SHA256.
|
||||
2. Record the new release and checksum here.
|
||||
3. Rebuild `bin/wasm/core.wasm` with `scripts/build_core_wasm.sh`.
|
||||
4. Rebuild the native runtime with `scripts/build_linux.sh`.
|
||||
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`.
|
||||
6. Run the full CLI suite including wasm kill tests:
|
||||
|
||||
```bash
|
||||
scripts/run_cli_tests.sh --include-wasm-kill
|
||||
```
|
||||
|
||||
## Known footgun
|
||||
|
||||
WASI SDK 33's `llvm-nm` was observed to crash on a degenerate but valid unit module with no exported handlers. `scripts/check_unit_wasm.py` treats that specific validator-tool crash as a skipped allocator-definition scan while still rejecting forbidden allocator exports and other ABI violations. This is one reason the toolchain is pinned instead of relying on whatever `/opt/wasi-sdk` happens to contain.
|
||||
@@ -0,0 +1,305 @@
|
||||
# UCE WASM Runtime Architecture
|
||||
|
||||
Status: current as of the W7e native-pipeline removal (June 2026). This document
|
||||
describes the **runtime architecture as built** — the process topology, the
|
||||
wasm membrane, the unified request dispatch, and the central WebSocket broker.
|
||||
Native `.so` unit execution/dlopen fallback has been removed; the parser and
|
||||
preprocessor remain only as the front-end that emits C++ for wasm side-module
|
||||
compilation.
|
||||
|
||||
The guiding principle: **request code never shares an address space or an
|
||||
allocator with the runtime.** Every unit runs as a WebAssembly module inside a
|
||||
per-request workspace behind a narrow host membrane. Long-lived connection
|
||||
state (WebSocket connections, listening HTTP sockets) lives in dedicated native
|
||||
broker processes that own no unit code — they hold the connection and forward
|
||||
the actual unit invocation to a clean-engine worker, exactly like a normal
|
||||
page render.
|
||||
|
||||
---
|
||||
|
||||
## 1. Process topology
|
||||
|
||||
A single native binary (`uce_fastcgi`) forks into a small set of long-lived
|
||||
roles. None of them is special-cased per request mode; the differences are
|
||||
purely *which socket a process listens on* and *which function inside a unit
|
||||
gets invoked*.
|
||||
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
nginx ──FastCGI──► worker pool (N processes) │ /run/uce.sock
|
||||
(port 80 etc.) │ uniform unit renderers │ (FastCGI + CLI)
|
||||
└─────────────▲──────────────┘
|
||||
│ forward render (FastCGI, uce.sock)
|
||||
│
|
||||
browser ──raw HTTP / WS──► ┌────────┴─────────┐
|
||||
(HTTP_PORT 8080) │ WS broker │ owns HTTP_PORT + every
|
||||
│ (1 process) │ WS connection; renders
|
||||
└────────▲─────────┘ nothing itself
|
||||
│ ws_* command flush
|
||||
│ (FastCGI, ws-broker.sock)
|
||||
on-demand serve_http ──► ┌────────┴─────────┐
|
||||
(per bind addr) │ custom-server │ owns one serve_http
|
||||
│ dispatcher(s) │ bind addr; forwards to pool
|
||||
└──────────────────┘
|
||||
|
||||
parent process: spawns/respawns all of the above + the proactive compiler.
|
||||
```
|
||||
|
||||
| Process | Owns | Renders units? | Source |
|
||||
|---|---|---|---|
|
||||
| **Parent** | nothing; supervises children | no | `main()`, `init_base_process()` |
|
||||
| **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()` |
|
||||
| **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()` |
|
||||
|
||||
**only workers instantiate Wasmtime and run unit code.**
|
||||
Every connection-owning process (broker, serve_http dispatcher) forwards the
|
||||
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
|
||||
forced by Wasmtime: an `Engine`/`Store` cannot be safely re-created across
|
||||
`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
|
||||
events the same way they respond to a page request — through a clean worker.
|
||||
|
||||
---
|
||||
|
||||
## 2. The membrane and the DValue ABI
|
||||
|
||||
Units are compiled to WebAssembly and linked against a host import surface (the
|
||||
"membrane"). Host and guest exchange structured values as **UCEB** (a compact
|
||||
binary `DValue` encoding — `ucb_encode`/`ucb_decode`); strings cross as
|
||||
`std::string` on the native side throughout (no raw `char*` ownership across
|
||||
the boundary).
|
||||
|
||||
- `DValue` is the universal value type: scalars plus an ordered child map
|
||||
(`_map`, a `std::map<String,DValue>`). `operator[]` is non-const (creates);
|
||||
`.key(k)` is the const probe returning `const DValue*`; `.each(fn)` iterates
|
||||
children yielding `const DValue&`.
|
||||
- The request context (`params`/`get`/`post`/`cookies`/`session`, the raw body
|
||||
`in`, and—for WS—the connection context) is marshalled into a single `ctx`
|
||||
DValue, UCEB-encoded, and handed to the workspace. The response (body,
|
||||
headers, status, and any `meta` such as `ws_commands`) comes back the same
|
||||
way.
|
||||
|
||||
See [`docs/wasm-phase1-dvalue-abi.md`](wasm-phase1-dvalue-abi.md) for the wire
|
||||
format details.
|
||||
|
||||
---
|
||||
|
||||
## 3. Units, handlers, and export naming
|
||||
|
||||
A `.uce` unit compiles to a wasm module that exports one symbol per **handler**.
|
||||
There is no per-mode machinery — a "CLI unit", "WebSocket unit", and "page" are
|
||||
the same compiled artifact invoked at different exports. The dispatcher passes a
|
||||
**handler string**; the host maps it to an export symbol:
|
||||
|
||||
```
|
||||
__uce_<base>[_<sanitize(suffix)>]
|
||||
```
|
||||
|
||||
`handler_export_symbol()` (`src/wasm/worker.cpp`) splits on the first `:`:
|
||||
|
||||
| Handler string | Export symbol |
|
||||
|---|---|
|
||||
| `render` | `__uce_render` |
|
||||
| `cli` | `__uce_cli` |
|
||||
| `websocket` | `__uce_websocket` |
|
||||
| `once` | `__uce_once` (optional; absence is not an error) |
|
||||
| `serve_http` | `__uce_serve_http` |
|
||||
| `serve_http:named` | `__uce_serve_http_named` |
|
||||
| `component:CARD` | `__uce_component_CARD` |
|
||||
| `exists` | probe only — resolves the unit, loads nothing |
|
||||
|
||||
`sanitize_symbol_suffix()` keeps `[A-Za-z0-9_]` (mirrors `ascii_safe_name`).
|
||||
`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
|
||||
without instantiating it.
|
||||
|
||||
`wasm_backend_should_handle(request, entry_unit)` checks whether the wasm
|
||||
backend is initialized and the requested artifact/handler is currently
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 4. The workspace runtime
|
||||
|
||||
Each request gets a fresh **workspace** — a per-request wasm instance tree with
|
||||
the membrane wired in. `wasm_worker_serve(worker, ctx, entry_unit, handler)`
|
||||
(`src/wasm/worker.cpp`) is the single entry point for *every* mode:
|
||||
|
||||
1. Birth a workspace (CoW-snapshot-based where available).
|
||||
2. Resolve `entry_unit` + `handler` to an export; components referenced at
|
||||
runtime are resolved on demand via the `uce_host_component_resolve` hostcall
|
||||
(`component_resolve()` → `__uce_<...>` slot), loading dependency modules
|
||||
lazily and recording resolve counts/timings.
|
||||
3. Invoke the export. Unit code calls host functions (filesystem, sqlite, regex,
|
||||
markdown, time, tasks, `ws_*`, …) across the membrane.
|
||||
4. Collect the response (`WasmResponse`: body, headers, status, `meta`).
|
||||
|
||||
Because the workspace owns no process-lifetime state, a unit crash or trap is
|
||||
contained: it fails the one request and the worker stays healthy. Workers
|
||||
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
|
||||
`serve_via_wasm` in `handle_complete`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Request dispatch (`handle_complete`)
|
||||
|
||||
`handle_complete()` (`src/linux_fastcgi.cpp`) is the worker's single dispatch
|
||||
point. It selects a handler string from request params and calls the same
|
||||
`serve_via_wasm(entry_unit, handler)` lambda for all of them:
|
||||
|
||||
```
|
||||
UCE_WS == "1" → serve_via_wasm(entry_unit, "websocket")
|
||||
request.resources.is_cli → serve_via_wasm(entry_unit, "cli")
|
||||
UCE_SERVE_HTTP == "1" → serve_via_wasm(entry_unit, "serve_http"[:fn])
|
||||
otherwise (page) → serve_via_wasm(entry_unit, "render")
|
||||
```
|
||||
|
||||
The `UCE_*` params are set by whichever broker forwarded the request:
|
||||
|
||||
- **Page render**: FastCGI nginx → `/run/uce.sock` directly; no `UCE_*` flags → `render`.
|
||||
- **CLI**: the CLI socket sets `is_cli`.
|
||||
- **serve_http**: the custom-server dispatcher sets `UCE_SERVE_HTTP=1` plus
|
||||
`UCE_SERVE_HTTP_FUNCTION` and rewrites `SCRIPT_FILENAME` to the configured
|
||||
unit (`custom_server_http_complete`).
|
||||
- **WebSocket**: the WS broker sets `UCE_WS=1` and carries the connection
|
||||
identity as `UCE_WS_*` params (see §6). `handle_complete` rebuilds
|
||||
`request.resources.websocket_*` and `request.connection` from them before
|
||||
invoking `__uce_websocket`.
|
||||
|
||||
If a unit still cannot be served after the on-demand wasm compile, the worker
|
||||
returns a clean 500 with the wasm/compile error; it does not execute native unit
|
||||
code.
|
||||
|
||||
---
|
||||
|
||||
## 6. The central WebSocket broker
|
||||
|
||||
**One process owns the HTTP port
|
||||
and every WebSocket connection**, so any unit's `ws_*` call can reach any or all
|
||||
connections, and a unit-code crash (which happens in a worker) never drops live
|
||||
connections.
|
||||
|
||||
### 6.1 Inbound: a WS frame → a worker render (non-blocking)
|
||||
|
||||
`ws_broker_ws_message(request, message, opcode)` fires when a complete
|
||||
(reassembled) WS message arrives on a connection. It does **not** block the
|
||||
broker loop:
|
||||
|
||||
1. Build FastCGI params: `SCRIPT_FILENAME`, `REQUEST_METHOD=GET`, a
|
||||
`REQUEST_URI` (required — `handle_request()` rejects requests without one
|
||||
*before* `on_complete` runs), `UCE_WS=1`, and the connection context as
|
||||
`UCE_WS_CONNECTION_ID / SCOPE / OPCODE / BINARY / CONNECTIONS / STATE`.
|
||||
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
|
||||
before `on_complete` ever runs.
|
||||
3. Connect to `/run/uce.sock` (non-blocking) and queue the encoded request in
|
||||
`ws_broker_outbound[fd]`.
|
||||
|
||||
`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
|
||||
output comes back via the command socket, not this reply), closing the fd when
|
||||
the worker closes its end.
|
||||
|
||||
### 6.2 Outbound: `ws_*` commands flushed back to the broker
|
||||
|
||||
Any unit code — not just WebSocket handlers — may call `ws_send` / `ws_send_to`
|
||||
/ `ws_close`. In the workspace these **record dispatch commands** rather than
|
||||
touching a socket (the workspace owns no connections); `wasm-core`'s `ws_*`
|
||||
(`src/lib/sys.cpp`) append to `websocket_dispatch_commands`, and
|
||||
`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands` (plus
|
||||
`ws_connection_state` if the handler mutated per-connection state).
|
||||
|
||||
`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
|
||||
socket (`WS_BROKER_SOCKET_PATH`, `/run/uce/ws-broker.sock`) via
|
||||
`fcgi_forward_request` with `UCE_WS_DISPATCH=1`. This is the only path WS data
|
||||
takes out of a workspace.
|
||||
|
||||
`ws_broker_apply_commands()` decodes the batch and applies each command against
|
||||
the full registry it owns: `broadcast` (by scope), `send_to` (by connection id),
|
||||
`close`. If the batch carries `connection_state`, it persists that onto the
|
||||
matching live connection's `websocket_state`.
|
||||
|
||||
### 6.3 Un-upgraded HTTP on the WS port
|
||||
|
||||
The WS port can also receive ordinary (non-Upgrade) HTTP requests.
|
||||
`ws_broker_complete()` routes by param: `UCE_WS_DISPATCH=1` → apply commands;
|
||||
otherwise → `forward_request_to_worker()` — the *same* shared facility the
|
||||
serve_http dispatcher uses, so there is no duplicated request-forwarding code.
|
||||
|
||||
### 6.4 The broker loop
|
||||
|
||||
`run_ws_broker()` drops the worker listeners it inherited
|
||||
(`close_inherited_server_sockets`), installs permissive `on_request`/`on_data`
|
||||
handlers (it renders nothing, so it accepts every request straight through to
|
||||
`on_complete`), wires `on_complete=ws_broker_complete` and
|
||||
`on_websocket_message=ws_broker_ws_message`, listens on `HTTP_PORT` and the
|
||||
command socket, and loops `process(50)` + `drain_outbound()`. The design is
|
||||
**non-blocking outbound dispatch + async command-socket flush, all in 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`
|
||||
in `main()`).
|
||||
|
||||
---
|
||||
|
||||
## 7. The serve_http facility
|
||||
|
||||
`serve_http` units are reachable on their own bind address via a custom-server
|
||||
dispatcher (`custom_server_http_dispatcher_loop`). The dispatcher owns the bind
|
||||
socket and, on each request, sets `UCE_SERVE_HTTP=1` + the configured unit/
|
||||
function and calls `forward_request_to_worker()`. The worker then runs
|
||||
`serve_via_wasm(entry_unit, "serve_http"[:fn])`. This is the same
|
||||
hold-connection-forward-render model as the WS broker, sharing
|
||||
`forward_request_to_worker` and `fcgi_forward.h`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Build / object layout
|
||||
|
||||
The native side is split into separately-compiled objects so editing the wasm
|
||||
runtime does not recompile the whole TU (`scripts/build_linux.sh`):
|
||||
|
||||
| Object | Contents |
|
||||
|---|---|
|
||||
| `bin/sqlite3.o` | sqlite amalgamation (cached) |
|
||||
| `bin/wasm.o` | `src/wasm/wasm_module.cpp` (backend.cpp + worker.cpp + wasmtime) |
|
||||
| `bin/main.o` | `src/linux_fastcgi.cpp` (includes `lib/uce_lib.cpp`) |
|
||||
|
||||
Linked into one `-rdynamic` binary. ODR hazards across the objects are handled
|
||||
deliberately: `context` and `my_pid`/`parent_pid` are `extern` (guarded for the
|
||||
wasm core vs. unit builds), `operator new`/`delete` live in `types.cpp`, and
|
||||
header free-functions are `inline`. The wasm backend exposes only declarations
|
||||
(`src/wasm/backend.h`) to `main.o`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Configuration keys
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `WASM_BACKEND_VERBOSE` | `0` | Emit `X-UCE-Wasm-*` workspace timing headers (benchmark only). |
|
||||
| `FCGI_SOCKET_PATH` | `/run/uce.sock` | Worker pool FastCGI socket (brokers forward here). |
|
||||
| `CLI_SOCKET_PATH` | `/run/uce/cli.sock` | Worker CLI socket. |
|
||||
| `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. |
|
||||
| `WORKER_COUNT` | `4` | Number of uniform worker processes. |
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing
|
||||
|
||||
- **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.
|
||||
|
||||
- **WebSocket end-to-end**: a headless client performs a raw WS handshake to
|
||||
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
|
||||
`SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full
|
||||
broker → worker → broker → client chain across process boundaries.
|
||||
|
||||
|
||||
+48
-2
@@ -7,6 +7,14 @@ SESSION_PATH=/var/lib/uce/sessions
|
||||
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||
FCGI_PORT=9993
|
||||
|
||||
# HTTP-over-Unix command socket for local CLI/control tooling.
|
||||
# Example: curl --unix-socket /run/uce/cli.sock http://localhost/ping
|
||||
CLI_SOCKET_PATH=/run/uce/cli.sock
|
||||
|
||||
# Built-in HTTP/WebSocket listener used for WebSocket Upgrade requests.
|
||||
# Keep this behind nginx/Apache on localhost or firewall it from public access.
|
||||
HTTP_PORT=8080
|
||||
|
||||
# OPTIONAL PROACTIVE COMPILE ROOT
|
||||
# Leave empty to scan SITE_DIRECTORY relative to the runtime root.
|
||||
PRECOMPILE_FILES_IN=
|
||||
@@ -17,11 +25,21 @@ SITE_DIRECTORY=site
|
||||
# ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT
|
||||
JIT_COMPILE_ON_REQUEST=1
|
||||
|
||||
# WASM SIDE-MODULE COMPILER USED FOR .uce UNITS
|
||||
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
|
||||
|
||||
# WASM RUNTIME SETTINGS. Unit execution is always routed through wasm.
|
||||
WASM_BACKEND_VERBOSE=0
|
||||
WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm
|
||||
WASM_MEMORY_LIMIT_BYTES=536870912
|
||||
WASM_EPOCH_DEADLINE_TICKS=200
|
||||
WASM_EPOCH_PERIOD_MS=50
|
||||
|
||||
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
|
||||
PROACTIVE_COMPILE_ENABLED=1
|
||||
|
||||
# AFTER A FAILED COMPILE, SERVE THE LAST COMPILER OUTPUT AND WAIT THIS MANY SECONDS
|
||||
# BEFORE TRYING AGAIN IF THE SOURCE FILE HAS NOT CHANGED
|
||||
# AFTER A FAILED COMPILE, UCE NOW REUSES THE PERSISTED COMPILER OUTPUT UNTIL
|
||||
# THE SOURCE OR COMPILER INPUTS CHANGE. THIS SETTING IS KEPT FOR COMPATIBILITY.
|
||||
COMPILE_FAILURE_RETRY_SECONDS=10
|
||||
|
||||
# PERIODIC KNOWN-.uce RECHECK INTERVAL IN SECONDS
|
||||
@@ -33,5 +51,33 @@ WORKER_COUNT=4
|
||||
# MAX MEMORY PER REQUEST
|
||||
MAX_MEMORY=16777216
|
||||
|
||||
# TRANSPORT / DOS LIMITS
|
||||
TRANSPORT_MAX_CLIENT_CONNECTIONS=256
|
||||
TRANSPORT_MAX_HTTP_HEADER_BYTES=16384
|
||||
TRANSPORT_MAX_HTTP_BODY_BYTES=1048576
|
||||
TRANSPORT_MAX_WEBSOCKET_FRAME_BYTES=1048576
|
||||
TRANSPORT_MAX_WEBSOCKET_MESSAGE_BYTES=1048576
|
||||
TRANSPORT_MAX_WEBSOCKET_OUTPUT_BYTES=4194304
|
||||
TRANSPORT_MAX_RESPONSE_BYTES=8388608
|
||||
TRANSPORT_HTTP_REQUEST_TIMEOUT_SECONDS=15
|
||||
TRANSPORT_CONNECTION_IDLE_TIMEOUT_SECONDS=120
|
||||
# Empty means COMPILER_SYS_PATH for the built-in direct HTTP listener.
|
||||
HTTP_DOCUMENT_ROOT=
|
||||
|
||||
# CUSTOM SERVER LIMITS
|
||||
CUSTOM_SERVER_MAX_SERVERS=16
|
||||
CUSTOM_SERVER_MIN_PORT=1024
|
||||
CUSTOM_SERVER_MAX_PORT=65535
|
||||
CUSTOM_SERVER_ALLOW_PUBLIC_BIND=0
|
||||
CUSTOM_SERVER_UNIX_SOCKET_PREFIX=/tmp/uce/custom-servers/
|
||||
CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS=30
|
||||
# Empty means COMPILER_SYS_PATH/SITE_DIRECTORY.
|
||||
CUSTOM_SERVER_UCE_ROOT=
|
||||
|
||||
# ARCHIVE HELPER LIMITS
|
||||
ARCHIVE_MAX_INPUT_BYTES=67108864
|
||||
ARCHIVE_MAX_OUTPUT_BYTES=67108864
|
||||
ARCHIVE_MAX_ZIP_ENTRIES=4096
|
||||
|
||||
# LIFETIME OF SESSION COOKIES IN SECONDS
|
||||
SESSION_TIME=2592000
|
||||
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guard the hand-maintained unit-facing API coverage manifest.
|
||||
|
||||
This intentionally avoids network/external services. It checks that public API
|
||||
names we expose to wasm units are either mentioned by a site test or explicitly
|
||||
marked internal/integration-only, and that active docs exist for doc-required
|
||||
APIs. The manifest is deliberately source-controlled so a new public function
|
||||
requires an explicit coverage decision.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TEST_DIR = ROOT / "site" / "tests"
|
||||
DOC_DIR = ROOT / "site" / "doc" / "pages"
|
||||
|
||||
# name, needs_doc, status. status: public | internal | integration
|
||||
PUBLIC_APIS = [
|
||||
("shell_exec", True, "public"), ("shell_escape", True, "public"),
|
||||
("basename", True, "public"), ("dirname", True, "public"), ("path_join", True, "public"),
|
||||
("path_real", True, "public"), ("path_is_within", True, "public"),
|
||||
("file_open_locked", True, "public"), ("file_close_locked", 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"),
|
||||
("file_mtime", True, "public"), ("file_unlink", True, "public"), ("expand_path", True, "public"),
|
||||
("ls", True, "public"), ("config_map_u64", True, "public"), ("config_map_f64", 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"),
|
||||
("time_format_relative", True, "public"), ("time_parse", True, "public"),
|
||||
("backtrace_frames_string", False, "public"), ("capture_backtrace_string", False, "public"),
|
||||
("signal_name", False, "public"), ("memcache_escape_key", True, "public"),
|
||||
("memcache_escape_keys", True, "public"), ("memcache_command", True, "public"),
|
||||
("memcache_get_multiple", True, "public"), ("runtime_safe_key", True, "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"),
|
||||
("to_json", False, "public"), ("remove", False, "public"), ("clear", False, "public"),
|
||||
("gen_sha1", True, "public"), ("gen_noise32", True, "public"), ("gen_noise64", True, "public"),
|
||||
("gen_noise01", True, "public"), ("gen_int", True, "public"), ("gen_float", True, "public"),
|
||||
("draw_int", True, "public"), ("draw_float", True, "public"),
|
||||
("encode_query", True, "public"), ("request_script_url", True, "public"),
|
||||
("request_base_url", True, "public"), ("request_route_from_raw_path", True, "public"),
|
||||
("cli_arg", True, "public"), ("unit_compile", True, "public"),
|
||||
("cleanup_sqlite_connections", False, "internal"), ("cleanup_mysql_connections", False, "internal"),
|
||||
("mysql_connect", True, "integration"), ("mysql_query", True, "integration"),
|
||||
]
|
||||
|
||||
REMOVED_APIS = ["unit_load", "concat"]
|
||||
|
||||
|
||||
def all_test_text() -> str:
|
||||
parts = []
|
||||
for path in TEST_DIR.glob("*.uce"):
|
||||
parts.append(path.read_text(errors="ignore"))
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def doc_exists(name: str) -> bool:
|
||||
path = DOC_DIR / f"{name}.txt"
|
||||
return path.exists() and "Removed" not in path.read_text(errors="ignore")[:200]
|
||||
|
||||
|
||||
def has_call(text: str, name: str) -> bool:
|
||||
return f"{name}(" in text or f".{name}(" in text or f'"{name}"' in text
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tests = all_test_text()
|
||||
errors = []
|
||||
for name, needs_doc, status in PUBLIC_APIS:
|
||||
if status == "public" and not has_call(tests, name):
|
||||
errors.append(f"missing test coverage: {name}")
|
||||
if needs_doc and status in {"public", "integration"} and not doc_exists(name):
|
||||
errors.append(f"missing active doc page: {name}")
|
||||
compiler_h = (ROOT / "src" / "lib" / "compiler.h").read_text(errors="ignore")
|
||||
if "#ifndef __UCE_WASM_UNIT__\nSharedUnit* unit_load" not in compiler_h:
|
||||
errors.append("unit_load is not guarded out of wasm-unit exposure")
|
||||
for name in REMOVED_APIS:
|
||||
page = DOC_DIR / f"{name}.txt"
|
||||
if name == "concat" and page.exists() and "Removed" not in page.read_text(errors="ignore")[:300]:
|
||||
errors.append("concat doc is not tombstoned")
|
||||
if name == "unit_load" and page.exists() and "native-only" not in page.read_text(errors="ignore"):
|
||||
errors.append("unit_load doc is not native-only/tombstoned")
|
||||
if errors:
|
||||
print("API coverage manifest FAILED")
|
||||
for error in errors:
|
||||
print("- " + error)
|
||||
return 1
|
||||
print(f"API coverage manifest ok: {len(PUBLIC_APIS)} entries checked")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Build the production W1 UCE WASM core from the production runtime carve-out.
|
||||
# Run on k-uce from any working directory.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SDK=${WASI_SDK:-/opt/wasi-sdk}
|
||||
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w1}
|
||||
mkdir -p "$OUT" bin/wasm
|
||||
|
||||
if [ ! -x "$SDK/bin/clang++" ]; then
|
||||
echo "wasi-sdk clang++ not found; set WASI_SDK" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
|
||||
-O1 -g -std=c++20 -fno-exceptions -fno-rtti \
|
||||
-D__UCE_WASM_CORE__ \
|
||||
-I. -Isrc/lib \
|
||||
src/wasm/core.cpp -o "$OUT/core.wasm" \
|
||||
-Wl,--export-all \
|
||||
-Wl,--export=__heap_base \
|
||||
-Wl,--export=__stack_pointer \
|
||||
-Wl,--import-table \
|
||||
-Wl,-z,stack-size=8388608 \
|
||||
-Wl,--allow-undefined-file=src/wasm/core_hostcalls.syms \
|
||||
-Wl,--no-entry \
|
||||
$(sed "s/^/-Wl,--export-if-defined=/" src/wasm/core_libc_exports.syms | tr "\n" " ")
|
||||
|
||||
cp "$OUT/core.wasm" bin/wasm/core.wasm
|
||||
ls -lh "$OUT/core.wasm" bin/wasm/core.wasm
|
||||
+69
-4
@@ -9,16 +9,81 @@ GF="uce_fastcgi"
|
||||
mkdir bin > /dev/null 2>&1
|
||||
mkdir bin/tmp > /dev/null 2>&1
|
||||
mkdir bin/assets > /dev/null 2>&1
|
||||
mkdir bin/wasm > /dev/null 2>&1
|
||||
mkdir work > /dev/null 2>&1
|
||||
|
||||
COMPILER="clang++"
|
||||
FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
|
||||
# -rdynamic is a link-time flag; the -c compiles below do not need it.
|
||||
FLAGS="-g -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
|
||||
|
||||
LIBS="-ldl -lm -lpthread `mysql_config --cflags --libs`"
|
||||
# Wasmtime C++ API — needed only by the wasm backend object (src/wasm).
|
||||
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
|
||||
WASM_FLAGS="-I$WASMTIME_HOME/include"
|
||||
WASM_LIBS="-L$WASMTIME_HOME/lib -Wl,-rpath,$WASMTIME_HOME/lib -lwasmtime"
|
||||
|
||||
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs` $WASM_LIBS"
|
||||
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
|
||||
|
||||
echo "Compliling executable..."
|
||||
time -p $COMPILER src/linux_fastcgi.cpp $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
|
||||
# The runtime is split into separately-compiled objects so an edit to one
|
||||
# module no longer recompiles the others (notably: editing the wasm backend
|
||||
# does not recompile the rest, and vendored SQLite is built once):
|
||||
# bin/sqlite3.o vendored SQLite amalgamation (depends only on its own source)
|
||||
# bin/wasm.o wasm backend + worker + wasmtime.hh (src/wasm)
|
||||
# bin/main.o linux_fastcgi.cpp + the uce_lib core amalgamation
|
||||
# All link into the single -rdynamic binary. Delete bin/*.o to force a clean
|
||||
# rebuild.
|
||||
|
||||
# Rebuild $1 if it is missing or anything under the remaining find-args is newer.
|
||||
needs_rebuild() {
|
||||
local obj="$1"; shift
|
||||
[ ! -f "$obj" ] && return 0
|
||||
[ -n "$(find "$@" -newer "$obj" -print -quit 2>/dev/null)" ] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# core.wasm: guest runtime loaded by the native wasm backend.
|
||||
if needs_rebuild bin/wasm/core.wasm src/wasm/core.cpp src/lib src/wasm/core_hostcalls.syms src/wasm/core_libc_exports.syms scripts/build_core_wasm.sh; then
|
||||
echo "Compiling wasm core..."
|
||||
bash scripts/build_core_wasm.sh || exit 1
|
||||
else
|
||||
echo "Reusing bin/wasm/core.wasm"
|
||||
fi
|
||||
|
||||
# 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
|
||||
echo "Compiling SQLite..."
|
||||
clang -g -O2 -fPIC \
|
||||
-DSQLITE_THREADSAFE=1 \
|
||||
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
|
||||
-DSQLITE_DQS=0 \
|
||||
-DSQLITE_DEFAULT_FOREIGN_KEYS=1 \
|
||||
-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \
|
||||
-c src/3rdparty/sqlite/sqlite3.c -o bin/sqlite3.o 2>&1 || exit 1
|
||||
else
|
||||
echo "Reusing bin/sqlite3.o"
|
||||
fi
|
||||
|
||||
# wasm backend object: the wasm sources plus the lib headers it includes for
|
||||
# declarations (not the lib .cpp — those are compiled into main.o).
|
||||
if needs_rebuild bin/wasm.o src/wasm src/lib/*.h; then
|
||||
echo "Compiling wasm backend..."
|
||||
time -p $COMPILER -c src/wasm/wasm_module.cpp $SRCFLAGS $FLAGS $WASM_FLAGS -o bin/wasm.o 2>&1 || exit 1
|
||||
else
|
||||
echo "Reusing bin/wasm.o"
|
||||
fi
|
||||
|
||||
# main object: the FastCGI entrypoint + the uce_lib core amalgamation. Depends
|
||||
# on linux_fastcgi.cpp, the whole lib tree, fcgicc, and the wasm backend header
|
||||
# (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
|
||||
echo "Compiling main..."
|
||||
time -p $COMPILER -c src/linux_fastcgi.cpp $SRCFLAGS $FLAGS -o bin/main.o 2>&1 || exit 1
|
||||
else
|
||||
echo "Reusing bin/main.o"
|
||||
fi
|
||||
|
||||
echo "Linking..."
|
||||
$COMPILER -rdynamic bin/main.o bin/wasm.o bin/sqlite3.o $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
|
||||
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
|
||||
Executable
+235
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a W2 UCE PIC unit wasm artifact.
|
||||
|
||||
Checks intentionally stay small and explicit: section walk for dylink.0,
|
||||
uce.abi, imports/exports, plus llvm-nm for allocator definitions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_u32leb(data: bytes, pos: int) -> tuple[int, int]:
|
||||
result = 0
|
||||
shift = 0
|
||||
while True:
|
||||
if pos >= len(data):
|
||||
raise ValueError("truncated leb128")
|
||||
b = data[pos]
|
||||
pos += 1
|
||||
result |= (b & 0x7F) << shift
|
||||
if (b & 0x80) == 0:
|
||||
return result, pos
|
||||
shift += 7
|
||||
if shift > 35:
|
||||
raise ValueError("oversized leb128")
|
||||
|
||||
|
||||
def read_name(data: bytes, pos: int) -> tuple[str, int]:
|
||||
n, pos = read_u32leb(data, pos)
|
||||
end = pos + n
|
||||
if end > len(data):
|
||||
raise ValueError("truncated name")
|
||||
return data[pos:end].decode("utf-8", "replace"), end
|
||||
|
||||
|
||||
def walk_sections(data: bytes):
|
||||
if not data.startswith(b"\0asm\x01\0\0\0"):
|
||||
raise ValueError("not a wasm v1 module")
|
||||
pos = 8
|
||||
while pos < len(data):
|
||||
section_id = data[pos]
|
||||
pos += 1
|
||||
size, pos = read_u32leb(data, pos)
|
||||
end = pos + size
|
||||
if end > len(data):
|
||||
raise ValueError("section extends past EOF")
|
||||
payload = data[pos:end]
|
||||
yield section_id, payload
|
||||
pos = end
|
||||
|
||||
|
||||
def parse_imports(payload: bytes):
|
||||
pos = 0
|
||||
count, pos = read_u32leb(payload, pos)
|
||||
imports = []
|
||||
for _ in range(count):
|
||||
module, pos = read_name(payload, pos)
|
||||
name, pos = read_name(payload, pos)
|
||||
if pos >= len(payload):
|
||||
raise ValueError("truncated import kind")
|
||||
kind = payload[pos]
|
||||
pos += 1
|
||||
# Skip type descriptors. We only need module/name/kind for W2 policy.
|
||||
if kind == 0: # func type index
|
||||
_, pos = read_u32leb(payload, pos)
|
||||
elif kind == 1: # table
|
||||
if pos >= len(payload): raise ValueError("truncated table import")
|
||||
pos += 1
|
||||
flags, pos = read_u32leb(payload, pos)
|
||||
_, pos = read_u32leb(payload, pos)
|
||||
if flags & 1: _, pos = read_u32leb(payload, pos)
|
||||
elif kind == 2: # memory
|
||||
flags, pos = read_u32leb(payload, pos)
|
||||
_, pos = read_u32leb(payload, pos)
|
||||
if flags & 1: _, pos = read_u32leb(payload, pos)
|
||||
elif kind == 3: # global
|
||||
pos += 2
|
||||
else:
|
||||
raise ValueError(f"unknown import kind {kind}")
|
||||
imports.append((module, name, kind))
|
||||
return imports
|
||||
|
||||
|
||||
def parse_exports(payload: bytes):
|
||||
pos = 0
|
||||
count, pos = read_u32leb(payload, pos)
|
||||
exports = []
|
||||
for _ in range(count):
|
||||
name, pos = read_name(payload, pos)
|
||||
if pos >= len(payload):
|
||||
raise ValueError("truncated export kind")
|
||||
kind = payload[pos]
|
||||
pos += 1
|
||||
_, pos = read_u32leb(payload, pos)
|
||||
exports.append((name, kind))
|
||||
return exports
|
||||
|
||||
|
||||
def collect(path: Path):
|
||||
data = path.read_bytes()
|
||||
customs: dict[str, list[bytes]] = {}
|
||||
imports = []
|
||||
exports = []
|
||||
for section_id, payload in walk_sections(data):
|
||||
if section_id == 0:
|
||||
name, pos = read_name(payload, 0)
|
||||
customs.setdefault(name, []).append(payload[pos:])
|
||||
elif section_id == 2:
|
||||
imports = parse_imports(payload)
|
||||
elif section_id == 7:
|
||||
exports = parse_exports(payload)
|
||||
return customs, imports, exports
|
||||
|
||||
|
||||
def dylink_has_valid_mem_info(payload: bytes) -> bool:
|
||||
pos = 0
|
||||
while pos < len(payload):
|
||||
subsection_id = payload[pos]
|
||||
pos += 1
|
||||
size, pos = read_u32leb(payload, pos)
|
||||
end = pos + size
|
||||
if end > len(payload):
|
||||
raise ValueError("dylink.0 subsection extends past section")
|
||||
if subsection_id == 1:
|
||||
mem_size, p = read_u32leb(payload, pos)
|
||||
mem_align, p = read_u32leb(payload, p)
|
||||
table_size, p = read_u32leb(payload, p)
|
||||
table_align, p = read_u32leb(payload, p)
|
||||
if p > end:
|
||||
raise ValueError("truncated dylink.0 mem_info")
|
||||
return mem_align < 32 and table_align < 32 and mem_size < (1 << 31) and table_size < (1 << 31)
|
||||
pos = end
|
||||
return False
|
||||
|
||||
|
||||
def defined_symbols(path: Path, llvm_nm: str) -> list[str]:
|
||||
proc = subprocess.run([llvm_nm, "--defined-only", str(path)], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if proc.returncode != 0:
|
||||
# llvm-nm (wasi-sdk) can SIGSEGV on degenerate-but-valid modules — e.g. a
|
||||
# unit with no exported handlers (`empty.uce`). A toolchain crash is not
|
||||
# evidence of a forbidden allocator, so skip this defense-in-depth scan
|
||||
# rather than fail the unit; forbidden allocator *exports* are still
|
||||
# rejected by the export-section check above.
|
||||
crashed = proc.returncode < 0 or "Stack dump:" in proc.stderr or "PLEASE submit a bug report" in proc.stderr
|
||||
if crashed:
|
||||
return []
|
||||
raise RuntimeError(proc.stderr.strip() or "llvm-nm failed")
|
||||
symbols = []
|
||||
for line in proc.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if parts:
|
||||
symbols.append(parts[-1])
|
||||
return symbols
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("wasm", type=Path)
|
||||
ap.add_argument("--abi-version", default="6")
|
||||
ap.add_argument("--llvm-nm", default=None)
|
||||
ap.add_argument("--verbose", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
try:
|
||||
customs, imports, exports = collect(args.wasm)
|
||||
errors = []
|
||||
dylink_payloads = customs.get("dylink.0", [])
|
||||
if not dylink_payloads:
|
||||
errors.append("missing dylink.0 custom section")
|
||||
elif not any(dylink_has_valid_mem_info(payload) for payload in dylink_payloads):
|
||||
errors.append("dylink.0 missing valid mem_info subsection")
|
||||
abi_payloads = customs.get("uce.abi", [])
|
||||
if not abi_payloads:
|
||||
errors.append("missing uce.abi custom section")
|
||||
else:
|
||||
abi_text = abi_payloads[-1].decode("utf-8", "replace")
|
||||
required = ["format=uce-wasm-unit-abi-v1", f"unit_abi_version={args.abi_version}", "toolchain="]
|
||||
for needle in required:
|
||||
if needle not in abi_text:
|
||||
errors.append(f"uce.abi missing {needle!r}")
|
||||
export_names = {name for name, _ in exports}
|
||||
forbidden_exports = {"uce_alloc", "uce_free"}
|
||||
for name in sorted(export_names & forbidden_exports):
|
||||
errors.append(f"forbidden allocator export {name}")
|
||||
import_map = {(module, name): kind for module, name, kind in imports}
|
||||
required_imports = {
|
||||
("env", "memory"): 2,
|
||||
("env", "__memory_base"): 3,
|
||||
}
|
||||
# units without indirect calls / stack spills / table needs
|
||||
# legitimately omit these; if present, the kind must be right
|
||||
optional_imports = {
|
||||
("env", "__indirect_function_table"): 1,
|
||||
("env", "__stack_pointer"): 3,
|
||||
("env", "__table_base"): 3,
|
||||
}
|
||||
for key, kind in required_imports.items():
|
||||
if import_map.get(key) != kind:
|
||||
errors.append(f"missing required import {key[0]}.{key[1]}")
|
||||
for key, kind in optional_imports.items():
|
||||
if key in import_map and import_map[key] != kind:
|
||||
errors.append(f"wrong kind for import {key[0]}.{key[1]}")
|
||||
for module, name, kind in imports:
|
||||
if module.startswith("wasi_") or module == "wasi_snapshot_preview1":
|
||||
errors.append(f"forbidden WASI import {module}.{name}")
|
||||
if module not in {"env", "GOT.mem", "GOT.func"} and not module.startswith("GOT."):
|
||||
errors.append(f"unexpected import module {module}.{name}")
|
||||
if module.startswith("GOT.") and kind != 3:
|
||||
errors.append(f"GOT import is not a global: {module}.{name}")
|
||||
llvm_nm = args.llvm_nm or shutil.which("llvm-nm") or "/opt/wasi-sdk/bin/llvm-nm"
|
||||
if Path(llvm_nm).exists():
|
||||
bad_prefixes = ("_Znwm", "_Znam", "_ZdlPv", "_ZdaPv", "_ZdlPvm", "_ZdaPvm")
|
||||
for sym in defined_symbols(args.wasm, llvm_nm):
|
||||
if sym in {"uce_alloc", "uce_free"} or sym.startswith(bad_prefixes):
|
||||
errors.append(f"forbidden allocator definition {sym}")
|
||||
else:
|
||||
errors.append("llvm-nm not found; cannot verify allocator definitions")
|
||||
if errors:
|
||||
for e in errors:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
return 1
|
||||
if args.verbose:
|
||||
print(f"UCE W2 unit check PASS: {args.wasm}")
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
cd ..
|
||||
|
||||
SRC_DIR="$1"
|
||||
DEST_DIR="$2"
|
||||
|
||||
SRC_FN="$3"
|
||||
PP_FN="$4"
|
||||
SO_FN="$5"
|
||||
|
||||
LINK_OBJECTS="$6"
|
||||
|
||||
#echo "Source Dir: $SRC_DIR"
|
||||
#echo "Dest Dir: $DEST_DIR"
|
||||
#echo "Source File: $SRC_FN"
|
||||
#echo "Preprocessed File: $PP_FN"
|
||||
#echo "Dest File: $SO_FN"
|
||||
|
||||
mkdir -p "$DEST_DIR" > /dev/null 2>&1
|
||||
|
||||
export CPLUS_INCLUDE_PATH="${CPLUS_INCLUDE_PATH:+${CPLUS_INCLUDE_PATH}:}$SRC_DIR"
|
||||
|
||||
BUILDMODE="debug"
|
||||
OPT_FLAG="O0"
|
||||
|
||||
COMPILER="clang++"
|
||||
#COMPILER="g++"
|
||||
FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC"
|
||||
|
||||
LIBS="-ldl -lm -lpthread"
|
||||
SRCFLAGS="-D PLATFORM_NAME=\"linux\""
|
||||
|
||||
# echo "Compliling executable..."
|
||||
$COMPILER "$DEST_DIR/$PP_FN" $SRCFLAGS $FLAGS $LIBS -o "$DEST_DIR/$SO_FN"
|
||||
|
||||
# separate .o file
|
||||
#$COMPILER -c "$DEST_DIR/$PP_FN" $SRCFLAGS $FLAGS $LIBS -o "$DEST_DIR/$PP_FN.o"
|
||||
#$COMPILER "$DEST_DIR/$PP_FN.o" "$LINK_OBJECTS" $SRCFLAGS $FLAGS $LIBS -o "$DEST_DIR/$SO_FN"
|
||||
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
# ls -lh "$DEST_DIR"
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
# Compile a preprocessed UCE unit into a PIC WebAssembly side module.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
cd ..
|
||||
|
||||
SRC_DIR="$1"
|
||||
DEST_DIR="$2"
|
||||
SRC_FN="$3"
|
||||
PP_FN="$4"
|
||||
WASM_FN="$5"
|
||||
|
||||
SDK=${WASI_SDK:-/opt/wasi-sdk}
|
||||
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
|
||||
ROOT=$(pwd)
|
||||
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
|
||||
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
|
||||
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
|
||||
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
|
||||
COMMON_FLAGS=(
|
||||
--target=wasm32-wasip1
|
||||
-fPIC -fvisibility=default -fvisibility-inlines-hidden
|
||||
-O1 -g -std=c++20
|
||||
# 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
|
||||
# silent; warnings are intentionally suppressed.
|
||||
-w
|
||||
# must match the core build ABI: units with RTTI/EH enabled import
|
||||
# typeinfo/unwind symbols the -fno-rtti/-fno-exceptions core cannot provide
|
||||
-fno-exceptions -fno-rtti
|
||||
-D__UCE_WASM_UNIT__
|
||||
-DPLATFORM_NAME=\"wasm32-wasip1\"
|
||||
)
|
||||
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOOLCHAIN_ID=$(${SDK}/bin/clang++ --version | head -n 1)
|
||||
HEADER_HASH=$(find src/lib -maxdepth 1 -name '*.h' -type f -print0 | sort -z | xargs -0 sha1sum | 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_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
|
||||
|
||||
mkdir -p "$DEST_DIR" >/dev/null 2>&1
|
||||
|
||||
build_pch_if_needed() {
|
||||
if [ "$PCH_ENABLED" = "0" ]; then
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$PCH_DIR"
|
||||
if [ -s "$PCH_FN" ] && [ -z "$(find src/lib -maxdepth 1 -name '*.h' -type f -newer "$PCH_FN" -print -quit)" ]; then
|
||||
return 0
|
||||
fi
|
||||
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
|
||||
-Isrc/lib \
|
||||
-x c++-header src/lib/uce_lib.h -o "$PCH_FN.tmp"
|
||||
mv "$PCH_FN.tmp" "$PCH_FN"
|
||||
}
|
||||
|
||||
cat > "$ABI_TMP" <<EOF
|
||||
format=uce-wasm-unit-abi-v1
|
||||
unit_abi_version=$ABI_VERSION
|
||||
toolchain=$TOOLCHAIN_ID
|
||||
source=$SRC_FN
|
||||
EOF
|
||||
|
||||
build_pch_if_needed
|
||||
PCH_FLAGS=()
|
||||
if [ "$PCH_ENABLED" != "0" ]; then
|
||||
PCH_FLAGS=(-include-pch "$PCH_FN")
|
||||
fi
|
||||
|
||||
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
|
||||
-I"$SRC_DIR" -I"$ROOT/src/lib" \
|
||||
"${PCH_FLAGS[@]}" \
|
||||
-c "$DEST_DIR/$PP_FN" -o "$OBJ_FN"
|
||||
|
||||
"$SDK/bin/wasm-ld" -shared --experimental-pic \
|
||||
--unresolved-symbols=import-dynamic \
|
||||
--Bsymbolic \
|
||||
"$OBJ_FN" -o "$DEST_DIR/$WASM_FN" \
|
||||
--export-if-defined=__uce_set_current_request \
|
||||
--export-if-defined=__uce_render \
|
||||
--export-if-defined=__uce_component \
|
||||
--export-if-defined=__uce_websocket \
|
||||
--export-if-defined=__uce_cli \
|
||||
--export-if-defined=__uce_serve_http \
|
||||
--export-if-defined=__uce_once \
|
||||
--export-if-defined=__uce_init
|
||||
|
||||
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$DEST_DIR/$WASM_FN"
|
||||
|
||||
python3 scripts/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
|
||||
|
||||
rm -f "$OBJ_FN" "$ABI_TMP"
|
||||
@@ -3,7 +3,7 @@ Version: @VERSION@
|
||||
Section: web
|
||||
Priority: optional
|
||||
Architecture: @ARCH@
|
||||
Depends: bash, clang, libmariadb-dev | default-libmysqlclient-dev, systemd
|
||||
Depends: bash, python3, curl, systemd, libpcre2-8-0, zlib1g, libssl3, libstdc++6, libgcc-s1, libmariadb3 | libmysqlclient21 | libmysqlclient24
|
||||
Maintainer: Udo Schroeter <udo@openfu.com>
|
||||
Installed-Size: @INSTALLED_SIZE@
|
||||
Description: UCE FastCGI runtime and live-compiling C++ web environment
|
||||
|
||||
@@ -5,6 +5,7 @@ if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
if [ "$1" = "configure" ]; then
|
||||
systemctl enable uce.service >/dev/null 2>&1 || true
|
||||
systemctl restart uce.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ StateDirectory=uce
|
||||
CacheDirectory=uce
|
||||
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 /usr/lib/uce/scripts/build_linux.sh
|
||||
ExecStart=/usr/lib/uce/bin/uce_fastcgi.linux.bin
|
||||
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||
Restart=always
|
||||
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
cd "$repo_root"
|
||||
|
||||
out_dir="${1:-changelog}"
|
||||
mkdir -p "$out_dir"
|
||||
rm -f "$out_dir"/*.log
|
||||
|
||||
while IFS= read -r commit; do
|
||||
commit_date=$(git show -s --format=%cs "$commit")
|
||||
out_file="$out_dir/$commit_date.log"
|
||||
{
|
||||
printf 'commit %s\n' "$commit"
|
||||
git show -s --format=%B "$commit"
|
||||
printf '\n'
|
||||
} >> "$out_file"
|
||||
done < <(git rev-list --reverse HEAD)
|
||||
|
||||
printf 'Wrote changelog files to %s\n' "$out_dir"
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install the pinned WASI SDK used by UCE's request-time wasm compiler.
|
||||
# This is a runtime dependency: UCE compiles .uce units on demand and during
|
||||
# proactive startup scans, so every deployment host needs the same toolchain.
|
||||
|
||||
WASI_SDK_VERSION="${WASI_SDK_VERSION:-33.0}"
|
||||
WASI_SDK_RELEASE_TAG="${WASI_SDK_RELEASE_TAG:-wasi-sdk-33}"
|
||||
WASI_SDK_ARCHIVE="${WASI_SDK_ARCHIVE:-wasi-sdk-33.0-x86_64-linux.tar.gz}"
|
||||
WASI_SDK_SHA256="${WASI_SDK_SHA256:-0ba8b5bfaeb2adf3f29bab5841d76cf5318ab8e1642ea195f88baba1abd47bce}"
|
||||
WASI_SDK_URL="${WASI_SDK_URL:-https://github.com/WebAssembly/wasi-sdk/releases/download/${WASI_SDK_RELEASE_TAG}/${WASI_SDK_ARCHIVE}}"
|
||||
INSTALL_BASE="${INSTALL_BASE:-/opt}"
|
||||
INSTALL_DIR="${WASI_SDK_INSTALL_DIR:-${INSTALL_BASE}/wasi-sdk-${WASI_SDK_VERSION}-x86_64-linux}"
|
||||
SYMLINK_PATH="${WASI_SDK_SYMLINK:-${INSTALL_BASE}/wasi-sdk}"
|
||||
CACHE_DIR="${WASI_SDK_CACHE_DIR:-/tmp/uce-deps}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: scripts/install_wasi_sdk.sh [--check-only]
|
||||
|
||||
Installs pinned WASI SDK ${WASI_SDK_VERSION} for UCE request-time unit compilation.
|
||||
|
||||
Environment overrides:
|
||||
WASI_SDK_VERSION ${WASI_SDK_VERSION}
|
||||
WASI_SDK_RELEASE_TAG ${WASI_SDK_RELEASE_TAG}
|
||||
WASI_SDK_ARCHIVE ${WASI_SDK_ARCHIVE}
|
||||
WASI_SDK_SHA256 ${WASI_SDK_SHA256}
|
||||
WASI_SDK_URL ${WASI_SDK_URL}
|
||||
WASI_SDK_INSTALL_DIR ${INSTALL_DIR}
|
||||
WASI_SDK_SYMLINK ${SYMLINK_PATH}
|
||||
WASI_SDK_CACHE_DIR ${CACHE_DIR}
|
||||
EOF
|
||||
}
|
||||
|
||||
check_only=0
|
||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
elif [[ "${1:-}" == "--check-only" ]]; then
|
||||
check_only=1
|
||||
elif [[ $# -gt 0 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Required command not found: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
verify_tree() {
|
||||
local root="$1"
|
||||
for tool in clang++ wasm-ld llvm-objcopy llvm-nm; do
|
||||
if [[ ! -x "$root/bin/$tool" ]]; then
|
||||
echo "Missing WASI SDK tool: $root/bin/$tool" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
"$root/bin/clang++" --version | head -n 1
|
||||
}
|
||||
|
||||
if [[ $check_only -eq 1 ]]; then
|
||||
verify_tree "${WASI_SDK:-$SYMLINK_PATH}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_command curl
|
||||
require_command sha256sum
|
||||
require_command tar
|
||||
require_command mkdir
|
||||
require_command ln
|
||||
|
||||
mkdir -p "$CACHE_DIR" "$INSTALL_BASE"
|
||||
archive_path="$CACHE_DIR/$WASI_SDK_ARCHIVE"
|
||||
|
||||
if [[ ! -f "$archive_path" ]]; then
|
||||
echo "Downloading $WASI_SDK_URL"
|
||||
curl -fL --proto '=https' --tlsv1.2 -o "$archive_path.tmp" "$WASI_SDK_URL"
|
||||
mv "$archive_path.tmp" "$archive_path"
|
||||
fi
|
||||
|
||||
printf '%s %s\n' "$WASI_SDK_SHA256" "$archive_path" | sha256sum -c -
|
||||
|
||||
rm -rf -- "$INSTALL_DIR.tmp"
|
||||
mkdir -p "$INSTALL_DIR.tmp"
|
||||
tar -xf "$archive_path" -C "$INSTALL_DIR.tmp" --strip-components=1
|
||||
verify_tree "$INSTALL_DIR.tmp"
|
||||
|
||||
rm -rf -- "$INSTALL_DIR"
|
||||
mv "$INSTALL_DIR.tmp" "$INSTALL_DIR"
|
||||
ln -sfn "$INSTALL_DIR" "$SYMLINK_PATH"
|
||||
|
||||
echo "Installed WASI SDK at $INSTALL_DIR"
|
||||
echo "Updated symlink $SYMLINK_PATH -> $INSTALL_DIR"
|
||||
verify_tree "$SYMLINK_PATH"
|
||||
+97
-13
@@ -5,16 +5,21 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEB_ASSET_DIR="$SCRIPT_DIR/deb"
|
||||
PACKAGE_NAME="uce"
|
||||
REVISION="${UCE_DEB_REVISION:-1}"
|
||||
REVISION="${UCE_DEB_REVISION:-}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/make_deb.sh VERSION
|
||||
scripts/make_deb.sh [VERSION]
|
||||
|
||||
When VERSION is omitted, scripts/make_deb.sh reads VERSION from version.txt.
|
||||
|
||||
Environment:
|
||||
UCE_DEB_REVISION Debian package revision suffix (default: 1)
|
||||
UCE_DEB_ARCH Override package architecture
|
||||
UCE_DEB_REVISION Optional Debian package revision suffix
|
||||
UCE_DEB_ARCH Override package architecture
|
||||
UCE_DEB_WEBROOT Public web root staged into the package (default: /var/www/html)
|
||||
UCE_DEB_BUNDLE_WASI_SDK Bundle pinned /opt/wasi-sdk into the package (default: 1)
|
||||
UCE_DEB_BUNDLE_WASMTIME Bundle /opt/wasmtime into the package (default: 1)
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -39,7 +44,7 @@ resolve_arch() {
|
||||
|
||||
validate_version() {
|
||||
local version="$1"
|
||||
if [[ ! "$version" =~ ^[0-9][A-Za-z0-9.+:~]*$ ]]; then
|
||||
if [[ ! "$version" =~ ^[0-9][A-Za-z0-9.+:~-]*$ ]]; then
|
||||
echo "Invalid Debian version string: $version" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -47,12 +52,37 @@ validate_version() {
|
||||
|
||||
copy_payload() {
|
||||
local destination="$1"
|
||||
local webroot="$2"
|
||||
local stage_dir="$3"
|
||||
local path
|
||||
for path in LICENSE README.md codesearch bin scripts site src; do
|
||||
for path in LICENSE README.md codesearch bin scripts src docs; do
|
||||
cp -a "$REPO_ROOT/$path" "$destination/"
|
||||
done
|
||||
mkdir -p "$destination/etc"
|
||||
mkdir -p "$destination/etc" "$stage_dir$webroot"
|
||||
cp -a "$REPO_ROOT/etc/uce" "$destination/etc/"
|
||||
cp -a "$REPO_ROOT/site/." "$stage_dir$webroot/"
|
||||
}
|
||||
|
||||
write_packaged_settings() {
|
||||
local output_file="$1"
|
||||
local webroot="$2"
|
||||
python3 - "$REPO_ROOT/etc/uce/settings.cfg" "$output_file" "$webroot" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
src, dst, webroot = sys.argv[1:4]
|
||||
s = Path(src).read_text()
|
||||
replacements = {
|
||||
"SITE_DIRECTORY=site": f"SITE_DIRECTORY={webroot}",
|
||||
"WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm": "WASM_CORE_PATH=/usr/lib/uce/bin/wasm/core.wasm",
|
||||
"HTTP_DOCUMENT_ROOT=": f"HTTP_DOCUMENT_ROOT={webroot}",
|
||||
}
|
||||
for old, new in replacements.items():
|
||||
if old in s:
|
||||
s = s.replace(old, new)
|
||||
if "WASM_COMPILE_SCRIPT=" not in s:
|
||||
s += "\nWASM_COMPILE_SCRIPT=scripts/compile_wasm_unit\n"
|
||||
Path(dst).write_text(s)
|
||||
PY
|
||||
}
|
||||
|
||||
write_control_file() {
|
||||
@@ -69,20 +99,68 @@ write_control_file() {
|
||||
"$DEB_ASSET_DIR/control.in" > "$output_file"
|
||||
}
|
||||
|
||||
bundle_wasi_sdk() {
|
||||
local stage_dir="$1"
|
||||
local wasi_root="${WASI_SDK:-/opt/wasi-sdk}"
|
||||
if [[ "${UCE_DEB_BUNDLE_WASI_SDK:-1}" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ ! -x "$wasi_root/bin/clang++" || ! -x "$wasi_root/bin/wasm-ld" || ! -x "$wasi_root/bin/llvm-nm" ]]; then
|
||||
echo "UCE_DEB_BUNDLE_WASI_SDK=1 but WASI_SDK does not point at a complete SDK: $wasi_root" >&2
|
||||
exit 1
|
||||
fi
|
||||
local resolved
|
||||
resolved="$(readlink -f "$wasi_root")"
|
||||
local base
|
||||
base="$(basename "$resolved")"
|
||||
mkdir -p "$stage_dir/opt"
|
||||
cp -a "$resolved" "$stage_dir/opt/$base"
|
||||
ln -sfn "$base" "$stage_dir/opt/wasi-sdk"
|
||||
}
|
||||
|
||||
bundle_wasmtime() {
|
||||
local stage_dir="$1"
|
||||
local wasmtime_root="${WASMTIME_HOME:-/opt/wasmtime}"
|
||||
if [[ "${UCE_DEB_BUNDLE_WASMTIME:-1}" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ ! -f "$wasmtime_root/include/wasmtime.hh" || ! -f "$wasmtime_root/lib/libwasmtime.so" ]]; then
|
||||
echo "UCE_DEB_BUNDLE_WASMTIME=1 but WASMTIME_HOME does not point at a complete C API tree: $wasmtime_root" >&2
|
||||
exit 1
|
||||
fi
|
||||
local resolved
|
||||
resolved="$(readlink -f "$wasmtime_root")"
|
||||
local base
|
||||
base="$(basename "$resolved")"
|
||||
mkdir -p "$stage_dir/opt"
|
||||
cp -a "$resolved" "$stage_dir/opt/$base"
|
||||
ln -sfn "$base" "$stage_dir/opt/wasmtime"
|
||||
}
|
||||
|
||||
write_md5sums() {
|
||||
local stage_dir="$1"
|
||||
(
|
||||
cd "$stage_dir"
|
||||
find usr etc lib -type f -print0 | sort -z | xargs -0 md5sum > DEBIAN/md5sums
|
||||
find usr etc lib opt -type f -print0 2>/dev/null | sort -z | xargs -0 --no-run-if-empty md5sum > DEBIAN/md5sums
|
||||
)
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
if [[ $# -gt 1 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
if [[ $# -eq 1 ]]; then
|
||||
VERSION="$1"
|
||||
else
|
||||
if [[ ! -r "$REPO_ROOT/version.txt" ]]; then
|
||||
echo "Missing version.txt and no VERSION argument supplied" >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
. "$REPO_ROOT/version.txt"
|
||||
VERSION="${VERSION:-}"
|
||||
fi
|
||||
validate_version "$VERSION"
|
||||
|
||||
require_command bash
|
||||
@@ -101,11 +179,15 @@ require_command cp
|
||||
require_command sort
|
||||
|
||||
ARCH="$(resolve_arch)"
|
||||
PACKAGE_VERSION="${VERSION}-${REVISION}"
|
||||
PACKAGE_VERSION="$VERSION"
|
||||
if [[ -n "$REVISION" ]]; then
|
||||
PACKAGE_VERSION="${PACKAGE_VERSION}-${REVISION}"
|
||||
fi
|
||||
PACKAGE_BASENAME="${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCH}"
|
||||
STAGE_DIR="$REPO_ROOT/pkg/$PACKAGE_BASENAME"
|
||||
DEBIAN_DIR="$STAGE_DIR/DEBIAN"
|
||||
INSTALL_ROOT="$STAGE_DIR/usr/lib/uce"
|
||||
WEBROOT="${UCE_DEB_WEBROOT:-/var/www/html}"
|
||||
DIST_DIR="$REPO_ROOT/dist"
|
||||
OUTPUT_DEB="$DIST_DIR/$PACKAGE_BASENAME.deb"
|
||||
|
||||
@@ -117,9 +199,11 @@ bash "$REPO_ROOT/scripts/build_linux.sh"
|
||||
rm -rf -- "$STAGE_DIR"
|
||||
mkdir -p "$DEBIAN_DIR" "$INSTALL_ROOT" "$STAGE_DIR/etc/uce" "$STAGE_DIR/lib/systemd/system" "$DIST_DIR"
|
||||
|
||||
copy_payload "$INSTALL_ROOT"
|
||||
copy_payload "$INSTALL_ROOT" "$WEBROOT" "$STAGE_DIR"
|
||||
bundle_wasi_sdk "$STAGE_DIR"
|
||||
bundle_wasmtime "$STAGE_DIR"
|
||||
|
||||
install -m 0644 "$REPO_ROOT/etc/uce/settings.cfg" "$STAGE_DIR/etc/uce/settings.cfg"
|
||||
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/conffiles" "$DEBIAN_DIR/conffiles"
|
||||
install -m 0755 "$DEB_ASSET_DIR/postinst" "$DEBIAN_DIR/postinst"
|
||||
|
||||
Executable
+260
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PACKAGE_NAME="uce"
|
||||
RELEASE="${UCE_RPM_RELEASE:-1}"
|
||||
WEBROOT="${UCE_RPM_WEBROOT:-/var/www/html}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/make_rpm.sh [VERSION]
|
||||
|
||||
When VERSION is omitted, scripts/make_rpm.sh reads VERSION, MAJOR, and RELEASE from version.txt.
|
||||
|
||||
Environment:
|
||||
UCE_RPM_RELEASE Override RPM release suffix (default: RELEASE from version.txt)
|
||||
UCE_RPM_ARCH Override RPM architecture
|
||||
UCE_RPM_WEBROOT Public web root staged into the package (default: /var/www/html)
|
||||
UCE_RPM_BUNDLE_WASI_SDK Bundle pinned /opt/wasi-sdk into the package (default: 1)
|
||||
UCE_RPM_BUNDLE_WASMTIME Bundle /opt/wasmtime into the package (default: 1)
|
||||
EOF
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Required command not found: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_version() {
|
||||
local version="$1"
|
||||
if [[ ! "$version" =~ ^[0-9][A-Za-z0-9._+~]*$ ]]; then
|
||||
echo "Invalid RPM version string: $version" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_arch() {
|
||||
if [[ -n "${UCE_RPM_ARCH:-}" ]]; then
|
||||
printf '%s\n' "$UCE_RPM_ARCH"
|
||||
return
|
||||
fi
|
||||
uname -m
|
||||
}
|
||||
|
||||
copy_payload() {
|
||||
local destination="$1"
|
||||
local webroot="$2"
|
||||
local stage_dir="$3"
|
||||
local path
|
||||
for path in LICENSE README.md codesearch bin scripts src docs; do
|
||||
cp -a "$REPO_ROOT/$path" "$destination/"
|
||||
done
|
||||
mkdir -p "$destination/etc" "$stage_dir$webroot"
|
||||
cp -a "$REPO_ROOT/etc/uce" "$destination/etc/"
|
||||
cp -a "$REPO_ROOT/site/." "$stage_dir$webroot/"
|
||||
}
|
||||
|
||||
write_packaged_settings() {
|
||||
local output_file="$1"
|
||||
local webroot="$2"
|
||||
python3 - "$REPO_ROOT/etc/uce/settings.cfg" "$output_file" "$webroot" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
src, dst, webroot = sys.argv[1:4]
|
||||
s = Path(src).read_text()
|
||||
replacements = {
|
||||
"SITE_DIRECTORY=site": f"SITE_DIRECTORY={webroot}",
|
||||
"WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm": "WASM_CORE_PATH=/usr/lib/uce/bin/wasm/core.wasm",
|
||||
"HTTP_DOCUMENT_ROOT=": f"HTTP_DOCUMENT_ROOT={webroot}",
|
||||
}
|
||||
for old, new in replacements.items():
|
||||
if old in s:
|
||||
s = s.replace(old, new)
|
||||
if "WASM_COMPILE_SCRIPT=" not in s:
|
||||
s += "\nWASM_COMPILE_SCRIPT=scripts/compile_wasm_unit\n"
|
||||
Path(dst).write_text(s)
|
||||
PY
|
||||
}
|
||||
|
||||
bundle_wasi_sdk() {
|
||||
local stage_dir="$1"
|
||||
local wasi_root="${WASI_SDK:-/opt/wasi-sdk}"
|
||||
if [[ "${UCE_RPM_BUNDLE_WASI_SDK:-1}" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ ! -x "$wasi_root/bin/clang++" || ! -x "$wasi_root/bin/wasm-ld" || ! -x "$wasi_root/bin/llvm-nm" ]]; then
|
||||
echo "UCE_RPM_BUNDLE_WASI_SDK=1 but WASI_SDK does not point at a complete SDK: $wasi_root" >&2
|
||||
exit 1
|
||||
fi
|
||||
local resolved base
|
||||
resolved="$(readlink -f "$wasi_root")"
|
||||
base="$(basename "$resolved")"
|
||||
mkdir -p "$stage_dir/opt"
|
||||
cp -a "$resolved" "$stage_dir/opt/$base"
|
||||
ln -sfn "$base" "$stage_dir/opt/wasi-sdk"
|
||||
}
|
||||
|
||||
bundle_wasmtime() {
|
||||
local stage_dir="$1"
|
||||
local wasmtime_root="${WASMTIME_HOME:-/opt/wasmtime}"
|
||||
if [[ "${UCE_RPM_BUNDLE_WASMTIME:-1}" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ ! -f "$wasmtime_root/include/wasmtime.hh" || ! -f "$wasmtime_root/lib/libwasmtime.so" ]]; then
|
||||
echo "UCE_RPM_BUNDLE_WASMTIME=1 but WASMTIME_HOME does not point at a complete C API tree: $wasmtime_root" >&2
|
||||
exit 1
|
||||
fi
|
||||
local resolved base
|
||||
resolved="$(readlink -f "$wasmtime_root")"
|
||||
base="$(basename "$resolved")"
|
||||
mkdir -p "$stage_dir/opt"
|
||||
cp -a "$resolved" "$stage_dir/opt/$base"
|
||||
ln -sfn "$base" "$stage_dir/opt/wasmtime"
|
||||
}
|
||||
|
||||
if [[ $# -gt 1 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FULL_VERSION=""
|
||||
if [[ $# -eq 1 ]]; then
|
||||
FULL_VERSION="$1"
|
||||
RPM_VERSION="${FULL_VERSION%%-*}"
|
||||
RPM_RELEASE_PART="${FULL_VERSION#*-}"
|
||||
if [[ "$RPM_RELEASE_PART" == "$FULL_VERSION" ]]; then
|
||||
RPM_RELEASE_PART="$RELEASE"
|
||||
fi
|
||||
else
|
||||
if [[ ! -r "$REPO_ROOT/version.txt" ]]; then
|
||||
echo "Missing version.txt and no VERSION argument supplied" >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
. "$REPO_ROOT/version.txt"
|
||||
FULL_VERSION="${VERSION:-}"
|
||||
RPM_VERSION="${MAJOR:-${FULL_VERSION%%-*}}"
|
||||
RPM_RELEASE_PART="${RELEASE:-${FULL_VERSION#*-}}"
|
||||
fi
|
||||
|
||||
if [[ -z "$FULL_VERSION" || -z "$RPM_VERSION" || -z "$RPM_RELEASE_PART" ]]; then
|
||||
echo "Could not derive RPM version from VERSION/MAJOR/RELEASE" >&2
|
||||
exit 1
|
||||
fi
|
||||
RPM_RELEASE_PART="${RPM_RELEASE_PART//-/_}"
|
||||
VERSION="$RPM_VERSION"
|
||||
RELEASE="${UCE_RPM_RELEASE:-$RPM_RELEASE_PART}"
|
||||
validate_version "$VERSION"
|
||||
|
||||
require_command bash
|
||||
require_command rpmbuild
|
||||
require_command find
|
||||
require_command cp
|
||||
require_command tar
|
||||
require_command sed
|
||||
require_command python3
|
||||
require_command readlink
|
||||
|
||||
ARCH="$(resolve_arch)"
|
||||
PACKAGE_VERSION="$FULL_VERSION"
|
||||
BUILD_ROOT="$REPO_ROOT/pkg/rpm-build"
|
||||
STAGE_DIR="$BUILD_ROOT/stage"
|
||||
RPMBUILD_DIR="$BUILD_ROOT/rpmbuild"
|
||||
INSTALL_ROOT="$STAGE_DIR/usr/lib/uce"
|
||||
SPEC_FILE="$RPMBUILD_DIR/SPECS/$PACKAGE_NAME.spec"
|
||||
DIST_DIR="$REPO_ROOT/dist"
|
||||
|
||||
bash "$REPO_ROOT/scripts/build_linux.sh"
|
||||
|
||||
rm -rf -- "$BUILD_ROOT"
|
||||
mkdir -p "$INSTALL_ROOT" "$STAGE_DIR/etc/uce" "$STAGE_DIR/usr/lib/systemd/system" "$STAGE_DIR/var/cache/uce" "$STAGE_DIR/var/lib/uce" "$RPMBUILD_DIR"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} "$DIST_DIR"
|
||||
|
||||
copy_payload "$INSTALL_ROOT" "$WEBROOT" "$STAGE_DIR"
|
||||
bundle_wasi_sdk "$STAGE_DIR"
|
||||
bundle_wasmtime "$STAGE_DIR"
|
||||
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"
|
||||
|
||||
(
|
||||
cd "$STAGE_DIR"
|
||||
tar --sort=name --mtime='UTC 2026-01-01' --owner=0 --group=0 --numeric-owner -czf "$RPMBUILD_DIR/SOURCES/$PACKAGE_NAME-$VERSION.tar.gz" .
|
||||
)
|
||||
|
||||
cat > "$SPEC_FILE" <<EOF
|
||||
%global __os_install_post %{nil}
|
||||
|
||||
Name: $PACKAGE_NAME
|
||||
Version: $VERSION
|
||||
Release: $RELEASE%{?dist}
|
||||
Summary: UCE FastCGI runtime and live-compiling C++ web environment
|
||||
License: GPL-3.0-or-later
|
||||
URL: https://example.com/uce
|
||||
BuildArch: $ARCH
|
||||
Requires: bash
|
||||
Requires: python3
|
||||
Requires: curl
|
||||
Requires: systemd
|
||||
Requires: pcre2
|
||||
Requires: zlib
|
||||
Requires: openssl-libs
|
||||
Requires: libstdc++
|
||||
Requires: mariadb-connector-c
|
||||
|
||||
%description
|
||||
UCE is an experimental C/C++ web runtime with FastCGI request handling,
|
||||
on-demand wasm unit compilation, and a packaged site/doc/test tree.
|
||||
|
||||
%prep
|
||||
rm -rf %{_builddir}/$PACKAGE_NAME-$VERSION
|
||||
mkdir -p %{_builddir}/$PACKAGE_NAME-$VERSION
|
||||
cd %{_builddir}/$PACKAGE_NAME-$VERSION
|
||||
tar -xzf %{_sourcedir}/$PACKAGE_NAME-$VERSION.tar.gz
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
mkdir -p %{buildroot}
|
||||
cp -a %{_builddir}/$PACKAGE_NAME-$VERSION/. %{buildroot}/
|
||||
|
||||
%post
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl enable uce.service >/dev/null 2>&1 || true
|
||||
systemctl restart uce.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
%preun
|
||||
if [ "\$1" = "0" ] && command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now uce.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
%postun
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
%files
|
||||
%license /usr/lib/uce/LICENSE
|
||||
%doc /usr/lib/uce/README.md
|
||||
%config(noreplace) /etc/uce/settings.cfg
|
||||
/usr/lib/uce
|
||||
/usr/lib/systemd/system/uce.service
|
||||
$WEBROOT
|
||||
%dir /var/cache/uce
|
||||
%dir /var/lib/uce
|
||||
/opt/*
|
||||
|
||||
%changelog
|
||||
* Mon Jun 15 2026 UCE Packager <root@localhost> - $PACKAGE_VERSION
|
||||
- Package UCE runtime with pinned deployment toolchains.
|
||||
EOF
|
||||
|
||||
rpmbuild --define "_topdir $RPMBUILD_DIR" -bb "$SPEC_FILE"
|
||||
find "$RPMBUILD_DIR/RPMS" -type f -name '*.rpm' -print -exec cp -a {} "$DIST_DIR/" \;
|
||||
find "$DIST_DIR" -maxdepth 1 -type f -name "${PACKAGE_NAME}-${VERSION}-${RELEASE}*.rpm" -print
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
socket_path="${UCE_CLI_SOCKET:-/run/uce/cli.sock}"
|
||||
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)
|
||||
if [[ -n "${configured_socket:-}" ]]; then
|
||||
socket_path="$configured_socket"
|
||||
fi
|
||||
fi
|
||||
|
||||
include_kill=0
|
||||
action="run"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--include-wasm-kill|--include-kill)
|
||||
include_kill=1
|
||||
shift
|
||||
;;
|
||||
--list)
|
||||
action="list"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/run_cli_tests.sh [--include-wasm-kill] [--list]
|
||||
|
||||
Runs the UCE unit-based test suite through the runtime CLI socket.
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown option: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -S "$socket_path" ]]; then
|
||||
echo "UCE CLI socket not found: $socket_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}"
|
||||
exec curl -sS --fail-with-body --unix-socket "$socket_path" "$url"
|
||||
Executable
+156
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
default_socket="/run/uce/cli.sock"
|
||||
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)
|
||||
if [[ -n "${configured_socket:-}" ]]; then
|
||||
default_socket="$configured_socket"
|
||||
fi
|
||||
fi
|
||||
socket_path="${UCE_CLI_SOCKET:-$default_socket}"
|
||||
method="auto"
|
||||
raw_json=""
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
scripts/uce-cli [options] <unit-or-hook> [key=value ...]
|
||||
|
||||
Options:
|
||||
--socket PATH Unix socket path (default: CLI_SOCKET_PATH from /etc/uce/settings.cfg,
|
||||
UCE_CLI_SOCKET, or /run/uce/cli.sock)
|
||||
--get Send key=value parameters as a query string
|
||||
--post Send key=value parameters as a JSON POST (default when params exist)
|
||||
--json JSON Send raw JSON as POST body
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
scripts/uce-cli /ping
|
||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
|
||||
scripts/uce-cli --get /tests/cli.uce action=echo message=hello
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--socket)
|
||||
[[ $# -ge 2 ]] || { echo "--socket requires a path" >&2; exit 2; }
|
||||
socket_path="$2"
|
||||
shift 2
|
||||
;;
|
||||
--get)
|
||||
method="get"
|
||||
shift
|
||||
;;
|
||||
--post)
|
||||
method="post"
|
||||
shift
|
||||
;;
|
||||
--json)
|
||||
[[ $# -ge 2 ]] || { echo "--json requires a JSON value" >&2; exit 2; }
|
||||
raw_json="$2"
|
||||
method="post"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $# -ge 1 ]] || { usage >&2; exit 2; }
|
||||
path="$1"
|
||||
shift
|
||||
|
||||
if [[ "$path" != /* ]]; then
|
||||
path="/$path"
|
||||
fi
|
||||
|
||||
if [[ ! -S "$socket_path" ]]; then
|
||||
echo "UCE CLI socket not found: $socket_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$method" == "auto" ]]; then
|
||||
if [[ $# -gt 0 || -n "$raw_json" ]]; then
|
||||
method="post"
|
||||
else
|
||||
method="get"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$raw_json" ]]; then
|
||||
printf '%s' "$raw_json" | python3 -m json.tool >/dev/null
|
||||
curl -sS --fail-with-body --unix-socket "$socket_path" \
|
||||
-X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary "$raw_json" \
|
||||
"http://localhost$path"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [[ "$method" == "get" ]]; then
|
||||
url="http://localhost$path"
|
||||
if [[ $# -gt 0 ]]; then
|
||||
query=$(python3 - "$@" <<'PY'
|
||||
import sys, urllib.parse
|
||||
pairs = []
|
||||
for arg in sys.argv[1:]:
|
||||
if '=' not in arg:
|
||||
raise SystemExit(f"argument is not key=value: {arg}")
|
||||
key, value = arg.split('=', 1)
|
||||
pairs.append((key, value))
|
||||
print(urllib.parse.urlencode(pairs))
|
||||
PY
|
||||
)
|
||||
url="$url?$query"
|
||||
fi
|
||||
curl -sS --fail-with-body --unix-socket "$socket_path" "$url"
|
||||
else
|
||||
json=$(python3 - "$@" <<'PY'
|
||||
import json, sys
|
||||
payload = {}
|
||||
for arg in sys.argv[1:]:
|
||||
if '=' not in arg:
|
||||
raise SystemExit(f"argument is not key=value: {arg}")
|
||||
key, value = arg.split('=', 1)
|
||||
lowered = value.lower()
|
||||
if lowered == 'true':
|
||||
parsed = True
|
||||
elif lowered == 'false':
|
||||
parsed = False
|
||||
elif lowered in ('null', 'none'):
|
||||
parsed = None
|
||||
else:
|
||||
try:
|
||||
parsed = int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
parsed = float(value)
|
||||
except ValueError:
|
||||
parsed = value
|
||||
payload[key] = parsed
|
||||
print(json.dumps(payload, separators=(',', ':')))
|
||||
PY
|
||||
)
|
||||
curl -sS --fail-with-body --unix-socket "$socket_path" \
|
||||
-X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary "$json" \
|
||||
"http://localhost$path"
|
||||
fi
|
||||
@@ -1,6 +1,6 @@
|
||||
// Minimal exported helper used by the `unit_call()` demo page.
|
||||
|
||||
EXPORT DTree* test_func(DTree* call_param)
|
||||
EXPORT DValue* test_func(DValue* call_param)
|
||||
{
|
||||
print("HELLO FROM TEST FUNCTION");
|
||||
return(0);
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DValue p;
|
||||
p.set(context.params);
|
||||
|
||||
StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"};
|
||||
StringList unique_routes = list_unique(routes);
|
||||
StringList sorted_routes = list_sort(unique_routes);
|
||||
StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); });
|
||||
|
||||
DValue cards;
|
||||
DValue card;
|
||||
card["title"] = "Dashboard"; card["section"] = "app"; card["href"] = "?dashboard"; 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);
|
||||
|
||||
DValue app_cards = dv_filter(cards, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
|
||||
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>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
|
||||
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="index.uce">UCE Test Suite</a><a class="docs-link" href="../doc/index.uce?p=map">Collection Docs →</a></h1>
|
||||
<h2>Collection Helpers</h2>
|
||||
<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>DValue app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
|
||||
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
|
||||
<details>
|
||||
<summary>Request Parameters</summary>
|
||||
<pre><?= var_dump(p) ?></pre>
|
||||
</details>
|
||||
</body>
|
||||
</html></>
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree card_props;
|
||||
DValue card_props;
|
||||
card_props["title"] = "Component Example";
|
||||
card_props["body"] = "This card body comes from context.call and is rendered through component().";
|
||||
card_props["body"] = "This card body comes from context.props and is rendered through component().";
|
||||
|
||||
DTree named_props;
|
||||
DValue named_props;
|
||||
named_props["title"] = "Named Render Example";
|
||||
named_props["body"] = "This content is rendered through the COMPONENT:BODY(Request& context) entry point.";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<>
|
||||
<section style="border:1px solid #ccc;padding:1em;margin:1em 0;">
|
||||
<? component_render(":TITLE", context.props, context); ?>
|
||||
<? component_render(":BODY", context.props, context); ?>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
|
||||
COMPONENT:TITLE(Request& context)
|
||||
{
|
||||
<>
|
||||
<h3><?= first(context.props["title"].to_string(), "Component Title") ?></h3>
|
||||
</>
|
||||
}
|
||||
|
||||
COMPONENT:BODY(Request& context)
|
||||
{
|
||||
<>
|
||||
<p><?= first(context.props["body"].to_string(), "Component Body") ?></p>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
String lang = first(context.props["lang"].to_string(), "plain");
|
||||
<>
|
||||
<section>
|
||||
<p><strong>Code block:</strong> <?= lang ?></p>
|
||||
<div><?: context.props["default_html"].to_string() ?></div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
String title = first(
|
||||
context.props["node"]["attrs"]["title"].to_string(),
|
||||
context.props["argument"].to_string(),
|
||||
"Notice"
|
||||
);
|
||||
|
||||
<>
|
||||
<aside>
|
||||
<p><strong><?= title ?></strong></p>
|
||||
<div><?: context.props["children_html"].to_string() ?></div>
|
||||
</aside>
|
||||
</>
|
||||
}
|
||||
@@ -78,7 +78,7 @@ void test_demo_render_restricted_json(Request& context, String title, String ris
|
||||
{
|
||||
context.set_status(403, "Restricted");
|
||||
context.header["Content-Type"] = "application/json; charset=utf-8";
|
||||
DTree payload;
|
||||
DValue payload;
|
||||
payload["ok"].set_bool(false);
|
||||
payload["error"] = "restricted";
|
||||
payload["title"] = title;
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
DTree
|
||||
DValue
|
||||
</h1>
|
||||
|
||||
<h2>Value Types</h2>
|
||||
@@ -33,21 +33,21 @@ RENDER(Request& context)
|
||||
|
||||
<pre><?
|
||||
|
||||
DTree scalar_text;
|
||||
DValue scalar_text;
|
||||
scalar_text = "42.75";
|
||||
print("text -> f64: ", scalar_text.to_f64(), "\n");
|
||||
print("text -> u64: ", scalar_text.to_u64(), "\n");
|
||||
print("text -> s64: ", scalar_text.to_s64(), "\n");
|
||||
|
||||
DTree bool_text;
|
||||
DValue bool_text;
|
||||
bool_text = "yes";
|
||||
print("yes -> bool: ", bool_text.to_bool() ? "true" : "false", "\n");
|
||||
|
||||
DTree single_value_map;
|
||||
DValue single_value_map;
|
||||
single_value_map["value"] = "123";
|
||||
print("{value:123} -> u64: ", single_value_map.to_u64(), "\n");
|
||||
|
||||
DTree headers;
|
||||
DValue headers;
|
||||
headers["Content-Type"] = "text/plain";
|
||||
headers["X-Test"] = "123";
|
||||
print("map -> StringMap:\n", var_dump(headers.to_stringmap()), "\n");
|
||||
@@ -58,12 +58,12 @@ RENDER(Request& context)
|
||||
|
||||
<pre><?
|
||||
|
||||
DTree lookup;
|
||||
DValue lookup;
|
||||
print("has(user) before create: ", lookup.has("user") ? "true" : "false", "\n");
|
||||
print("key(user) before create: ", lookup.key("user") ? "present" : "missing", "\n");
|
||||
lookup.get_or_create("user")->set("Ada");
|
||||
print("has(user) after create: ", lookup.has("user") ? "true" : "false", "\n");
|
||||
if(DTree* user = lookup.key("user"))
|
||||
if(DValue* user = lookup.key("user"))
|
||||
print("key(user) after create: ", user->to_string(), "\n");
|
||||
|
||||
?></pre>
|
||||
@@ -90,11 +90,11 @@ RENDER(Request& context)
|
||||
|
||||
<pre><?
|
||||
|
||||
DTree assoc_left;
|
||||
DValue assoc_left;
|
||||
assoc_left["name"] = "left";
|
||||
assoc_left["shared"] = "left-value";
|
||||
|
||||
DTree assoc_right;
|
||||
DValue assoc_right;
|
||||
assoc_right["shared"] = "right-value";
|
||||
assoc_right["extra"] = "new-value";
|
||||
|
||||
@@ -108,15 +108,15 @@ RENDER(Request& context)
|
||||
|
||||
<pre><?
|
||||
|
||||
DTree list_left;
|
||||
DValue list_left;
|
||||
list_left.set_array();
|
||||
DTree a; a = "A"; list_left.push(a);
|
||||
DTree b; b = "B"; list_left.push(b);
|
||||
DValue a; a = "A"; list_left.push(a);
|
||||
DValue b; b = "B"; list_left.push(b);
|
||||
|
||||
DTree list_right;
|
||||
DValue list_right;
|
||||
list_right.set_array();
|
||||
DTree c; c = "C"; list_right.push(c);
|
||||
DTree d; d = "D"; list_right.push(d);
|
||||
DValue c; c = "C"; list_right.push(c);
|
||||
DValue d; d = "D"; list_right.push(d);
|
||||
|
||||
print("left:\n", var_dump(list_left), "\n");
|
||||
print("right:\n", var_dump(list_right), "\n");
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
// Fault-injection demo. Under the wasm runtime every unit fault is a guest
|
||||
// trap that the workspace turns into a clean 500 without harming the worker;
|
||||
// these modes exercise the three distinct trap causes (mirrors tests/wasm-kill).
|
||||
u64 error_reporting_recurse(volatile u64 depth)
|
||||
{
|
||||
volatile u64 next = depth + 1;
|
||||
return(next + error_reporting_recurse(next));
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
if(!test_demo_request_allowed(context))
|
||||
{
|
||||
test_demo_render_restricted_html(context, "Error reporting", "crash or abort request workers for diagnostics");
|
||||
return;
|
||||
}
|
||||
String mode = context.get["mode"];
|
||||
|
||||
if(mode == "trap")
|
||||
__builtin_trap();
|
||||
if(mode == "recurse")
|
||||
{
|
||||
volatile u64 sink = error_reporting_recurse(0);
|
||||
(void)sink;
|
||||
}
|
||||
if(mode == "loop")
|
||||
{
|
||||
volatile u64 i = 0;
|
||||
while(i >= 0)
|
||||
i++;
|
||||
}
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
Error reporting
|
||||
</h1>
|
||||
<p>These actions trigger guest traps so you can verify that UCE returns a usable `500` response from a healthy worker instead of dropping the upstream connection.</p>
|
||||
<ul>
|
||||
<li><a href="?mode=trap">Trigger an explicit trap (`__builtin_trap`)</a></li>
|
||||
<li><a href="?mode=recurse">Trigger stack exhaustion (unbounded recursion)</a></li>
|
||||
<li><a href="?mode=loop">Trigger a runaway loop (epoch interrupt)</a></li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
@@ -11,7 +11,7 @@ RENDER(Request& context)
|
||||
test_demo_render_restricted_html(context, "File Append", "write to server-side files");
|
||||
return;
|
||||
}
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
@@ -12,7 +12,7 @@ void render_card(String url, String title, String desc)
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree p;
|
||||
DValue p;
|
||||
p.set(context.params);
|
||||
bool allow_server_demos = test_demo_request_allowed(context);
|
||||
|
||||
@@ -27,6 +27,7 @@ RENDER(Request& context)
|
||||
<a class="docs-link" href="../doc/index.uce">API Docs →</a>
|
||||
</h1>
|
||||
<div class="test-grid">
|
||||
|
||||
<div class="grid-heading">Basics</div>
|
||||
<? render_card("hello.uce", "Hello World", "Basic output and server time"); ?>
|
||||
<? render_card("header.uce", "Headers", "HTTP response headers"); ?>
|
||||
@@ -34,10 +35,14 @@ RENDER(Request& context)
|
||||
<? if(allow_server_demos) { render_card("error-reporting.uce", "Error Reporting", "Error handling and output"); } ?>
|
||||
|
||||
<div class="grid-heading">Data Types & Parsing</div>
|
||||
<? render_card("dtree.uce", "DTree", "Dynamic hierarchical data tree"); ?>
|
||||
<? render_card("dvalue.uce", "DValue", "Dynamic hierarchical data tree"); ?>
|
||||
<? render_card("json.uce", "JSON", "Parse and encode JSON data"); ?>
|
||||
<? render_card("xml.uce", "XML", "Structural XML encode/decode with DValue"); ?>
|
||||
<? render_card("yaml.uce", "YAML", "Concise config files with DValue"); ?>
|
||||
<? render_card("preprocessor-comments.uce", "Preprocessor Comments", "Regression coverage for comment parsing in templates"); ?>
|
||||
<? render_card("regex.uce", "Regular Expressions", "PCRE2 matching, captures, replacement, and splitting"); ?>
|
||||
<? render_card("string.uce", "String", "String operations"); ?>
|
||||
<? render_card("collections.uce", "Collection Helpers", "Map, filter, group, pick, and omit render data"); ?>
|
||||
<? render_card("str_replace.uce", "String Replace", "Search and replace in strings"); ?>
|
||||
<? render_card("utf8.uce", "UTF-8", "Unicode string handling"); ?>
|
||||
<? render_card("random.uce", "RNG / Noise", "Random generation and noise"); ?>
|
||||
@@ -52,17 +57,20 @@ RENDER(Request& context)
|
||||
|
||||
<div class="grid-heading">Storage & I/O</div>
|
||||
<? render_card("fileio.uce", "File I/O", "Read and write files"); ?>
|
||||
<? if(allow_server_demos) { render_card("zip.uce", "ZIP", "Create, list, read, and extract ZIP archives"); } ?>
|
||||
<? if(allow_server_demos) { render_card("file_append.uce", "File Append", "Append data to files"); } ?>
|
||||
<? if(allow_server_demos) { render_card("shell.uce", "Shell", "Execute shell commands"); } ?>
|
||||
<? if(allow_server_demos) { render_card("memcached.uce", "Memcached", "Memcached key-value store"); } ?>
|
||||
<? if(allow_server_demos) { render_card("sqlite.uce", "SQLite", "Embedded SQLite database connector"); } ?>
|
||||
<? if(allow_server_demos) { render_card("mysql.uce", "MySQL", "MySQL database connector"); } ?>
|
||||
|
||||
<div class="grid-heading">Advanced</div>
|
||||
<? render_card("call_file.uce", "unit_call()", "Dynamic file inclusion"); ?>
|
||||
<? render_card("components.uce", "Components", "Reusable component system"); ?>
|
||||
<? render_card("once-init.uce", "ONCE / INIT", "Unit lifecycle hooks for worker load and request entry"); ?>
|
||||
<? render_card("markdown.uce", "Markdown", "Markdown parsing with components"); ?>
|
||||
<? render_card("script.uce", "Script", "UCE script integration"); ?>
|
||||
<? render_card("websockets.ws.uce", "WebSockets", "Real-time WebSocket chat"); ?>
|
||||
<? render_card("websockets.ws.uce", "WebSockets", "Live WebSocket chat"); ?>
|
||||
<? render_card("unit-browser.uce", "Unit Browser", "units_list(), unit_info(), unit_compile()"); ?>
|
||||
<? if(allow_server_demos) { render_card("task.uce", "Task", "Background task execution"); } ?>
|
||||
<? if(allow_server_demos) { render_card("task_repeat.uce", "Task Repeat", "Recurring task scheduling"); } ?>
|
||||
@@ -76,10 +84,16 @@ RENDER(Request& context)
|
||||
<div class="system-info">
|
||||
<h3>System Info</h3>
|
||||
<pre><?
|
||||
print("Worker PID: ", my_pid, "\n");
|
||||
print("Parent PID: ", parent_pid, "\n");
|
||||
DValue perf = request_perf();
|
||||
print("Worker PID: ", (u64)perf["worker_pid"].to_u64(), "\n");
|
||||
print("Parent PID: ", (u64)perf["parent_pid"].to_u64(), "\n");
|
||||
print("Request #: ", (u64)perf["request_count"].to_u64(), "\n");
|
||||
print("Accept us: ", (f64)perf["accept_us"].to_f64(), "\n");
|
||||
print("Running us: ", (f64)perf["running_us"].to_f64(), "\n");
|
||||
print("Total us: ", (f64)perf["total_us"].to_f64(), "\n");
|
||||
if(perf["workspace_birth_us"].type != 'S')
|
||||
print("Workspace birth us: ", (u64)perf["workspace_birth_us"].to_u64(), "\n");
|
||||
print("Output buffer size: ", context.ob->str().length(), "\n");
|
||||
print("Request #", context.server->request_count, "\n");
|
||||
?></pre>
|
||||
</div>
|
||||
<? } ?>
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
DTree/JSON
|
||||
DValue/JSON
|
||||
</h1>
|
||||
|
||||
JSON:
|
||||
@@ -1,6 +1,6 @@
|
||||
# Markdown Demo
|
||||
|
||||
This page exercises **strong**, *emphasis*, ~~strikethrough~~, `code spans`, and a bare URL: https://uce.openfu.com/doc/index.uce
|
||||
This page exercises **strong**, *emphasis*, ~~strikethrough~~, `code spans`, and a bare URL: https://example.com/doc/index.uce
|
||||
|
||||
## Task List
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree options;
|
||||
DValue options;
|
||||
options["components"][":::warning"] = "components/markdown/warning";
|
||||
options["components"]["node.code_block"] = "components/markdown/code_block";
|
||||
|
||||
@@ -9,7 +9,7 @@ RENDER(Request& context)
|
||||
file_get_contents("markdown-example.md")
|
||||
);
|
||||
|
||||
DTree ast = markdown_to_ast(markdown_src, options);
|
||||
DValue ast = markdown_to_ast(markdown_src, options);
|
||||
String html = markdown_to_html(markdown_src, options);
|
||||
|
||||
<>
|
||||
@@ -9,7 +9,7 @@ RENDER(Request& context)
|
||||
test_demo_render_restricted_html(context, "MemcacheD", "inspect and mutate local memcached state");
|
||||
return;
|
||||
}
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
@@ -0,0 +1,53 @@
|
||||
static s64 demo_worker_init_count = 0;
|
||||
static s64 demo_component_hits = 0;
|
||||
|
||||
INIT(Request& context)
|
||||
{
|
||||
(void)context;
|
||||
demo_worker_init_count += 1;
|
||||
}
|
||||
|
||||
ONCE(Request& context)
|
||||
{
|
||||
context.call["once_hits"] = context.call["once_hits"].to_s64() + 1;
|
||||
}
|
||||
|
||||
COMPONENT:PROBE(Request& context)
|
||||
{
|
||||
demo_component_hits += 1;
|
||||
|
||||
<>
|
||||
<div class="banner">
|
||||
<strong><?= context.props["label"].to_string() ?></strong>
|
||||
<div>worker INIT count for this loaded unit: <?= (u64)demo_worker_init_count ?></div>
|
||||
<div>request ONCE count for this request: <?= context.call["once_hits"].to_u64() ?></div>
|
||||
<div>component handler calls served by this worker copy: <?= (u64)demo_component_hits ?></div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DValue first;
|
||||
first["label"] = "First component call";
|
||||
|
||||
DValue second;
|
||||
second["label"] = "Second component call in the same request";
|
||||
|
||||
DValue third;
|
||||
third["label"] = "Third component call through unit_call(\"COMPONENT:PROBE\")";
|
||||
|
||||
<><html>
|
||||
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
ONCE() and INIT()
|
||||
</h1>
|
||||
<p>
|
||||
This page calls the same named component twice. `ONCE()` should only run once for the request, while `INIT()` should stay stable for the currently loaded worker copy.
|
||||
</p>
|
||||
<?: component(":PROBE", first, context) ?>
|
||||
<?: component(":PROBE", second, context) ?>
|
||||
<? unit_call("once-init.uce", "COMPONENT:PROBE", &third); ?>
|
||||
</html></>
|
||||
}
|
||||
@@ -11,6 +11,7 @@ RENDER(Request& context)
|
||||
</h1>
|
||||
|
||||
<p>This page exists to prove the template parser ignores quotes and template markers that appear inside C++ comments.</p>
|
||||
<p>It also renders a literal raw-string terminator sequence safely: <code>)"</code>.</p>
|
||||
|
||||
<?
|
||||
// Regression: this comment's apostrophe must not swallow the later ?> marker.
|
||||
@@ -36,4 +37,4 @@ RENDER(Request& context)
|
||||
<pre><?= var_dump(context.params) ?></pre>
|
||||
</details>
|
||||
</>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
#include "demo_guard.h"
|
||||
|
||||
void regex_row(String label, String result, String expect)
|
||||
{
|
||||
bool pass = (result == expect);
|
||||
<><tr>
|
||||
<td class="test-label"><?= label ?></td>
|
||||
<td class="test-result"><code><?= result ?></code></td>
|
||||
<td class="test-status <?= pass ? "pass" : "fail" ?>"><?= pass ? "pass" : "expected: " + expect ?></td>
|
||||
</tr></>
|
||||
}
|
||||
|
||||
void regex_bool_row(String label, bool result, bool expect)
|
||||
{
|
||||
regex_row(label, result ? "true" : "false", expect ? "true" : "false");
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
String text = "Contact ops@example.test or tag #uce, #docs, and café.";
|
||||
DValue first_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", text);
|
||||
DValue tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", text);
|
||||
StringList pieces = regex_split("\\s*,\\s*", "uce, components, markdown");
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<style>
|
||||
table.tests { width: 100%; border-collapse: collapse; margin-bottom: 8px; }
|
||||
table.tests td { padding: 6px 12px; border-bottom: 1px solid rgba(255,255,255,0.06); font-family: var(--font-mono); font-size: 0.88rem; }
|
||||
.test-label { color: var(--text-dim); white-space: nowrap; width: 1%; }
|
||||
.test-result code { background: var(--bg-code); padding: 2px 8px; border-radius: 4px; }
|
||||
.test-status.pass { color: #6f6; width: 1%; }
|
||||
.test-status.fail { color: #f66; }
|
||||
</style>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
Regular Expressions
|
||||
</h1>
|
||||
|
||||
<p>UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DValue values.</p>
|
||||
<p>sample = <code><?= text ?></code></p>
|
||||
|
||||
<h2>Validation And Search</h2>
|
||||
<table class="tests"><?
|
||||
regex_bool_row("regex_match(\"[A-Z][a-z]+\", \"Alice\")", regex_match("[A-Z][a-z]+", "Alice"), true);
|
||||
regex_bool_row("regex_match(\"[A-Z][a-z]+\", \"Alice!\")", regex_match("[A-Z][a-z]+", "Alice!"), false);
|
||||
regex_bool_row("regex_search(\"example\", text)[\"matched\"]", first_email["matched"].to_bool(), true);
|
||||
regex_row("first_email[\"match\"]", first_email["match"].to_string(), "ops@example.test");
|
||||
regex_row("first_email[\"named\"][\"user\"]", first_email["named"]["user"].to_string(), "ops");
|
||||
regex_row("first_email[\"named\"][\"host\"]", first_email["named"]["host"].to_string(), "example.test");
|
||||
regex_bool_row("regex_match(\"\\\\p{L}+\", \"café\")", regex_match("\\p{L}+", "café"), true);
|
||||
?></table>
|
||||
|
||||
<h2>All Matches</h2>
|
||||
<table class="tests"><?
|
||||
regex_row("regex_search_all hashtags count", tags["count"].to_string(), "2.000000");
|
||||
regex_row("first hashtag", tags["matches"]["0"]["named"]["tag"].to_string(), "uce");
|
||||
regex_row("second hashtag", tags["matches"]["1"]["named"]["tag"].to_string(), "docs");
|
||||
?></table>
|
||||
|
||||
<h2>Replace And Split</h2>
|
||||
<table class="tests"><?
|
||||
regex_row("regex_replace(\"#([A-Za-z0-9_]+)\", \"<tag>$1</tag>\", \"#uce\")", regex_replace("#([A-Za-z0-9_]+)", "<tag>$1</tag>", "#uce"), "<tag>uce</tag>");
|
||||
regex_row("join(regex_split(\"\\\\s*,\\\\s*\", ...), \"|\")", join(pieces, "|"), "uce|components|markdown");
|
||||
regex_row("case-insensitive flag", regex_search("uce", "Hello UCE", "i")["match"].to_string(), "UCE");
|
||||
?></table>
|
||||
|
||||
<h2>Structured Result</h2>
|
||||
<pre><?= json_encode(first_email) ?></pre>
|
||||
|
||||
<p><a href="../doc/index.uce?p=regex_search">Read the regex API documentation</a></p>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
// The native SharedUnit loader has been retired. Unit metadata is now
|
||||
// exposed through the wasm membrane via unit_info() (uce_host_units), which
|
||||
// resolves and introspects another unit without dlopen-ing a native .so.
|
||||
DValue info = unit_info("post.uce");
|
||||
print(json_encode(info));
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
if(!test_demo_request_allowed(context))
|
||||
{
|
||||
test_demo_render_restricted_html(context, "SQLite demo", "write to a server-side SQLite database");
|
||||
return;
|
||||
}
|
||||
|
||||
String db_path = "/tmp/uce-demo-sqlite.sqlite";
|
||||
SQLite* db = sqlite_connect(db_path);
|
||||
sqlite_query(db, "create table if not exists notes(id integer primary key autoincrement, body text not null, created_at text not null)");
|
||||
|
||||
if(context.params["REQUEST_METHOD"] == "POST" && trim(context.post["body"]) != "")
|
||||
{
|
||||
StringMap params;
|
||||
params["body"] = trim(context.post["body"]);
|
||||
params["created_at"] = time_format_utc("%Y-%m-%d %H:%M:%S");
|
||||
sqlite_query(db, "insert into notes(body, created_at) values(:body, :created_at)", params);
|
||||
sqlite_disconnect(db);
|
||||
redirect("sqlite.uce", 303);
|
||||
return;
|
||||
}
|
||||
|
||||
DValue notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
|
||||
String error = sqlite_error(db);
|
||||
?><html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
|
||||
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="index.uce">UCE Demo</a> / SQLite</h1>
|
||||
<div class="system-info">
|
||||
<p>This demo stores rows in <code><?= db_path ?></code> with runtime SQLite helpers and named prepared parameters.</p>
|
||||
<form method="post">
|
||||
<input name="body" placeholder="note text"></input>
|
||||
<button type="submit">Add note</button>
|
||||
</form>
|
||||
<? if(error != "ok" && error != "connected") { ?><pre><?= error ?></pre><? } ?>
|
||||
</div>
|
||||
<div class="test-grid">
|
||||
<? notes.each([&](DValue note, String key) { ?>
|
||||
<div class="test-card">
|
||||
<strong>#<?= note["id"].to_string() ?> <?= note["created_at"].to_string() ?></strong>
|
||||
<span><?= note["body"].to_string() ?></span>
|
||||
</div>
|
||||
<? }); ?>
|
||||
</div>
|
||||
</body>
|
||||
</html><?
|
||||
sqlite_disconnect(db);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ RENDER(Request& context)
|
||||
test_demo_render_restricted_html(context, "Tasks", "spawn and inspect background tasks");
|
||||
return;
|
||||
}
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
String task_name = first(context.get["task-name"], "example-task");
|
||||
|
||||
@@ -68,7 +68,7 @@ RENDER(Request& context)
|
||||
|
||||
<pre style="white-space: pre-wrap"><?
|
||||
|
||||
print("New Task ID: ", task("example-task", []() {
|
||||
print("New Task ID: ", task(task_name, []() {
|
||||
|
||||
sleep(10);
|
||||
|
||||
@@ -8,7 +8,7 @@ RENDER(Request& context)
|
||||
test_demo_render_restricted_html(context, "Task repeat", "schedule recurring background tasks");
|
||||
return;
|
||||
}
|
||||
DTree t;
|
||||
DValue t;
|
||||
|
||||
String task_name = first(context.get["task-name"], "example-task");
|
||||
|
||||
@@ -68,7 +68,7 @@ RENDER(Request& context)
|
||||
|
||||
<pre style="white-space: pre-wrap"><?
|
||||
|
||||
print("New Task ID: ", task_repeat("example-task", 5, []() {
|
||||
print("New Task ID: ", task_repeat(task_name, 5, []() {
|
||||
|
||||
sleep(1);
|
||||
|
||||
@@ -27,7 +27,7 @@ String unit_status_class(String compile_status, String error_status)
|
||||
return("status-muted");
|
||||
}
|
||||
|
||||
String unit_flag_label(DTree value)
|
||||
String unit_flag_label(DValue value)
|
||||
{
|
||||
return(value.to_string() == "(true)" ? "✅" : "❌");
|
||||
}
|
||||
@@ -61,7 +61,7 @@ RENDER(Request& context)
|
||||
if(selected_path == "" && unit_paths.size() > 0)
|
||||
selected_path = unit_paths.front();
|
||||
|
||||
DTree selected_info = unit_info(selected_path);
|
||||
DValue selected_info = unit_info(selected_path);
|
||||
String selected_compile_status = selected_info["compile_status"].to_string();
|
||||
String selected_error_status = selected_info["error_status"].to_string();
|
||||
String selected_status_class = unit_status_class(selected_compile_status, selected_error_status);
|
||||
@@ -157,16 +157,16 @@ RENDER(Request& context)
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<strong>Runtime Flags</strong>
|
||||
<span>loaded <?= unit_flag_label(selected_info["loaded"]) ?>, stale <?= unit_flag_label(selected_info["stale"]) ?>, current <?= unit_flag_label(selected_info["current_unit"]) ?></span>
|
||||
<span>wasm <?= unit_flag_label(selected_info["wasm_available"]) ?>, stale <?= unit_flag_label(selected_info["stale"]) ?>, current <?= unit_flag_label(selected_info["current_unit"]) ?></span>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<strong>Artifacts</strong>
|
||||
<code><?= selected_info["so_name"].to_string() ?></code>
|
||||
<code><?= selected_info["wasm_name"].to_string() ?></code>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<strong>Timestamps</strong>
|
||||
<span>Source file: <?= time_format_relative(selected_info["source_mtime"].to_u64()) ?><br>
|
||||
Binary: <?= time_format_relative(selected_info["compiled_mtime"].to_u64()) ?><br>
|
||||
Wasm: <?= time_format_relative(selected_info["compiled_mtime"].to_u64()) ?><br>
|
||||
Meta info: <?= time_format_relative(selected_info["metadata_mtime"].to_u64()) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
RENDER(Request& context)
|
||||
{
|
||||
bool allow_mutation = test_demo_request_allowed(context);
|
||||
DTree payload;
|
||||
DValue payload;
|
||||
|
||||
if(context.get["compile"] != "")
|
||||
{
|
||||
@@ -19,11 +19,11 @@ RENDER(Request& context)
|
||||
|
||||
payload["current"] = unit_info();
|
||||
|
||||
DTree units;
|
||||
DValue units;
|
||||
units.set_array();
|
||||
for(auto& unit_path : units_list())
|
||||
{
|
||||
DTree item;
|
||||
DValue item;
|
||||
item = unit_path;
|
||||
units.push(item);
|
||||
}
|
||||
@@ -8,14 +8,14 @@ String clean_chat_field(String raw, u32 max_length)
|
||||
return(value);
|
||||
}
|
||||
|
||||
DTree chat_event(Request& context, String type, String body)
|
||||
DValue chat_event(Request& context, String type, String body)
|
||||
{
|
||||
DTree event;
|
||||
DValue event;
|
||||
event["type"] = type;
|
||||
event["body"] = body;
|
||||
event["connection_id"] = ws_connection_id();
|
||||
event["scope"] = first(context.params["DOCUMENT_URI"], ws_scope());
|
||||
event["online"] = (f64)ws_connection_count();
|
||||
event["connection_id"] = context.params["WS_CONNECTION_ID"];
|
||||
event["scope"] = first(context.params["WS_DOCUMENT_URI"], context.params["DOCUMENT_URI"]);
|
||||
event["online"] = float_val(first(context.params["WS_CONNECTION_COUNT"], "0"));
|
||||
event["at"] = time_format_utc("%H:%M:%S");
|
||||
event["name"] = context.connection["name"].to_string();
|
||||
event["message_count"] = context.connection["message_count"];
|
||||
@@ -37,6 +37,7 @@ RENDER(Request& context)
|
||||
<pre>Page scope: <?= ws_url ?>
|
||||
Connected right now: <span id="online-count"><?= online ?></span>
|
||||
Status: <span id="status">Connecting...</span></pre>
|
||||
<p>This demo uses <code>context.in</code> for the current payload, <code>context.params["WS_..."]</code> for message metadata, and <code>context.connection</code> for per-client state.</p>
|
||||
|
||||
<form id="chat-form" action="?" method="post">
|
||||
<div>
|
||||
@@ -152,13 +153,13 @@ WS(Request& context)
|
||||
if(ws_is_binary())
|
||||
{
|
||||
ws_send_to(
|
||||
ws_connection_id(),
|
||||
context.params["WS_CONNECTION_ID"],
|
||||
json_encode(chat_event(context, "notice", "Binary messages are not handled by this demo"))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
DTree payload = json_decode(ws_message());
|
||||
DValue payload = json_decode(context.in);
|
||||
String type = clean_chat_field(payload["type"].to_string(), 24);
|
||||
String name = clean_chat_field(payload["name"].to_string(), 32);
|
||||
String body = clean_chat_field(payload["body"].to_string(), 500);
|
||||
@@ -182,7 +183,7 @@ WS(Request& context)
|
||||
context.connection["last_type"] = type;
|
||||
context.connection["last_body"] = body;
|
||||
context.connection["message_count"] = (f64)(message_count + 1);
|
||||
DTree event = chat_event(context, "message", body);
|
||||
DValue event = chat_event(context, "message", body);
|
||||
ws_send(json_encode(event));
|
||||
return;
|
||||
}
|
||||
@@ -190,6 +191,6 @@ WS(Request& context)
|
||||
if(type != "")
|
||||
{
|
||||
context.connection["last_type"] = type;
|
||||
ws_send_to(ws_connection_id(), json_encode(chat_event(context, "notice", "Unknown message type: " + type)));
|
||||
ws_send_to(context.params["WS_CONNECTION_ID"], json_encode(chat_event(context, "notice", "Unknown message type: " + type)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DValue book;
|
||||
book["name"] = "book";
|
||||
book["attrs"]["id"] = "b1";
|
||||
book["attrs"]["type"] = "reference";
|
||||
|
||||
DValue title;
|
||||
title["name"] = "title";
|
||||
title["text"] = "UCE & XML";
|
||||
book["children"].push(title);
|
||||
|
||||
DValue chapter;
|
||||
chapter["name"] = "chapter";
|
||||
chapter["attrs"]["number"] = "1";
|
||||
chapter["text"] = "Structural conversion without schema validation.";
|
||||
book["children"].push(chapter);
|
||||
|
||||
String encoded = xml_encode(book);
|
||||
DValue decoded = xml_decode(encoded);
|
||||
|
||||
DValue simple;
|
||||
simple["title"] = "Hello";
|
||||
simple["count"] = "3";
|
||||
|
||||
String incoming = "<note priority=\"high\"><to>UCE</to><body><![CDATA[5 < 6]]></body><symbol>AB</symbol></note>";
|
||||
DValue incoming_tree = xml_decode(incoming);
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
XML
|
||||
</h1>
|
||||
|
||||
<p><code>xml_encode()</code> and <code>xml_decode()</code> convert between XML strings and element-shaped <code>DValue</code> values without schema validation.</p>
|
||||
|
||||
<h2>Encoded Element Tree</h2>
|
||||
<pre><?= encoded ?></pre>
|
||||
|
||||
<h2>Decoded DValue</h2>
|
||||
<pre><?= json_encode(decoded) ?></pre>
|
||||
|
||||
<h2>Simple Map Encoding</h2>
|
||||
<pre><?= xml_encode(simple, "payload") ?></pre>
|
||||
|
||||
<h2>Decode Existing XML</h2>
|
||||
<p>Input XML:</p>
|
||||
<pre><?= incoming ?></pre>
|
||||
<p>Decoded DValue:</p>
|
||||
<pre><?= json_encode(incoming_tree) ?></pre>
|
||||
|
||||
<p>
|
||||
<a href="../doc/index.uce?p=xml_encode">xml_encode() docs</a>
|
||||
|
|
||||
<a href="../doc/index.uce?p=xml_decode">xml_decode() docs</a>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
String source = "# UCE app config\n"
|
||||
"app:\n"
|
||||
" name: UCE Starter\n"
|
||||
" debug: true\n"
|
||||
" port: 8080\n"
|
||||
" paths:\n"
|
||||
" - site\n"
|
||||
" - cache\n"
|
||||
"message: |\n"
|
||||
" Keep config files readable.\n"
|
||||
" Load them as DValue values.\n";
|
||||
|
||||
DValue cfg = yaml_decode(source);
|
||||
String encoded = yaml_encode(cfg);
|
||||
DValue roundtrip = yaml_decode(encoded);
|
||||
|
||||
DValue generated;
|
||||
generated["database"]["host"] = "localhost";
|
||||
generated["database"]["port"] = (f64)3306;
|
||||
generated["features"]["components"].set_bool(true);
|
||||
generated["features"]["markdown"].set_bool(true);
|
||||
|
||||
DValue theme;
|
||||
theme = "clean";
|
||||
generated["themes"].push(theme);
|
||||
theme = "compact";
|
||||
generated["themes"].push(theme);
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
YAML
|
||||
</h1>
|
||||
|
||||
<p><code>yaml_encode()</code> and <code>yaml_decode()</code> convert concise config-style YAML to and from <code>DValue</code> values.</p>
|
||||
|
||||
<h2>Config Source</h2>
|
||||
<pre><?= source ?></pre>
|
||||
|
||||
<h2>Decoded DValue</h2>
|
||||
<pre><?= json_encode(cfg) ?></pre>
|
||||
|
||||
<h2>Encoded Again</h2>
|
||||
<pre><?= encoded ?></pre>
|
||||
|
||||
<h2>Generated Config</h2>
|
||||
<pre><?= yaml_encode(generated) ?></pre>
|
||||
|
||||
<h2>Round Trip Reads</h2>
|
||||
<pre><?
|
||||
print("app.name = ", roundtrip["app"]["name"].to_string(), "\n");
|
||||
print("app.debug = ", roundtrip["app"]["debug"].to_bool() ? "true" : "false", "\n");
|
||||
print("app.port = ", std::to_string(roundtrip["app"]["port"].to_s64()), "\n");
|
||||
print("app.paths[1] = ", roundtrip["app"]["paths"]["1"].to_string(), "\n");
|
||||
?></pre>
|
||||
|
||||
<p>
|
||||
<a href="../doc/index.uce?p=yaml_encode">yaml_encode() docs</a>
|
||||
|
|
||||
<a href="../doc/index.uce?p=yaml_decode">yaml_decode() docs</a>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
if(!test_demo_request_allowed(context))
|
||||
{
|
||||
test_demo_render_restricted_html(context, "ZIP Demo", "create and extract temporary server-side archive files");
|
||||
return;
|
||||
}
|
||||
|
||||
String base = "/tmp/uce-demo-zip";
|
||||
String archive = path_join(base, "demo.zip");
|
||||
String extract_dir = path_join(base, "extract");
|
||||
mkdir(base);
|
||||
mkdir(extract_dir);
|
||||
|
||||
DValue entries;
|
||||
entries["hello.txt"] = "Hello from a generated ZIP archive.\n";
|
||||
entries["notes/readme.txt"] = "zip_create(), zip_list(), zip_read(), and zip_extract() are available to UCE pages.\n";
|
||||
zip_create(archive, entries);
|
||||
|
||||
DValue listing = zip_list(archive);
|
||||
String hello = zip_read(archive, "hello.txt");
|
||||
zip_extract(archive, extract_dir);
|
||||
String extracted = file_get_contents(path_join(extract_dir, "notes/readme.txt"));
|
||||
String gz_source = "This string was compressed with gz_compress() and restored with gz_uncompress().";
|
||||
String gz_body = gz_compress(gz_source);
|
||||
String gz_roundtrip = gz_uncompress(gz_body);
|
||||
|
||||
?><html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
|
||||
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="index.uce">UCE Test Suite</a> / ZIP</h1>
|
||||
<p>This demo creates a temporary archive at <code><?= archive ?></code>, lists it, reads one member, and extracts it under <code><?= extract_dir ?></code>.</p>
|
||||
<h2>zip_list()</h2>
|
||||
<pre><?= json_encode(listing) ?></pre>
|
||||
<h2>zip_read()</h2>
|
||||
<pre><?= hello ?></pre>
|
||||
<h2>Extracted File</h2>
|
||||
<pre><?= extracted ?></pre>
|
||||
<h2>gzip Helpers</h2>
|
||||
<p>Source bytes: <?= std::to_string((u64)gz_source.size()) ?>; compressed bytes: <?= std::to_string((u64)gz_body.size()) ?></p>
|
||||
<pre><?= gz_roundtrip ?></pre>
|
||||
</body>
|
||||
</html><?
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
Markup Functions
|
||||
markdown_to_ast
|
||||
markdown_to_html
|
||||
xml_decode
|
||||
xml_encode
|
||||
yaml_decode
|
||||
yaml_encode
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user