commit 9648b9e8dd064f1dab11d974d0f7931a86943c71 Author: Codex Bot Date: Sat Oct 17 23:42:39 2026 +0800 Initialize Aster VM and deploy PostgreSQL host on ces-control diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..12ecee6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +target +data +work +outputs +.env +*.db +*.db-shm +*.db-wal diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..451be57 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Docker Compose reads .env automatically; cargo programs read exported env only. +ASTER_PORT=8080 +# Local host settings (export explicitly when running cargo): +ASTER_LISTEN=127.0.0.1:8080 +SQLITE_PATH=data/aster.db +# Unset DATABASE_URL to use SQLite. Do not set it to an empty string. +# DATABASE_URL=postgres://aster:password@127.0.0.1:5432/aster +# PostgreSQL Compose only: replace with a random hex string, e.g. openssl rand -hex 24. +# Hex avoids URL-encoding ambiguity when used in the Compose connection URL. +POSTGRES_PASSWORD=replace-with-a-random-hex-password diff --git a/.gitea/workflows/ci-deploy.yml b/.gitea/workflows/ci-deploy.yml new file mode 100644 index 0000000..c92ac88 --- /dev/null +++ b/.gitea/workflows/ci-deploy.yml @@ -0,0 +1,33 @@ +name: Verify databases and deploy Aster VM + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: endorphin-aster-vm-production + cancel-in-progress: false + +jobs: + verify-and-deploy: + runs-on: ces-control + timeout-minutes: 60 + env: + DOCKER_BUILDKIT: "0" + ASTER_RELEASE_SHA: ${{ gitea.sha }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Verify SQLite, PostgreSQL and host behavior + run: sh scripts/ci-ces-control.sh + + - name: Build production image + run: docker build --target runtime -t "endorphin-aster-vm:$ASTER_RELEASE_SHA" . + + - name: Deploy persistent PostgreSQL host + if: gitea.ref == 'refs/heads/main' + run: sh scripts/deploy-ces-control.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b014fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target/ +/data/ +/work/ +/outputs/ +.env +*.db +*.db-shm +*.db-wal +__pycache__/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d5b6b22 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,27 @@ +# Aster VM 工作约定 + +## 项目边界 + +- 用中文沟通与编写交接文档。内核只负责 VM、堆和对象语义;数据库、网络、浏览器能力属于宿主。 +- 不实现词法、语法、渲染器、完整元类体系或默认 Python 兼容性。计划与已实现功能须分清。 +- `aster-core` 保持零依赖。类对象及描述在堆中,实例引用类;根类 Object 唯一且没有父类。 +- 句柄通过 VM 检查归属及对象种类,不暴露原始指针,不允许宿主构造堆索引。 +- 默认持久化 SQLite;显式 PostgreSQL 配置失败必须报错,不静默降级。存储不代表 VM 堆快照。 +- 用户明确禁止本地测试:不在本地运行测试或冒烟脚本,不为本地测试安装/下载 SQLite 或其他依赖。检查脚本保留供用户未来指定的 CI/部署环境使用;静态文件检查与 Git 配置检查可执行。未执行的验证须如实说明。 +- 已指定 ces-control 为远端 CI/部署环境;CI 必须验证 SQLite 与真实 PostgreSQL,正式持久部署选择 PostgreSQL。禁止改动其他项目或共享 Runner,特别是 autodl-gpu-runner-controller。 +- 已按用户提供的准确 URL 配置 origin;不创建远程仓库、不猜测 URL。未经用户明确授权,不 commit、不 push。 + +## Gitea Entroia 身份 + +- 对远程 host 为 `gitea.entroia.com` 的仓库,只使用专用账户 `codex-bot`;不得使用 `Zayden-Jung` 身份或其 SSH 密钥、凭据。 +- 提交前在仓库范围设置 author:`Codex Bot` / `codex-bot@gitea.entroia.local`,不影响其他仓库。 +- SSH 必须隔离现有配置和身份:`ssh -F /dev/null -o IdentitiesOnly=yes -i /Users/zzyang/.codex/credentials/gitea-entroia/codex_entroia_ed25519 -p 22222`。在仓库范围设置 `core.sshCommand` 为该命令。 +- 远程格式:`ssh://git@gitea.entroia.com:22222//.git`。SSH 用户为 `git`,由专用密钥识别为 `codex-bot`。owner 与认证身份分开,即使 owner 是 `Zayden-Jung` 也不切换凭据。 +- Gitea API/HTTPS 认证只能在进程内从 macOS Keychain 读取 service `gitea.entroia.com`、account `codex-bot` 的密码。禁止打印、记录、提交、写入仓库或指令文件;禁止将密码或 Authorization 头放入命令行参数或磁盘。 +- bot 权限不足时完成能做的本地工作,再请用户为 bot 授权,不借用其他账户。 + +## Git 操作后报告 + +- 每次 commit 或 push 后,中文最终报告须覆盖本次所有文件,包括新增、删除和重命名;说明各文件的功能部分、具体变化及目的。可按共同目的分组,无需粘贴源码、行号或逐行 diff。 +- 同时简报 commit 标识、push 是否成功、测试或检查结果;未执行或失败如实说明。 +- 这些是面向用户的结果报告要求,不写入 commit message;commit message 只清晰描述实际变更。 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..724fa64 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1873 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "aster-cli" +version = "0.1.0" +dependencies = [ + "aster-core", + "aster-storage", + "tokio", +] + +[[package]] +name = "aster-core" +version = "0.1.0" + +[[package]] +name = "aster-server" +version = "0.1.0" +dependencies = [ + "aster-core", + "aster-storage", + "axum", + "serde", + "tokio", +] + +[[package]] +name = "aster-storage" +version = "0.1.0" +dependencies = [ + "sqlx", + "tempfile", + "tokio", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "tokio", + "url", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-normalization" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2d0ca43 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[workspace] +members = ["crates/aster-core", "crates/aster-storage", "crates/aster-cli", "crates/aster-server"] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.91" +license = "MIT" + +[workspace.dependencies] +aster-core = { path = "crates/aster-core" } +aster-storage = { path = "crates/aster-storage" } +axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio", "tls-rustls-ring", "sqlite", "postgres"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "fs"] } + +[workspace.lints.rust] +unsafe_code = "forbid" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dc6d552 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM rust:1.91.1-bookworm AS source +WORKDIR /build +ENV CARGO_BUILD_JOBS=1 +COPY Cargo.toml Cargo.lock ./ +COPY crates ./crates +COPY scripts ./scripts + +FROM source AS ci +RUN apt-get update && apt-get install -y --no-install-recommends python3 \ + && rm -rf /var/lib/apt/lists/* \ + && rustup component add rustfmt clippy +ENV CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 +CMD ["sh", "scripts/check.sh"] + +FROM source AS build +RUN cargo build --locked --release -p aster-server -p aster-cli + +FROM debian:bookworm-slim AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --system --uid 10001 --create-home aster \ + && mkdir -p /data && chown aster:aster /data +COPY --from=build /build/target/release/aster-server /usr/local/bin/aster-server +COPY --from=build /build/target/release/aster /usr/local/bin/aster +USER 10001 +WORKDIR /data +ENV ASTER_LISTEN=0.0.0.0:8080 SQLITE_PATH=/data/aster.db +EXPOSE 8080 +HEALTHCHECK --interval=10s --timeout=6s --start-period=10s --retries=3 \ + CMD curl --fail --silent http://127.0.0.1:8080/health || exit 1 +CMD ["aster-server"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4a8dfea --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Aster VM contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c041d1e --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +# Aster VM + +Rust 编写的可嵌入语言运行时,当前为第一阶段:启动时引导根类 `Object`,并由对象工厂创建类和实例。这里的“顶级类”指根类;“生成器”指运行时引导与对象工厂。 + +本项目尚无词法/语法前端、字节码执行器、渲染器、GC 或完整元类体系,不承诺 Python 兼容。数据库只保存宿主元数据,不保存或恢复 VM 堆。 + +## 结构 + +| crate | 职责 | +| --- | --- | +| `aster-core` | 零外部依赖的 `Value`、安全句柄、堆、类、实例、VM 与工厂 | +| `aster-storage` | SQLx PostgreSQL/SQLite 适配、元数据表初始化、读写接口;不依赖内核 | +| `aster-cli` | `aster bootstrap` 演示,以及宿主元数据读写 | +| `aster-server` | 最小常驻宿主,仅提供健康检查与根类信息,供 Docker 等方式运行 | + +依赖方向:CLI/Server → Core 和 Storage;Core 与 Storage 互不依赖。未来浏览器宿主的网络、DOM、渲染等能力应留在宿主一侧。 + +## 直接运行 + +使用 Rust 1.91 或更高兼容版本;当前实测版本为 1.91.1,依赖锁定在 `Cargo.lock`。不需要 Docker。首次构建需要访问 crates.io。 + +```sh +cargo build --workspace --locked +cargo run -p aster-cli -- bootstrap +``` + +预期输出含 `root=Object parent=None`、`instances_distinct=true heap_objects=4`。这条命令不读取数据库配置、不打开数据库;即使设置了错误的 `DATABASE_URL` 也不影响引导。 + +```rust +use aster_core::{Value, Vm}; + +fn main() -> Result<(), aster_core::VmError> { + let mut vm = Vm::new()?; + let point = vm.create_class("Point", vm.root_class())?; + let instance = vm.create_instance(point)?; + vm.set_field(instance, "x", Value::Integer(42))?; + assert_eq!(vm.instance(instance)?.class(), point); + Ok(()) +} +``` + +嵌入时将 `aster-core` 加为 Cargo 路径依赖即可。上例与 crate 文档中的可运行示例语义相同。 + +## 宿主存储 + +不设置 `DATABASE_URL` 时,使用 `SQLITE_PATH` 指定的 SQLite 文件,默认 `data/aster.db`,相对于进程工作目录;自动创建父目录及文件。部署时必须把该目录放在持久卷上。`SQLITE_PATH=:memory:` 仅用于临时运行/测试,不持久化。 + +```sh +cargo run -p aster-cli -- metadata put project 'Aster VM' +cargo run -p aster-cli -- metadata get project +SQLITE_PATH=work/custom/aster.db cargo run -p aster-cli -- metadata put stage bootstrap +``` + +PostgreSQL 通过导出的 `DATABASE_URL=postgres://...` 或 `postgresql://...` 配置,优先于 `SQLITE_PATH`。显式空值、错误协议、连接失败都会返回错误并以非零状态退出,绝不静默降级。`DATABASE_URL` 不接受 SQLite URL;SQLite 使用独立路径配置。 + +连接成功后执行 `schema.sql` 中幂等的 v1 建表语句,提供 `Storage::put/get/ping/close`。读写使用参数绑定,重复键执行更新,缺失键返回 `None`。尚无复杂迁移版本管理。 + +Cargo 程序**不自动加载 `.env`**,请导出需要的环境变量。`.env.example` 是配置示例;Docker Compose 会自动读取复制后的 `.env`。连接 URL 可能含密码,不要贴到日志或提交到 Git。命令行只显示脱敏错误,底层诊断作为 Rust error source 保留给宿主。 + +## 常驻宿主 + +```sh +cargo run -p aster-server +curl --fail http://127.0.0.1:8080/health +curl --fail http://127.0.0.1:8080/bootstrap +``` + +`ASTER_LISTEN` 默认 `127.0.0.1:8080`。宿主启动时创建 VM、连接数据库、初始化表并写入 `host.version=0.1.0`;初始化失败即退出。`/health` 实际探测数据库,成功返回 200,失联返回 503;`/bootstrap` 返回 `Object`、无父类、堆对象数 1,多次读取不会重复引导。支持 Ctrl-C/Unix SIGTERM 优雅退出。 + +这两个端点不提供执行语言、对象写入或业务 API。服务没有认证层;示例 Compose 端口仅绑定宿主回环地址,外部访问由后续宿主部署决定。 + +## Docker:默认 SQLite + +```sh +cp .env.example .env +docker compose up --build -d +curl --fail http://127.0.0.1:8080/health +docker compose exec aster aster metadata put project 'Aster VM' +docker compose exec aster aster metadata get project +docker compose down +docker compose up -d +docker compose exec aster aster metadata get project +``` + +容器以 UID 10001 运行,`aster-sqlite` 命名卷挂载到 `/data`,保存数据库和 WAL 文件。`down` 保留卷;`down -v` 会删除持久数据。构建采用多阶段 Dockerfile,运行镜像同时含 `aster-server` 和 `aster`。Compose 的 SQLite 方案不会透传宿主 `DATABASE_URL`,以保证默认方案明确使用 SQLite。 + +## Docker:选择 PostgreSQL + +先将 `.env` 中的 `POSTGRES_PASSWORD` 换成随机十六进制字符串(可用 `openssl rand -hex 24` 生成)。该值同时用于数据库和连接 URL;使用其他字符时需要自行正确处理 URL 编码。 + +```sh +docker compose -f docker-compose.postgres.yml up --build -d +docker compose -f docker-compose.postgres.yml exec aster aster metadata put project 'Aster VM' +docker compose -f docker-compose.postgres.yml exec aster aster metadata get project +docker compose -f docker-compose.postgres.yml down +``` + +这是**独立替代方案**,只传入这一个 Compose 文件,不与默认文件叠加;两个方案不要同时占用同一个宿主端口。PostgreSQL 17 使用 `aster-postgres` 命名卷,数据库不向宿主公开端口。示例内部网络禁用数据库 TLS;跨不可信网络的部署应使用 PostgreSQL TLS 验证配置,例如 URL 参数 `sslmode=verify-full`。更改已有 PostgreSQL 卷的密码需要数据库管理操作,修改 `.env` 不会修改旧卷中的用户密码。 + +## ces-control CI/CD 与持久化部署 + +正式部署选择 PostgreSQL;默认 SQLite Compose 仍保留作为独立运行选项。 + +`.gitea/workflows/ci-deploy.yml` 在 `ces-control` Runner 上响应 `main` push 和手动触发:先在远端 Docker 容器中执行 fmt、workspace 测试、严格 Clippy、真实 PostgreSQL 集成测试和 SQLite/HTTP 冒烟检查,通过后构建生产镜像,再部署。使用独立的临时 PostgreSQL 容器做测试,不连接生产数据库。测试容器与网络在退出时清理,编译缓存仅保留在远端专属 Docker 卷中。 + +正式服务由 `docker-compose.ces-control.yml` 管理,默认仅绑定 `127.0.0.1:18081`,内部连接 PostgreSQL 17。应用/数据库使用 `unless-stopped` 自动重启,日志轮换;数据库不发布端口。部署使用 commit SHA 标记镜像与 release 目录,健康检查或元数据读写失败时尝试恢复上一版应用,保留数据库。 + +| 服务器路径 | 用途 | +| --- | --- | +| `/opt/endorphin-aster-vm/releases//` | 固定版本的 Compose 与镜像配置 | +| `/opt/endorphin-aster-vm/current` | 最近健康版本的符号链接 | +| `/etc/endorphin-aster-vm/production.env` | 首次自动生成的数据库密码和端口,权限 0600 | +| `/var/lib/endorphin-aster-vm/postgres` | PostgreSQL 持久数据,替换应用/数据库容器后保留 | + +无需在 Gitea 中设置生产数据库密码。不要删除或重新生成已有数据库对应的 `production.env`;修改密码需先通过 PostgreSQL 管理操作完成同步。首次部署没有可用的旧版可以回退;数据库 schema 的未来升级也需要另行设计兼容性,应用回退不等于回退数据库。 + +在服务器查看/停止当前部署(停止不删除数据): + +```sh +docker compose -p endorphin-aster-vm \ + --env-file /etc/endorphin-aster-vm/production.env \ + --env-file /opt/endorphin-aster-vm/current/release.env \ + -f /opt/endorphin-aster-vm/current/compose.yml ps +curl --fail http://127.0.0.1:18081/health +curl --fail http://127.0.0.1:18081/bootstrap +# 同样参数接 stop 停服,接 up -d --wait 恢复。 +``` + +本机不执行上述 CI 测试,也不为其安装 Rust/SQLite/PostgreSQL。正式部署与测试只运行在用户指定的 `ces-control`。该工作流不改动 `autodl-gpu-runner-controller`、rednote 或 TiktokUI。工作流语义依据 [Gitea Actions 文档](https://docs.gitea.com/usage/actions/),环境文件加载依据 [Docker Compose 文档](https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/)。 + +## 验证(在用户指定的环境执行) + +当前工作约定禁止在本地运行测试,也不为本地测试下载依赖。下面的脚本和示例在指定的 ces-control CI/部署环境执行,不代表每次修改后已全部执行。实际结果及此前清理情况见 [验证记录](docs/validation.md)。 + +```sh +sh scripts/check.sh +python3 scripts/smoke.py +``` + +检查脚本运行 fmt、workspace 测试和拒绝所有 warning 的 Clippy;冒烟脚本使用临时 SQLite 文件,验证独立引导、跨进程持久化、配置错误、HTTP 端点和 SIGTERM 退出,不需要外部数据库。 + +PostgreSQL 集成测试默认显式忽略,需要测试数据库。以下是指定验证环境中的 Docker 示例;仅操作专用测试容器,不使用生产库,当前不在本机执行: + +```sh +docker run --rm -d --name aster-pg-test \ + -e POSTGRES_PASSWORD=aster-test -e POSTGRES_USER=aster -e POSTGRES_DB=aster \ + -p 127.0.0.1:55432:5432 postgres:17-bookworm +# 等待以下命令成功后再运行测试: +docker exec aster-pg-test pg_isready -U aster -d aster +ASTER_TEST_POSTGRES_URL='postgres://aster:aster-test@127.0.0.1:55432/aster?sslmode=disable' \ + cargo test -p aster-storage --test postgres --locked -- --ignored +docker stop aster-pg-test +``` + +测试会留下唯一键的元数据,专用测试库可随容器删除。缺少测试 URL 时显式运行该测试会失败,不会假装通过。真实 PostgreSQL 成功路径及容器结果以 [验证记录](docs/validation.md) 为准。 + +## 当前语义与后续开发 + +- `Object` 是每个 VM 启动时唯一分配的根类,没有父类。类描述与类对象合为一个堆对象;实例保存类句柄。普通类必须指定已有父类,名称只是标签,无全局命名空间;保留精确名称 `Object`,其他同名类允许存在。 +- 对外只提供私有字段的 `ObjectRef`/`ClassRef`/`InstanceRef`。句柄包含进程内 VM 身份与槽位,所有访问检查归属、范围、种类;写入对象字段也检查引用归属。句柄不是持久 ID,不支持外部构造或反序列化。 +- 堆仅分配,不单独回收、不复用槽位;随 VM 销毁统一释放。宿主持有的句柄不延长 VM 生命周期。未来 GC 需要另行设计根集、代际槽位或稳定间接句柄,不能直接开始复用当前索引。 +- Nil/布尔/整数/浮点直接放入 `Value`,字符串由 Rust `String` 持有,类和实例使用堆句柄。尚未定义完整语言数值、方法查找、继承执行或异常语义。 + +下一阶段建议先确定字段/方法查找与宿主调用边界,再设计可测的最小指令执行循环;等对象可达性规则明确后引入 GC。详见 [架构](docs/architecture.md) 与 [开发交接](docs/development.md)。 + +依赖 API 依据 [SQLx 0.8.6 官方文档](https://docs.rs/sqlx/0.8.6/sqlx/) 和 [Axum 官方文档](https://docs.rs/axum/0.8.9/axum/) 实现。SQLx 0.9 要求更高 Rust 版本,本阶段使用与本机工具链兼容的 0.8.6;确切解析版本以锁文件为准。 diff --git a/crates/aster-cli/Cargo.toml b/crates/aster-cli/Cargo.toml new file mode 100644 index 0000000..e3d91d5 --- /dev/null +++ b/crates/aster-cli/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "aster-cli" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[[bin]] +name = "aster" +path = "src/main.rs" + +[dependencies] +aster-core.workspace = true +aster-storage.workspace = true +tokio.workspace = true + +[lints] +workspace = true diff --git a/crates/aster-cli/src/main.rs b/crates/aster-cli/src/main.rs new file mode 100644 index 0000000..0cdc32b --- /dev/null +++ b/crates/aster-cli/src/main.rs @@ -0,0 +1,66 @@ +use std::error::Error; +use std::process::ExitCode; + +use aster_core::{Value, Vm}; +use aster_storage::{Storage, StorageConfig}; + +const USAGE: &str = "Usage: aster [bootstrap | metadata put | metadata get ]"; + +#[tokio::main] +async fn main() -> ExitCode { + match run().await { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("aster: {error}"); + ExitCode::FAILURE + } + } +} + +async fn run() -> Result<(), Box> { + let args: Vec = std::env::args().skip(1).collect(); + match args + .iter() + .map(String::as_str) + .collect::>() + .as_slice() + { + [] | ["bootstrap"] => { + let mut vm = Vm::new()?; + let root = vm.bootstrap(); + let class = vm.create_class("Example", root)?; + let first = vm.create_instance(class)?; + let second = vm.create_instance(class)?; + vm.set_field(first, "message", Value::Text("Hello from Aster VM".into()))?; + println!( + "root={} parent={:?}", + vm.class(root)?.name(), + vm.class(root)?.parent() + ); + println!( + "class={} instances_distinct={} heap_objects={}", + vm.class(class)?.name(), + first != second, + vm.object_count() + ); + println!("bootstrap is independent of host storage"); + } + ["help"] | ["--help"] | ["-h"] => println!("{USAGE}"), + ["metadata", "put", key, value] => { + let store = Storage::connect(&StorageConfig::from_env()?).await?; + store.put(key, value).await?; + println!("stored ({})", store.backend_name()); + store.close().await; + } + ["metadata", "get", key] => { + let store = Storage::connect(&StorageConfig::from_env()?).await?; + println!( + "{}", + store.get(key).await?.as_deref().unwrap_or("(missing)") + ); + store.close().await; + } + _ => return Err(USAGE.into()), + } + Ok(()) +} diff --git a/crates/aster-core/Cargo.toml b/crates/aster-core/Cargo.toml new file mode 100644 index 0000000..2c1056a --- /dev/null +++ b/crates/aster-core/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "aster-core" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[lints] +workspace = true diff --git a/crates/aster-core/src/heap.rs b/crates/aster-core/src/heap.rs new file mode 100644 index 0000000..1607363 --- /dev/null +++ b/crates/aster-core/src/heap.rs @@ -0,0 +1,187 @@ +use std::collections::BTreeMap; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::VmError; + +static NEXT_HEAP_ID: AtomicU64 = AtomicU64::new(1); + +/// Opaque process-local identity. Cannot be constructed or deserialized by callers. +/// Handles do not keep the VM alive; every dereference goes through its owner VM. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ObjectRef { + heap_id: u64, + index: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ClassRef(pub(crate) ObjectRef); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct InstanceRef(pub(crate) ObjectRef); + +impl From for ObjectRef { + fn from(value: ClassRef) -> Self { + value.0 + } +} + +impl From for ObjectRef { + fn from(value: InstanceRef) -> Self { + value.0 + } +} + +/// Primitive values are immediate; only classes and instances live on the heap. +#[derive(Debug, Clone, PartialEq)] +pub enum Value { + Nil, + Bool(bool), + Integer(i64), + Float(f64), + Text(String), + Object(ObjectRef), +} + +/// The class object and its immutable description are one heap allocation. +/// There is deliberately no metaclass or implicit class-of-class relationship. +#[derive(Debug)] +pub struct Class { + pub(crate) name: String, + pub(crate) parent: Option, +} + +impl Class { + pub fn name(&self) -> &str { + &self.name + } + + pub fn parent(&self) -> Option { + self.parent + } +} + +#[derive(Debug)] +pub struct Instance { + pub(crate) class: ClassRef, + pub(crate) fields: BTreeMap, +} + +impl Instance { + pub fn class(&self) -> ClassRef { + self.class + } + + pub fn field(&self, name: &str) -> Option<&Value> { + self.fields.get(name) + } +} + +#[derive(Debug)] +pub enum Object { + Class(Class), + Instance(Instance), +} + +/// Allocation-only arena. No deletion or slot reuse until a GC model is chosen. +#[derive(Debug)] +pub(crate) struct Heap { + id: u64, + objects: Vec, +} + +impl Heap { + pub(crate) fn new() -> Result { + let id = NEXT_HEAP_ID + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)) + .map_err(|_| VmError::IdentityExhausted)?; + Ok(Self { + id, + objects: Vec::new(), + }) + } + + pub(crate) fn allocate(&mut self, object: Object) -> ObjectRef { + let reference = ObjectRef { + heap_id: self.id, + index: self.objects.len(), + }; + self.objects.push(object); + reference + } + + pub(crate) fn get(&self, reference: ObjectRef) -> Result<&Object, VmError> { + self.validate_owner(reference)?; + self.objects + .get(reference.index) + .ok_or(VmError::InvalidHandle) + } + + pub(crate) fn get_mut(&mut self, reference: ObjectRef) -> Result<&mut Object, VmError> { + self.validate_owner(reference)?; + self.objects + .get_mut(reference.index) + .ok_or(VmError::InvalidHandle) + } + + fn validate_owner(&self, reference: ObjectRef) -> Result<(), VmError> { + if reference.heap_id != self.id { + return Err(VmError::ForeignHandle); + } + Ok(()) + } + + pub(crate) fn len(&self) -> usize { + self.objects.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_index_is_an_error_for_both_access_paths() { + let mut heap = Heap::new().unwrap(); + let invalid = ObjectRef { + heap_id: heap.id, + index: usize::MAX, + }; + assert!(matches!(heap.get(invalid), Err(VmError::InvalidHandle))); + assert!(matches!(heap.get_mut(invalid), Err(VmError::InvalidHandle))); + } + + #[test] + fn typed_handles_are_checked_at_runtime_too() { + let mut vm = crate::Vm::new().unwrap(); + let root = vm.root_class(); + let instance = vm.create_instance(root).unwrap(); + assert!(matches!( + vm.class(ClassRef(instance.0)), + Err(VmError::ExpectedClass) + )); + assert!(matches!( + vm.instance(InstanceRef(root.0)), + Err(VmError::ExpectedInstance) + )); + } + + #[test] + fn invalid_value_reference_cannot_replace_a_field() { + let mut vm = crate::Vm::new().unwrap(); + let root = vm.root_class(); + let instance = vm.create_instance(root).unwrap(); + vm.set_field(instance, "field", Value::Integer(1)).unwrap(); + let invalid = ObjectRef { + heap_id: root.0.heap_id, + index: usize::MAX, + }; + assert_eq!( + vm.set_field(instance, "field", Value::Object(invalid)), + Err(VmError::InvalidHandle) + ); + assert_eq!( + vm.instance(instance).unwrap().field("field"), + Some(&Value::Integer(1)) + ); + } +} diff --git a/crates/aster-core/src/lib.rs b/crates/aster-core/src/lib.rs new file mode 100644 index 0000000..d51ee3a --- /dev/null +++ b/crates/aster-core/src/lib.rs @@ -0,0 +1,17 @@ +//! Embeddable object runtime. No database, network, parser or executor dependencies. +//! +//! ``` +//! use aster_core::{Value, Vm}; +//! let mut vm = Vm::new()?; +//! let point = vm.create_class("Point", vm.root_class())?; +//! let instance = vm.create_instance(point)?; +//! vm.set_field(instance, "x", Value::Integer(42))?; +//! assert_eq!(vm.instance(instance)?.class(), point); +//! # Ok::<(), aster_core::VmError>(()) +//! ``` + +mod heap; +mod vm; + +pub use heap::{Class, ClassRef, Instance, InstanceRef, Object, ObjectRef, Value}; +pub use vm::{Vm, VmError}; diff --git a/crates/aster-core/src/vm.rs b/crates/aster-core/src/vm.rs new file mode 100644 index 0000000..c984f48 --- /dev/null +++ b/crates/aster-core/src/vm.rs @@ -0,0 +1,146 @@ +use std::collections::BTreeMap; +use std::fmt; + +use crate::heap::Heap; +use crate::{Class, ClassRef, Instance, InstanceRef, Object, ObjectRef, Value}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VmError { + ForeignHandle, + InvalidHandle, + ExpectedClass, + ExpectedInstance, + EmptyClassName, + ReservedClassName, + IdentityExhausted, +} + +impl fmt::Display for VmError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::ForeignHandle => "handle belongs to another VM", + Self::InvalidHandle => "handle does not identify a live object", + Self::ExpectedClass => "object is not a class", + Self::ExpectedInstance => "object is not an instance", + Self::EmptyClassName => "class name cannot be blank", + Self::ReservedClassName => "Object is reserved for the root class", + Self::IdentityExhausted => "process-local VM identities exhausted", + }) + } +} + +impl std::error::Error for VmError {} + +/// Owns its heap and exactly one root class. Not Clone: handles have one owner. +#[derive(Debug)] +pub struct Vm { + heap: Heap, + root: ClassRef, +} + +impl Vm { + /// Bootstraps Object before returning. Does not perform any host I/O. + pub fn new() -> Result { + let mut heap = Heap::new()?; + let root = ClassRef(heap.allocate(Object::Class(Class { + name: "Object".into(), + parent: None, + }))); + Ok(Self { heap, root }) + } + + /// Idempotent: construction has already performed bootstrap exactly once. + pub fn bootstrap(&mut self) -> ClassRef { + self.root + } + + pub fn root_class(&self) -> ClassRef { + self.root + } + + pub fn object_count(&self) -> usize { + self.heap.len() + } + + /// All user classes have an existing parent. Class names are labels rather + /// than a global registry; duplicate non-reserved names are permitted. + pub fn create_class( + &mut self, + name: impl Into, + parent: ClassRef, + ) -> Result { + self.class(parent)?; + let name = name.into(); + if name.trim().is_empty() { + return Err(VmError::EmptyClassName); + } + if name == "Object" { + return Err(VmError::ReservedClassName); + } + Ok(ClassRef(self.heap.allocate(Object::Class(Class { + name, + parent: Some(parent), + })))) + } + + pub fn create_instance(&mut self, class: ClassRef) -> Result { + self.class(class)?; + Ok(InstanceRef(self.heap.allocate(Object::Instance( + Instance { + class, + fields: BTreeMap::new(), + }, + )))) + } + + pub fn object(&self, reference: ObjectRef) -> Result<&Object, VmError> { + self.heap.get(reference) + } + + pub fn class(&self, reference: ClassRef) -> Result<&Class, VmError> { + match self.heap.get(reference.0)? { + Object::Class(class) => Ok(class), + Object::Instance(_) => Err(VmError::ExpectedClass), + } + } + + pub fn instance(&self, reference: InstanceRef) -> Result<&Instance, VmError> { + match self.heap.get(reference.0)? { + Object::Instance(instance) => Ok(instance), + Object::Class(_) => Err(VmError::ExpectedInstance), + } + } + + /// Converts a generic reference only after validating ownership and kind. + pub fn as_class(&self, reference: ObjectRef) -> Result { + let typed = ClassRef(reference); + self.class(typed)?; + Ok(typed) + } + + pub fn as_instance(&self, reference: ObjectRef) -> Result { + let typed = InstanceRef(reference); + self.instance(typed)?; + Ok(typed) + } + + /// Checks both destination and referenced value before changing any field. + pub fn set_field( + &mut self, + instance: InstanceRef, + name: impl Into, + value: Value, + ) -> Result<(), VmError> { + self.instance(instance)?; + if let Value::Object(reference) = &value { + self.heap.get(*reference)?; + } + match self.heap.get_mut(instance.0)? { + Object::Instance(instance) => { + instance.fields.insert(name.into(), value); + Ok(()) + } + Object::Class(_) => Err(VmError::ExpectedInstance), + } + } +} diff --git a/crates/aster-core/tests/runtime.rs b/crates/aster-core/tests/runtime.rs new file mode 100644 index 0000000..de353d4 --- /dev/null +++ b/crates/aster-core/tests/runtime.rs @@ -0,0 +1,120 @@ +use aster_core::{Value, Vm, VmError}; + +#[test] +fn bootstrap_is_unique_and_root_has_no_parent() { + let mut vm = Vm::new().unwrap(); + let root = vm.root_class(); + assert_eq!(vm.bootstrap(), root); + assert_eq!(vm.bootstrap(), root); + assert_eq!(vm.object_count(), 1); + assert_eq!(vm.class(root).unwrap().name(), "Object"); + assert_eq!(vm.class(root).unwrap().parent(), None); +} + +#[test] +fn factories_preserve_class_links_and_distinct_identity() { + let mut vm = Vm::new().unwrap(); + let root = vm.root_class(); + let class = vm.create_class("Point", root).unwrap(); + let child = vm.create_class("ColoredPoint", class).unwrap(); + let first = vm.create_instance(class).unwrap(); + let second = vm.create_instance(class).unwrap(); + let plain = vm.create_instance(root).unwrap(); + assert_ne!(first, second); + assert_eq!(vm.class(child).unwrap().parent(), Some(class)); + assert_eq!(vm.instance(first).unwrap().class(), class); + assert_eq!(vm.instance(plain).unwrap().class(), root); + vm.set_field(first, "x", Value::Integer(1)).unwrap(); + vm.set_field(first, "next", Value::Object(second.into())) + .unwrap(); + assert_eq!( + vm.instance(first).unwrap().field("x"), + Some(&Value::Integer(1)) + ); + assert_eq!(vm.instance(second).unwrap().field("x"), None); + assert_eq!(vm.as_class(class.into()).unwrap(), class); + assert_eq!(vm.as_instance(first.into()).unwrap(), first); + assert_eq!(vm.as_instance(class.into()), Err(VmError::ExpectedInstance)); + assert_eq!(vm.as_class(first.into()), Err(VmError::ExpectedClass)); +} + +#[test] +fn foreign_handles_are_rejected_without_mutation() { + let mut vm = Vm::new().unwrap(); + let mut other = Vm::new().unwrap(); + let local = vm.create_instance(vm.root_class()).unwrap(); + let foreign = other.create_instance(other.root_class()).unwrap(); + let count = vm.object_count(); + assert_eq!( + vm.create_class("Alien", other.root_class()), + Err(VmError::ForeignHandle) + ); + assert_eq!( + vm.create_instance(other.root_class()), + Err(VmError::ForeignHandle) + ); + assert!(matches!( + vm.object(foreign.into()), + Err(VmError::ForeignHandle) + )); + assert!(matches!(vm.instance(foreign), Err(VmError::ForeignHandle))); + assert_eq!( + vm.set_field(foreign, "x", Value::Nil), + Err(VmError::ForeignHandle) + ); + vm.set_field(local, "x", Value::Integer(7)).unwrap(); + assert_eq!( + vm.set_field(local, "x", Value::Object(foreign.into())), + Err(VmError::ForeignHandle) + ); + assert_eq!( + vm.instance(local).unwrap().field("x"), + Some(&Value::Integer(7)) + ); + assert_eq!(vm.object_count(), count); +} + +#[test] +fn handles_outliving_a_vm_cannot_alias_a_new_vm() { + let old = { + let vm = Vm::new().unwrap(); + vm.root_class() + }; + let vm = Vm::new().unwrap(); + assert_ne!(old, vm.root_class()); + assert!(matches!(vm.class(old), Err(VmError::ForeignHandle))); +} + +#[test] +fn invalid_names_do_not_allocate_and_names_are_not_a_namespace() { + let mut vm = Vm::new().unwrap(); + assert_eq!( + vm.create_class(" ", vm.root_class()), + Err(VmError::EmptyClassName) + ); + assert_eq!( + vm.create_class("Object", vm.root_class()), + Err(VmError::ReservedClassName) + ); + assert_eq!(vm.object_count(), 1); + let a = vm.create_class("Label", vm.root_class()).unwrap(); + let b = vm.create_class("Label", vm.root_class()).unwrap(); + assert_ne!(a, b); +} + +#[test] +fn handles_remain_valid_when_the_arena_grows() { + let mut vm = Vm::new().unwrap(); + let root = vm.root_class(); + let first = vm.create_instance(root).unwrap(); + vm.set_field(first, "stable", Value::Bool(true)).unwrap(); + for _ in 0..4096 { + vm.create_instance(root).unwrap(); + } + assert_eq!(vm.instance(first).unwrap().class(), root); + assert_eq!( + vm.instance(first).unwrap().field("stable"), + Some(&Value::Bool(true)) + ); + assert_eq!(vm.class(root).unwrap().name(), "Object"); +} diff --git a/crates/aster-server/Cargo.toml b/crates/aster-server/Cargo.toml new file mode 100644 index 0000000..9a6b5c9 --- /dev/null +++ b/crates/aster-server/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "aster-server" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +aster-core.workspace = true +aster-storage.workspace = true +axum.workspace = true +serde.workspace = true +tokio.workspace = true + +[lints] +workspace = true diff --git a/crates/aster-server/src/main.rs b/crates/aster-server/src/main.rs new file mode 100644 index 0000000..c0643fd --- /dev/null +++ b/crates/aster-server/src/main.rs @@ -0,0 +1,126 @@ +use std::error::Error; +use std::future::Future; +use std::process::ExitCode; +use std::sync::Arc; + +use aster_core::Vm; +use aster_storage::{Storage, StorageConfig}; +use axum::{Json, Router, extract::State, http::StatusCode, routing::get}; +use serde::Serialize; + +#[derive(Clone)] +struct AppState { + vm: Arc, + storage: Storage, +} + +#[derive(Serialize)] +struct Health { + status: &'static str, + storage: &'static str, +} + +#[derive(Serialize)] +struct BootstrapInfo { + root_class: String, + root_has_parent: bool, + heap_objects: usize, +} + +#[tokio::main] +async fn main() -> ExitCode { + match run().await { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("aster-server: {error}"); + ExitCode::FAILURE + } + } +} + +async fn run() -> Result<(), Box> { + let vm = Arc::new(Vm::new()?); + let storage = Storage::connect(&StorageConfig::from_env()?).await?; + storage + .put("host.version", env!("CARGO_PKG_VERSION")) + .await?; + let address = std::env::var("ASTER_LISTEN").unwrap_or_else(|_| "127.0.0.1:8080".into()); + let listener = tokio::net::TcpListener::bind(&address).await?; + let shutdown = shutdown_signal()?; + let app = Router::new() + .route("/health", get(health)) + .route("/bootstrap", get(bootstrap)) + .with_state(AppState { + vm, + storage: storage.clone(), + }); + println!( + "Aster host listening on {} (storage={})", + listener.local_addr()?, + storage.backend_name() + ); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown) + .await?; + storage.close().await; + Ok(()) +} + +async fn health(State(state): State) -> (StatusCode, Json) { + let (code, status) = match state.storage.ping().await { + Ok(()) => (StatusCode::OK, "ok"), + Err(_) => (StatusCode::SERVICE_UNAVAILABLE, "storage_unavailable"), + }; + ( + code, + Json(Health { + status, + storage: state.storage.backend_name(), + }), + ) +} + +async fn bootstrap(State(state): State) -> Json { + let root = state + .vm + .class(state.vm.root_class()) + .expect("VM construction guarantees a root class"); + Json(BootstrapInfo { + root_class: root.name().into(), + root_has_parent: root.parent().is_some(), + heap_objects: state.vm.object_count(), + }) +} + +fn shutdown_signal() -> std::io::Result> { + #[cfg(unix)] + let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?; + Ok(async move { + #[cfg(unix)] + tokio::select! { _ = tokio::signal::ctrl_c() => {}, _ = terminate.recv() => {} } + #[cfg(not(unix))] + let _ = tokio::signal::ctrl_c().await; + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn health_reports_unavailable_after_storage_closes() { + let storage = Storage::connect(&StorageConfig::SqliteMemory) + .await + .unwrap(); + let state = AppState { + vm: Arc::new(Vm::new().unwrap()), + storage, + }; + let (status, _) = health(State(state.clone())).await; + assert_eq!(status, StatusCode::OK); + state.storage.close().await; + let (status, Json(body)) = health(State(state)).await; + assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(body.status, "storage_unavailable"); + } +} diff --git a/crates/aster-storage/Cargo.toml b/crates/aster-storage/Cargo.toml new file mode 100644 index 0000000..69681bc --- /dev/null +++ b/crates/aster-storage/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "aster-storage" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +sqlx.workspace = true +tokio.workspace = true + +[dev-dependencies] +tempfile = "3" + +[lints] +workspace = true diff --git a/crates/aster-storage/src/lib.rs b/crates/aster-storage/src/lib.rs new file mode 100644 index 0000000..a93630c --- /dev/null +++ b/crates/aster-storage/src/lib.rs @@ -0,0 +1,249 @@ +//! Host metadata storage; deliberately has no dependency on aster-core. +use std::fmt; +use std::path::PathBuf; +use std::str::FromStr; +use std::time::Duration; + +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}; +use sqlx::{PgPool, SqlitePool}; + +const SCHEMA: &str = include_str!("schema.sql"); + +/// An explicit PostgreSQL configuration never falls back to SQLite. +#[derive(Clone)] +pub enum StorageConfig { + SqliteFile(PathBuf), + SqliteMemory, + Postgres(String), +} + +impl fmt::Debug for StorageConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::SqliteFile(path) => f.debug_tuple("SqliteFile").field(path).finish(), + Self::SqliteMemory => f.write_str("SqliteMemory"), + Self::Postgres(_) => f.write_str("Postgres([REDACTED])"), + } + } +} + +impl StorageConfig { + /// DATABASE_URL accepts PostgreSQL only; SQLITE_PATH selects file or :memory:. + /// Empty/invalid explicit values are errors rather than default selection. + pub fn from_values( + database_url: Option<&str>, + sqlite_path: Option<&str>, + ) -> Result { + if let Some(url) = database_url { + if !(url.starts_with("postgres://") || url.starts_with("postgresql://")) { + return Err(StorageError::Config( + "DATABASE_URL must be a PostgreSQL URL", + )); + } + return Ok(Self::Postgres(url.into())); + } + match sqlite_path.unwrap_or("data/aster.db") { + "" => Err(StorageError::Config("SQLITE_PATH cannot be empty")), + ":memory:" => Ok(Self::SqliteMemory), + path => Ok(Self::SqliteFile(path.into())), + } + } + + pub fn from_env() -> Result { + let database_url = read_env("DATABASE_URL")?; + // Irrelevant SQLite settings must not prevent PostgreSQL configuration. + if database_url.is_some() { + return Self::from_values(database_url.as_deref(), None); + } + let sqlite_path = read_env("SQLITE_PATH")?; + Self::from_values(None, sqlite_path.as_deref()) + } +} + +fn read_env(name: &str) -> Result, StorageError> { + match std::env::var(name) { + Ok(value) => Ok(Some(value)), + Err(std::env::VarError::NotPresent) => Ok(None), + Err(std::env::VarError::NotUnicode(_)) => Err(StorageError::Config( + "storage environment must be valid Unicode", + )), + } +} + +#[derive(Debug)] +pub enum StorageError { + Config(&'static str), + Directory(std::io::Error), + Database(sqlx::Error), +} + +impl fmt::Display for StorageError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Config(message) => f.write_str(message), + Self::Directory(_) => f.write_str("cannot create SQLite parent directory"), + // CLI/HTTP logs must never print a connection URL or credentials. + Self::Database(_) => { + f.write_str("database operation failed; no fallback was attempted") + } + } + } +} + +impl std::error::Error for StorageError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Config(_) => None, + Self::Directory(error) => Some(error), + Self::Database(error) => Some(error), + } + } +} + +impl From for StorageError { + fn from(error: sqlx::Error) -> Self { + Self::Database(error) + } +} + +#[derive(Clone)] +enum Backend { + Sqlite(SqlitePool), + Postgres(PgPool), +} + +/// Clones share a pool. A memory SQLite store uses exactly one connection, so +/// initialization and every operation see the same database for its lifetime. +#[derive(Clone)] +pub struct Storage { + backend: Backend, +} + +impl Storage { + /// Connects eagerly and initializes the v1 metadata schema before returning. + pub async fn connect(config: &StorageConfig) -> Result { + let backend = match config { + StorageConfig::Postgres(url) => { + let options = PgConnectOptions::from_str(url) + .map_err(|_| StorageError::Config("invalid PostgreSQL connection URL"))?; + Backend::Postgres( + PgPoolOptions::new() + .max_connections(5) + .acquire_timeout(Duration::from_secs(5)) + .connect_with(options) + .await?, + ) + } + StorageConfig::SqliteFile(path) => { + if path.as_os_str().is_empty() || path == std::path::Path::new(":memory:") { + return Err(StorageError::Config( + "use SqliteMemory for an in-memory database", + )); + } + if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) { + tokio::fs::create_dir_all(parent) + .await + .map_err(StorageError::Directory)?; + } + let options = SqliteConnectOptions::new() + .filename(path) + .create_if_missing(true) + .journal_mode(SqliteJournalMode::Wal) + .busy_timeout(Duration::from_secs(5)); + Backend::Sqlite( + SqlitePoolOptions::new() + .max_connections(5) + .acquire_timeout(Duration::from_secs(5)) + .connect_with(options) + .await?, + ) + } + StorageConfig::SqliteMemory => { + let options = SqliteConnectOptions::new().in_memory(true); + Backend::Sqlite( + SqlitePoolOptions::new() + .max_connections(1) + .min_connections(1) + .idle_timeout(None) + .max_lifetime(None) + .connect_with(options) + .await?, + ) + } + }; + let storage = Self { backend }; + storage.initialize().await?; + Ok(storage) + } + + async fn initialize(&self) -> Result<(), StorageError> { + match &self.backend { + Backend::Sqlite(pool) => { + sqlx::query(SCHEMA).execute(pool).await?; + } + Backend::Postgres(pool) => { + sqlx::query(SCHEMA).execute(pool).await?; + } + } + Ok(()) + } + + pub fn backend_name(&self) -> &'static str { + match &self.backend { + Backend::Sqlite(_) => "sqlite", + Backend::Postgres(_) => "postgresql", + } + } + + pub async fn ping(&self) -> Result<(), StorageError> { + match &self.backend { + Backend::Sqlite(pool) => { + sqlx::query("SELECT 1").execute(pool).await?; + } + Backend::Postgres(pool) => { + sqlx::query("SELECT 1").execute(pool).await?; + } + } + Ok(()) + } + + pub async fn put(&self, key: &str, value: &str) -> Result<(), StorageError> { + match &self.backend { + Backend::Sqlite(pool) => { + sqlx::query("INSERT INTO aster_metadata (key, value) VALUES (?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value") + .bind(key).bind(value).execute(pool).await?; + } + Backend::Postgres(pool) => { + sqlx::query("INSERT INTO aster_metadata (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = excluded.value") + .bind(key).bind(value).execute(pool).await?; + } + } + Ok(()) + } + + pub async fn get(&self, key: &str) -> Result, StorageError> { + Ok(match &self.backend { + Backend::Sqlite(pool) => { + sqlx::query_scalar("SELECT value FROM aster_metadata WHERE key = ?1") + .bind(key) + .fetch_optional(pool) + .await? + } + Backend::Postgres(pool) => { + sqlx::query_scalar("SELECT value FROM aster_metadata WHERE key = $1") + .bind(key) + .fetch_optional(pool) + .await? + } + }) + } + + /// Explicitly closes the shared pool, including all Storage clones. + pub async fn close(&self) { + match &self.backend { + Backend::Sqlite(pool) => pool.close().await, + Backend::Postgres(pool) => pool.close().await, + } + } +} diff --git a/crates/aster-storage/src/schema.sql b/crates/aster-storage/src/schema.sql new file mode 100644 index 0000000..cd6b39b --- /dev/null +++ b/crates/aster-storage/src/schema.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS aster_metadata ( + key TEXT PRIMARY KEY NOT NULL, + value TEXT NOT NULL +) diff --git a/crates/aster-storage/tests/postgres.rs b/crates/aster-storage/tests/postgres.rs new file mode 100644 index 0000000..ab0a1df --- /dev/null +++ b/crates/aster-storage/tests/postgres.rs @@ -0,0 +1,35 @@ +use aster_storage::{Storage, StorageConfig}; + +/// Run explicitly against a disposable database; never silently skip a missing URL. +#[tokio::test] +#[ignore = "requires ASTER_TEST_POSTGRES_URL pointing to a test PostgreSQL database"] +async fn postgres_roundtrip_and_reconnect() { + let url = std::env::var("ASTER_TEST_POSTGRES_URL") + .expect("set ASTER_TEST_POSTGRES_URL to a disposable PostgreSQL database"); + let config = StorageConfig::from_values(Some(&url), None).unwrap(); + let store = Storage::connect(&config).await.unwrap(); + assert_eq!(store.backend_name(), "postgresql"); + store.ping().await.unwrap(); + let key = format!( + "test.{}.{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + assert_eq!(store.get(&key).await.unwrap(), None); + store.put(&key, "你好 ' ; $1").await.unwrap(); + assert_eq!( + store.get(&key).await.unwrap().as_deref(), + Some("你好 ' ; $1") + ); + store.put(&key, "updated").await.unwrap(); + store.close().await; + let reopened = Storage::connect(&config).await.unwrap(); + assert_eq!( + reopened.get(&key).await.unwrap().as_deref(), + Some("updated") + ); + reopened.close().await; +} diff --git a/crates/aster-storage/tests/storage.rs b/crates/aster-storage/tests/storage.rs new file mode 100644 index 0000000..f79d223 --- /dev/null +++ b/crates/aster-storage/tests/storage.rs @@ -0,0 +1,97 @@ +use aster_storage::{Storage, StorageConfig}; + +#[test] +fn configuration_defaults_precedence_and_errors() { + assert!( + matches!(StorageConfig::from_values(None, None).unwrap(), StorageConfig::SqliteFile(p) if p.to_str() == Some("data/aster.db")) + ); + assert!(matches!( + StorageConfig::from_values(None, Some(":memory:")).unwrap(), + StorageConfig::SqliteMemory + )); + assert!(matches!( + StorageConfig::from_values(Some("postgresql://localhost/aster"), Some("")).unwrap(), + StorageConfig::Postgres(_) + )); + assert!(StorageConfig::from_values(Some(""), None).is_err()); + assert!(StorageConfig::from_values(Some("mysql://localhost/aster"), None).is_err()); + assert!(StorageConfig::from_values(None, Some("")).is_err()); + let config = StorageConfig::Postgres("postgres://user:secret@localhost/db".into()); + assert!(!format!("{config:?}").contains("secret")); +} + +#[tokio::test] +async fn memory_store_is_shared_by_clones_but_isolated_between_stores() { + let store = Storage::connect(&StorageConfig::SqliteMemory) + .await + .unwrap(); + let other = Storage::connect(&StorageConfig::SqliteMemory) + .await + .unwrap(); + store.ping().await.unwrap(); + assert_eq!(store.get("missing").await.unwrap(), None); + store.put("message", "你好 ' ? $1 ;").await.unwrap(); + assert_eq!( + store.clone().get("message").await.unwrap().as_deref(), + Some("你好 ' ? $1 ;") + ); + assert_eq!(other.get("message").await.unwrap(), None); + store.put("message", "updated").await.unwrap(); + assert_eq!( + store.get("message").await.unwrap().as_deref(), + Some("updated") + ); + store.close().await; + other.close().await; +} + +#[tokio::test] +async fn sqlite_file_creates_parents_and_survives_reopen() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("nested/data/metadata.db"); + let config = StorageConfig::SqliteFile(path.clone()); + let store = Storage::connect(&config).await.unwrap(); + store.put("project", "Aster VM").await.unwrap(); + store.close().await; + assert!(path.is_file()); + let reopened = Storage::connect(&config).await.unwrap(); + assert_eq!( + reopened.get("project").await.unwrap().as_deref(), + Some("Aster VM") + ); + reopened.put("project", "v2").await.unwrap(); + assert_eq!( + reopened.get("project").await.unwrap().as_deref(), + Some("v2") + ); + reopened.close().await; +} + +#[tokio::test] +async fn invalid_sqlite_directory_reports_error() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("file"); + std::fs::write(&file, "occupied").unwrap(); + assert!( + Storage::connect(&StorageConfig::SqliteFile(file.join("aster.db"))) + .await + .is_err() + ); +} + +#[tokio::test] +async fn postgres_connection_failure_is_an_error() { + // Hold a TCP port without serving PostgreSQL: deterministic local failure. + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = format!( + "postgres://aster:secret@{}/aster?sslmode=disable", + listener.local_addr().unwrap() + ); + let result = Storage::connect(&StorageConfig::Postgres(url)).await; + let error = match result { + Ok(_) => panic!("unreachable PostgreSQL must fail"), + Err(error) => error, + }; + assert!(!error.to_string().contains("secret")); + assert!(error.to_string().contains("no fallback")); +} diff --git a/docker-compose.ces-control.yml b/docker-compose.ces-control.yml new file mode 100644 index 0000000..9182f49 --- /dev/null +++ b/docker-compose.ces-control.yml @@ -0,0 +1,42 @@ +name: endorphin-aster-vm + +services: + aster: + image: ${ASTER_IMAGE:?set ASTER_IMAGE to the verified release image} + init: true + ports: + - "127.0.0.1:${ASTER_PORT:-18081}:8080" + environment: + ASTER_LISTEN: 0.0.0.0:8080 + DATABASE_URL: "postgres://aster:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/aster?sslmode=disable" + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + stop_grace_period: 20s + logging: + driver: json-file + options: + max-size: 10m + max-file: "3" + + postgres: + image: postgres:17-bookworm + environment: + POSTGRES_DB: aster + POSTGRES_USER: aster + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} + volumes: + - /var/lib/endorphin-aster-vm/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U aster -d aster"] + interval: 5s + timeout: 3s + retries: 20 + restart: unless-stopped + stop_grace_period: 30s + logging: + driver: json-file + options: + max-size: 10m + max-file: "3" diff --git a/docker-compose.postgres.yml b/docker-compose.postgres.yml new file mode 100644 index 0000000..e806fa2 --- /dev/null +++ b/docker-compose.postgres.yml @@ -0,0 +1,31 @@ +# Standalone alternative: docker compose -f docker-compose.postgres.yml up --build -d +services: + aster: + build: . + ports: + - "127.0.0.1:${ASTER_PORT:-8080}:8080" + environment: + ASTER_LISTEN: 0.0.0.0:8080 + DATABASE_URL: "postgres://aster:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/aster?sslmode=disable" + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + + postgres: + image: postgres:17-bookworm + environment: + POSTGRES_DB: aster + POSTGRES_USER: aster + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + volumes: + - aster-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U aster -d aster"] + interval: 5s + timeout: 3s + retries: 10 + restart: unless-stopped + +volumes: + aster-postgres: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2802930 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + aster: + build: . + ports: + - "127.0.0.1:${ASTER_PORT:-8080}:8080" + environment: + ASTER_LISTEN: 0.0.0.0:8080 + SQLITE_PATH: /data/aster.db + volumes: + - aster-sqlite:/data + restart: unless-stopped + +volumes: + aster-sqlite: diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..8bf29a9 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,33 @@ +# 架构与语义 + +## 内核 + +`Vm` 拥有私有 `Heap`。`Vm::new` 分配一个 `Object::Class(Class)`,其描述为名称 Object、父类 None,然后保存 `ClassRef`。不存在可观察的“半初始化 VM”;`bootstrap` 只返回已建立的根类句柄。因此根类不依赖数据库、全局命名空间、元类、网络或异步运行时。 + +`create_class(name, parent)` 校验已有父类后创建不可变描述,父类不可事后改写,不能形成继承环;只有内核启动路径可以创建没有父类的类。`create_instance` 校验类句柄再创建独立字段表。没有复制原型或隐式字段继承。未实现方法与实例字段查找的语言规则。 + +句柄结构是私有 VM 身份加 `usize` 槽位。VM 身份用原子计数器分配,耗尽时返回错误,不回绕复用。`Vec` 扩容会移动 Rust 对象,但外部不保存它们的地址,旧句柄继续通过索引访问。当前绝不删除槽位,所以无代际号。VM 不实现 Clone,复制句柄不能复制 VM 身份。 + +检查顺序是归属、范围、种类。外部没有从整数构造句柄的 API;低层无效槽位与伪造类型用私有单元测试覆盖。通用句柄可通过 `as_class/as_instance` 做校验后的转换。不同 VM 的引用、已经销毁 VM 的引用,在新 VM 中都报 `ForeignHandle`。同一 VM 中不在有效槽位的引用报 `InvalidHandle`。写字段前检查目标和引用值,失败不会覆盖原字段。 + +`Value` 中标量和字符串不强制堆分配,引用目前仅指类或实例。当前字符串值是按 Rust 所有权复制的文本,并非具有对象身份的字符串实例。`Float` 使用 Rust f64/PartialEq 行为,尚未作为最终语言语义承诺。类不是自身或其他类的实例,根类也没有隐式元类。 + +没有 GC,循环引用不形成 Rust 引用计数环,因为边只是句柄;所有内存随 VM 释放。长期运行时分配只增不减,尚无资源限额或内存不足的可恢复错误处理。宿主必须控制使用范围。 + +## 宿主与持久化 + +`aster-storage` 不引用任何内核类型。它连接一个确定的后端,初始化 `aster_metadata(key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)`,通过参数绑定完成幂等更新及读取。当前 schema 是单条幂等 DDL;修改 schema 时再引入版本化迁移,禁止把 CREATE IF NOT EXISTS 当作升级已有列结构的工具。 + +SQLite 文件模式自动创建父目录、开启 WAL,连接池最多 5 条连接,busy timeout 为 5 秒。内存模式单连接,关闭空闲回收和最大存活时间,防止池正常轮换导致数据库消失;独立 Storage 实例的内存数据库相互隔离。`Storage` 克隆共享连接池,`close` 关闭全部克隆的池。 + +PostgreSQL 最多 5 条连接,获取/建立连接超时 5 秒,启动真实连接而非惰性连接。其配置失败或初始化失败均上抛,不进入 SQLite 分支。SQLite 的缺省只发生在**未配置** DATABASE_URL 时。 + +元数据仅供宿主管理,不代表类、实例、句柄或整堆快照。重启宿主会创建新的 VM、新的 Object 和新句柄,只保留文件/数据库内的元数据。服务仅写入 `host.version`,不会把句柄调试值写入数据库。 + +`aster-server` 保存只读 VM 和共享 Storage。GET /bootstrap 只读根类;GET /health 探测数据库可用性。没有对象操作 API、语言执行 API 或鉴权协议。HTTP/Tokio/SQLx 都不进入内核。后续浏览器宿主应通过明确的能力接口桥接,不能把浏览器类型塞进核心对象工厂。 + +## 已决定与未决定 + +已决定:唯一无父类 Object、类和实例分离、实例引用类、句柄由 VM 校验、存储属于宿主。 + +未决定:字节码格式、调用栈、方法解析、语言异常、外部对象协议、线程间共享 VM、GC/根集、堆序列化、浏览器通信、完整字符串对象/数值对象语义。不得在后续交接中把这些当成已实现契约。 diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..9220234 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,33 @@ +# 开发交接 + +## 入口与验证 + +先阅读根目录 AGENTS.md。工作区四个 crate 的边界必须保持,内核无数据库/HTTP 依赖。用户明确禁止本地测试,不在本机运行检查/冒烟脚本,也不为此下载依赖。指定的远端验证环境是 ces-control;CI 在 Docker 中运行 `sh scripts/check.sh`、`python3 scripts/smoke.py` 和真实 PostgreSQL 集成测试。正式持久部署选择 PostgreSQL。检查记录见 validation.md。 + +核心 API 示例在 `aster-core/src/lib.rs`,由 rustdoc 实际编译执行。公开语义测试在 `aster-core/tests/runtime.rs`;伪造句柄仅在私有堆单元测试中构造,禁止为测试添加公开非法构造器。 + +存储测试用 `tempfile` 隔离文件,关闭池后重开,验证独立连接生命周期中的持久化。内存测试验证克隆共享、独立 store 隔离、特殊字符参数绑定、更新/缺失键。PostgreSQL 失败测试用只监听但不处理协议的本地 TCP socket,检验真实连接超时和不降级;成功测试显式 ignore,需要用户提供专用测试库。 + +尚未运行的冒烟脚本用于跨进程调用 CLI、启动 HTTP 宿主、探测真实 TCP 端点、发送 SIGTERM 并确认正常退出。它只创建临时目录,不改 `.env`,不操作外部服务器。端口通过临时绑定选择,释放后启动间存在很小的端口竞争窗口;在指定验证环境中若冲突,可重试整个脚本。 + +依赖使用 Cargo.lock。此前编译器为 1.91.1;更换依赖后须在指定环境检查 MSRV 和容器构建版本。核心测试无需数据库,但 Cargo 首次依赖解析仍可能访问注册表;不要因此在本机恢复下载或测试。 + +## 下一阶段建议 + +1. 先约定对象字段与方法查找规则、宿主调用的参数/返回值和错误边界,补语义测试。 +2. 在 core 内构建最小、可测的执行循环,输入先用手写指令,继续不做解析器和渲染器。 +3. 根据执行栈、对象图、宿主持有引用确定 GC 根集;如需回收槽位,升级代际/间接句柄模型并补陈旧句柄测试。 +4. 对外部对象、浏览器能力、配额与取消建立最小接口,以实际需求驱动,不预先堆砌空 trait。 +5. 需要升级元数据 schema 时引入可追踪的迁移;若要求 VM 快照,另行定义持久身份与格式,不能直接序列化现有句柄。 + +## Git 状态与后续远程 + +origin 为用户提供的 `ssh://git@gitea.entroia.com:22222/Zayden-Jung/endorphin-aster-vm.git`。仓库级 author 和隔离 SSH 命令已配置;不得复制其他身份或猜测 owner。发布 CI/CD 需要提交、推送并跟踪远端结果;遵守 AGENTS.md 的授权及中文逐文件报告要求。初始开发阶段记录与后续部署记录在 validation.md 中分开说明。 + +## 远端 CI 与部署 + +`scripts/ci-ces-control.sh` 构建 Dockerfile 的 ci 阶段,创建专属网络与临时 PostgreSQL,再依次运行 workspace 检查、显式 PostgreSQL 测试、SQLite/宿主端到端冒烟。Docker daemon 为 legacy builder,工作流显式设置 `DOCKER_BUILDKIT=0`;没有依赖 buildx。远端内存约 1.7 GiB,Rust 单任务编译、调试构建不生成 debug 信息。 + +`scripts/deploy-ces-control.sh` 仅部署已经构建的 SHA 镜像;使用文件锁防止并行切换生产版本,第一次自动生成服务器密码文件,后续保留。Compose 固定项目名,独占 PostgreSQL 数据目录,与其他项目隔离。部署通过健康检查和真实元数据写入/读取后才切换 current;失败时尝试恢复上一版健康 Compose,不删除数据库或镜像。 + +更换应用容器不应重建密码或清理数据目录。PostgreSQL major 版本升级与 schema 变更须另外计划迁移;禁止把应用镜像回退当作数据库回退。修改此链路后,验证必须继续放在 ces-control,不要恢复本机依赖下载。 diff --git a/docs/validation.md b/docs/validation.md new file mode 100644 index 0000000..4add134 --- /dev/null +++ b/docs/validation.md @@ -0,0 +1,50 @@ +# 实现与验证记录 + +日期:2026-10-17(Asia/Shanghai)。 + +## 初始开发阶段记录 + +用户已明确禁止在本地测试,并要求清理下载内容。停止继续下载、构建与测试;后续检查脚本仅供用户指定的 CI/部署环境使用。测试结果是此前已启动进程返回的历史记录,不代表此后又运行了测试。 + +尝试中断此前的 `cargo test --workspace` 时,该进程已经自然结束,返回状态 0,SQLite 构建依赖也已下载。随后按用户要求清理了本次新增缓存和构建产物,不再运行测试。 + +## 已观察结果 + +| 检查 | 实际结果 | +| --- | --- | +| `cargo fmt --all --check` | 通过;此后只修改文档与约定,没有修改 Rust 代码 | +| `cargo test -p aster-core --offline` | 3 项私有单元测试、6 项公开语义测试、1 项文档测试通过 | +| `cargo clippy -p aster-core --all-targets --offline -- -D warnings` | 通过,仅覆盖内核 | +| 此前启动的 `cargo test --workspace` | 最终返回 16 项通过、0 失败、1 项明确忽略;含核心 9 项、宿主 1 项、存储 5 项、文档 1 项 | +| PostgreSQL 失败路径 | 使用不处理协议的本地 TCP socket,连接超时并报错、不降级;已包含于上述存储测试 | +| 真实 PostgreSQL 成功路径 | 未验证;集成测试显式忽略,需专用测试库 | +| 两个 Compose 文件 | Ruby YAML 静态解析通过,未执行 Compose | +| `scripts/check.sh`/`scripts/smoke.py` | 分别通过 Shell/Python 语法检查,未运行完整脚本 | +| `cargo clippy --workspace --all-targets -- -D warnings` | 未执行,不宣称全工作区无 Clippy 警告 | +| CLI/HTTP/SIGTERM 端到端冒烟 | 未执行;宿主健康状态函数测试已在上述 workspace 测试通过 | +| Docker 构建及容器运行 | 未执行,本机无 Docker | + +以上为初始阶段记录,当时没有外部部署、PostgreSQL 服务、commit 或 push。 + +## 清理结果 + +- 删除本项目创建后新增、且属于 Cargo.lock 依赖列表的 150 个 Cargo 下载归档与 150 个解压目录,包括 SQLite/SQLx 本次新增依赖。 +- 删除项目 `target/` 构建产物及 Python 字节码缓存。 +- 保留此前已存在的 Cargo 缓存、共享注册表索引、项目源码与 Cargo.lock;不清空其他项目的既有依赖。 +- 没有运行 CLI 创建默认 `data/aster.db`。此前文件数据库测试使用的临时目录随测试自动释放。 + +## Git + +初始阶段本地分支 main 尚无提交。origin 是用户提供的 `ssh://git@gitea.entroia.com:22222/Zayden-Jung/endorphin-aster-vm.git`。 + +只读 `ls-remote --symref origin HEAD` 使用专用 codex-bot 密钥、隔离 SSH 配置、禁用其他身份及交互式凭据,返回状态 0,未公布 HEAD 引用。只读成功不代表已验证写权限。 + +仓库级身份:`Codex Bot` / `codex-bot@gitea.entroia.local`;`core.sshCommand` 使用项目约定的隔离命令。没有读取密码或切换为仓库 owner 身份。 + +## ces-control CI/CD 准备 + +用户随后指定在 ces-control 上持久部署,并要求同时测试 SQLite 与 PostgreSQL、正式部署使用 PostgreSQL。新增的工作流和脚本将这些检查放在远端 Docker 中,不在本机下载依赖或运行测试。 + +准备阶段只读检查:Docker 29.1.3、Compose 2.40.3、x86_64、约 1.7 GiB 内存/4 GiB swap、73 GiB 可用磁盘;没有主机 Rust 或 buildx,已缓存 PostgreSQL 17 镜像。选用单任务 Rust 构建、legacy Docker builder,默认生产端口 18081(服务器回环地址),PostgreSQL 数据目录独立。 + +此处不预先宣称远端 CI 或部署已成功;最终结果以 Gitea Actions 对应运行与服务器状态核验为准。 diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100644 index 0000000..80a1a65 --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu +cargo fmt --all --check +cargo test --workspace --locked +cargo clippy --workspace --all-targets --locked -- -D warnings diff --git a/scripts/ci-ces-control.sh b/scripts/ci-ces-control.sh new file mode 100644 index 0000000..b5e08ae --- /dev/null +++ b/scripts/ci-ces-control.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Run only on the designated remote CI host, never on the developer's machine. +set -eu +cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" + +release_sha=${ASTER_RELEASE_SHA:?ASTER_RELEASE_SHA is required} +case "$release_sha" in *[!0-9a-f]*|'') echo "Invalid release SHA" >&2; exit 1;; esac +test "${#release_sha}" -eq 40 + +ci_id="${GITHUB_RUN_ID:-manual}-$$" +network="endorphin-aster-vm-ci-$ci_id" +postgres="endorphin-aster-vm-ci-pg-$ci_id" +checks="endorphin-aster-vm-ci-checks-$ci_id" +image="endorphin-aster-vm-ci:$release_sha" +cleanup() { + docker rm -f "$checks" "$postgres" >/dev/null 2>&1 || true + docker network rm "$network" >/dev/null 2>&1 || true +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +docker build --target ci -t "$image" . +docker network create "$network" >/dev/null +# Disposable test database: no production mounts and no published ports. +docker run -d --name "$postgres" --network "$network" --network-alias postgres \ + --env POSTGRES_USER=aster --env POSTGRES_DB=aster \ + --env POSTGRES_PASSWORD=aster-ci-only \ + --tmpfs /var/lib/postgresql/data postgres:17-bookworm >/dev/null + +ready=false +for attempt in $(seq 1 60); do + if docker exec "$postgres" pg_isready -h 127.0.0.1 -U aster -d aster >/dev/null 2>&1; then + ready=true + break + fi + sleep 1 +done +if [ "$ready" != true ]; then + echo "CI PostgreSQL did not become ready" >&2 + exit 1 +fi + +docker run --name "$checks" --init --network "$network" \ + --mount type=volume,src=endorphin-aster-vm-ci-cargo,dst=/usr/local/cargo/registry \ + --mount type=volume,src=endorphin-aster-vm-ci-target,dst=/build/target \ + --env ASTER_TEST_POSTGRES_URL='postgres://aster:aster-ci-only@postgres:5432/aster?sslmode=disable' \ + "$image" sh -eu -c ' + sh scripts/check.sh + cargo test -p aster-storage --test postgres --locked -- --ignored + python3 scripts/smoke.py + ' diff --git a/scripts/deploy-ces-control.sh b/scripts/deploy-ces-control.sh new file mode 100644 index 0000000..ffd4f76 --- /dev/null +++ b/scripts/deploy-ces-control.sh @@ -0,0 +1,72 @@ +#!/bin/sh +# Deploy only the image produced after the remote database checks succeed. +set -eu +cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +test "$(id -u)" -eq 0 || { echo "Deployment requires root on ces-control" >&2; exit 1; } +release_sha=${ASTER_RELEASE_SHA:?ASTER_RELEASE_SHA is required} +case "$release_sha" in *[!0-9a-f]*|'') echo "Invalid release SHA" >&2; exit 1;; esac +test "${#release_sha}" -eq 40 +image="endorphin-aster-vm:$release_sha" +docker image inspect "$image" >/dev/null + +umask 077 +root=/opt/endorphin-aster-vm +config=/etc/endorphin-aster-vm/production.env +release="$root/releases/$release_sha" +mkdir -p "$release" /etc/endorphin-aster-vm /var/lib/endorphin-aster-vm/postgres +exec 9>/var/lock/endorphin-aster-vm-deploy.lock +flock 9 + +if [ ! -f "$config" ]; then + password=$(openssl rand -hex 24) + printf 'POSTGRES_PASSWORD=%s\nASTER_PORT=18081\n' "$password" > "$config" + unset password +fi +chmod 600 "$config" +cp docker-compose.ces-control.yml "$release/compose.yml" +printf 'ASTER_IMAGE=%s\n' "$image" > "$release/release.env" +previous=$(readlink -f "$root/current" 2>/dev/null || true) + +compose_at() { + location=$1 + shift + docker compose -p endorphin-aster-vm --env-file "$config" \ + --env-file "$location/release.env" -f "$location/compose.yml" "$@" +} +rollback() { + echo "Deployment failed; application data has been retained" >&2 + if [ -n "$previous" ] && [ "$previous" != "$release" ] && [ -f "$previous/compose.yml" ]; then + echo "Restoring the previously healthy release" >&2 + compose_at "$previous" up -d --no-build --wait --wait-timeout 180 || true + fi +} + +if ! compose_at "$release" up -d --no-build --wait --wait-timeout 180; then + rollback + exit 1 +fi +if ! compose_at "$release" exec -T aster aster metadata put deployment.release "$release_sha"; then + rollback + exit 1 +fi +if ! actual=$(compose_at "$release" exec -T aster aster metadata get deployment.release); then + rollback + exit 1 +fi +if [ "$actual" != "$release_sha" ]; then + rollback + exit 1 +fi +if ! health=$(compose_at "$release" exec -T aster curl --fail --silent http://127.0.0.1:8080/health); then + rollback + exit 1 +fi +printf '%s' "$health" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d == {"status":"ok", "storage":"postgresql"}' || { + rollback + exit 1 +} +ln -sfn "$release" "$root/current.next" +mv -Tf "$root/current.next" "$root/current" +compose_at "$release" ps +echo "Deployed release=$release_sha backend=postgresql endpoint=127.0.0.1:18081" +echo "Persistent PostgreSQL data: /var/lib/endorphin-aster-vm/postgres" diff --git a/scripts/smoke.py b/scripts/smoke.py new file mode 100644 index 0000000..e180900 --- /dev/null +++ b/scripts/smoke.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Local end-to-end smoke test, no Docker or external DB required.""" +import json +import os +from pathlib import Path +import signal +import socket +import subprocess +import tempfile +import time +import urllib.request + +ROOT = Path(__file__).resolve().parents[1] + + +def run(*args, env): + return subprocess.run(args, cwd=ROOT, env=env, check=True, text=True, capture_output=True).stdout.strip() + + +def main(): + env = os.environ.copy() + env.pop("DATABASE_URL", None) + run("cargo", "build", "--workspace", "--locked", env=env) + # Cargo may use a configured CARGO_TARGET_DIR; discover the actual binaries. + metadata = json.loads(run("cargo", "metadata", "--no-deps", "--format-version", "1", env=env)) + binary_dir = Path(metadata["target_directory"]) / "debug" + cli = str(binary_dir / "aster") + server = str(binary_dir / "aster-server") + with tempfile.TemporaryDirectory(prefix="aster-smoke-") as directory: + env["SQLITE_PATH"] = str(Path(directory) / "nested" / "aster.db") + # This must succeed even when an explicit database URL is invalid. + isolated_env = {**env, "DATABASE_URL": "invalid-database-url"} + bootstrap = run(cli, "bootstrap", env=isolated_env) + assert "root=Object parent=None" in bootstrap, bootstrap + assert "instances_distinct=true heap_objects=4" in bootstrap, bootstrap + assert not Path(env["SQLITE_PATH"]).exists(), "bootstrap must not open storage" + assert "sqlite" in run(cli, "metadata", "put", "smoke", "持久化成功", env=env) + assert run(cli, "metadata", "get", "smoke", env=env) == "持久化成功" + failed = subprocess.run([cli, "metadata", "get", "smoke"], cwd=ROOT, env=isolated_env, capture_output=True) + assert failed.returncode != 0, "invalid explicit DB config must fail" + + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + env["ASTER_LISTEN"] = f"127.0.0.1:{port}" + with tempfile.TemporaryFile(mode="w+") as log: + process = subprocess.Popen([server], cwd=ROOT, env=env, stdout=log, stderr=log) + try: + deadline = time.monotonic() + 15 + while True: + try: + with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=1) as response: + health = json.load(response) + break + except (OSError, ValueError): + if process.poll() is not None or time.monotonic() >= deadline: + log.seek(0) + raise AssertionError(f"server did not become healthy: {log.read()}") + time.sleep(0.1) + assert health == {"status": "ok", "storage": "sqlite"}, health + for _ in range(2): + with urllib.request.urlopen(f"http://127.0.0.1:{port}/bootstrap", timeout=2) as response: + info = json.load(response) + assert info == {"root_class": "Object", "root_has_parent": False, "heap_objects": 1}, info + finally: + process.send_signal(signal.SIGTERM) + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + raise AssertionError("server did not shut down gracefully") + assert process.returncode == 0, process.returncode + assert run(cli, "metadata", "get", "host.version", env=env) == "0.1.0" + print("PASS: bootstrap isolation; SQLite persistence; config failure; HTTP health/bootstrap; SIGTERM shutdown") + + +if __name__ == "__main__": + main()