├── .github ├── dependabot.yml └── workflows │ ├── cargo-deny-pr.yml │ ├── ci.yml │ └── homebrew.yml ├── .gitignore ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets └── logo.svg ├── benches └── benchmark.rs ├── crates ├── hash-map-id │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-common-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-control-axum │ ├── Cargo.toml │ └── src │ │ ├── api.rs │ │ ├── lib.rs │ │ ├── routes.rs │ │ └── server.rs ├── lunatic-control-submillisecond │ ├── .cargo │ │ └── config.toml │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── api.rs │ │ ├── host.rs │ │ ├── main.rs │ │ ├── routes.rs │ │ ├── server.rs │ │ └── server │ │ └── store.rs ├── lunatic-control │ ├── Cargo.toml │ └── src │ │ ├── api.rs │ │ └── lib.rs ├── lunatic-distributed-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-distributed │ ├── Cargo.toml │ └── src │ │ ├── congestion │ │ └── mod.rs │ │ ├── control │ │ ├── cert.rs │ │ ├── client.rs │ │ ├── mod.rs │ │ └── server.rs │ │ ├── distributed │ │ ├── client.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ └── server.rs │ │ ├── lib.rs │ │ └── quic │ │ ├── mod.rs │ │ └── quin.rs ├── lunatic-error-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-messaging-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-metrics-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-networking-api │ ├── Cargo.toml │ └── src │ │ ├── dns.rs │ │ ├── lib.rs │ │ ├── tcp.rs │ │ ├── tls_tcp.rs │ │ └── udp.rs ├── lunatic-process-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-process │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ ├── env.rs │ │ ├── lib.rs │ │ ├── mailbox.rs │ │ ├── message.rs │ │ ├── runtimes │ │ ├── mod.rs │ │ └── wasmtime.rs │ │ ├── state.rs │ │ └── wasm.rs ├── lunatic-registry-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-sqlite-api │ ├── Cargo.toml │ └── src │ │ ├── guest_api │ │ ├── guest_bindings.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── sqlite_bindings.rs │ │ └── wire_format │ │ ├── bind_values │ │ ├── host_api.rs │ │ └── mod.rs │ │ ├── mod.rs │ │ └── sqlite_value │ │ ├── guest_api.rs │ │ ├── host_api.rs │ │ └── mod.rs ├── lunatic-stdout-capture │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-timer-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-trap-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── lunatic-version-api │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── lunatic-wasi-api │ ├── Cargo.toml │ └── src │ └── lib.rs ├── deny.toml ├── examples └── native_process.rs ├── src ├── cargo_lunatic.rs ├── config.rs ├── lib.rs ├── main.rs ├── mode │ ├── app.rs │ ├── cargo_test.rs │ ├── common.rs │ ├── config.rs │ ├── control.rs │ ├── deploy │ │ ├── README.md │ │ ├── artefact.rs │ │ ├── build.rs │ │ └── mod.rs │ ├── execution.rs │ ├── init.rs │ ├── login.rs │ ├── mod.rs │ ├── node.rs │ └── run.rs └── state.rs └── wat ├── all_imports.wat └── hello.wat /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "02:00" # UTC 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/cargo-deny-pr.yml: -------------------------------------------------------------------------------- 1 | name: cargo deny 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**/Cargo.toml' 7 | pull_request: 8 | paths: 9 | - '**/Cargo.toml' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | cargo-deny: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | checks: 21 | - advisories 22 | - bans licenses sources 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: EmbarkStudios/cargo-deny-action@v1 27 | with: 28 | command: check ${{ matrix.checks }} 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow does 2 things: 2 | # - For every push & pull request it will run the tests. 3 | # - For every tag it will create a release. 4 | # The tags must be in the following format: "vX.Y.Z", where X.Y.Z is the release version. 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | name: Test & (release) 11 | 12 | jobs: 13 | test_or_release: 14 | name: Test & (release) 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | include: 19 | - os: ubuntu-latest 20 | target_name: lunatic 21 | asset_name: lunatic-linux-amd64.tar.gz 22 | content_type: application/gzip 23 | - os: macos-11 24 | target_name: lunatic 25 | asset_name: lunatic-macos-universal.tar.gz 26 | content_type: application/gzip 27 | - os: windows-latest 28 | target_name: lunatic.exe 29 | asset_name: lunatic-windows-amd64.zip 30 | content_type: application/zip 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v3 34 | - name: Install latest Rust 35 | if: runner.os != 'macOS' 36 | uses: actions-rs/toolchain@v1 37 | with: 38 | toolchain: stable 39 | override: true 40 | components: rustfmt, clippy 41 | - name: Install latest Rust with an additional AArch64 target on macOS 42 | if: runner.os == 'macOS' 43 | uses: actions-rs/toolchain@v1 44 | with: 45 | toolchain: stable 46 | target: aarch64-apple-darwin 47 | override: true 48 | components: rustfmt, clippy 49 | # Rust builds can take some time, cache them. 50 | - uses: Swatinem/rust-cache@v2 51 | - name: "Run clippy check" 52 | run: cargo clippy --examples --tests --benches -- -D warnings 53 | - name: "Check formatting" 54 | run: cargo fmt -- --check 55 | - name: "Run tests" 56 | run: cargo test --all 57 | 58 | # Create a release: 59 | # - The next steps will only run if a tag was added during the push 60 | - name: Build project on Linux and Windows 61 | if: startsWith(github.ref, 'refs/tags/') && runner.os != 'macOS' 62 | run: | 63 | cargo build --release 64 | mv ./target/release/${{ matrix.target_name }} ${{ matrix.target_name }} 65 | - name: Build project on macOs and package into universal binary 66 | if: startsWith(github.ref, 'refs/tags/') && runner.os == 'macOS' 67 | run: | 68 | cargo build --release --target x86_64-apple-darwin 69 | cargo build --release --target aarch64-apple-darwin 70 | lipo -create -output lunatic target/aarch64-apple-darwin/release/lunatic target/x86_64-apple-darwin/release/lunatic 71 | - name: Tar release on Unix 72 | if: startsWith(github.ref, 'refs/tags/') && runner.os != 'Windows' 73 | run: tar czf ${{ matrix.asset_name }} README.md LICENSE-MIT LICENSE-APACHE ${{ matrix.target_name }} 74 | - name: Zip release on Windows 75 | if: startsWith(github.ref, 'refs/tags/') && runner.os == 'Windows' 76 | uses: vimtor/action-zip@v1 77 | with: 78 | files: README.md LICENSE-MIT LICENSE-APACHE ${{ matrix.target_name }} 79 | dest: ${{ matrix.asset_name }} 80 | - name: Get release name 81 | if: startsWith(github.ref, 'refs/tags/') 82 | id: getReleaseName 83 | run: echo "RELEASE_NAME=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT 84 | - name: Generate release notes 85 | if: startsWith(github.ref, 'refs/tags/') 86 | run: | 87 | awk '/^## v[0-9]+\.[0-9]+\.[0-9]+/ && STATE=="show" { exit } 88 | STATE=="show"; 89 | /^## ${{ steps.getReleaseName.outputs.RELEASE_NAME }}/ { STATE="catch" } 90 | /^Released [0-9]+-[0-9]+-[0-9]+/ && STATE=="catch" { STATE="show" }' CHANGELOG.md \ 91 | | awk 'NF { SHOW=1 } SHOW' > RELEASE_NOTES.md 92 | - name: Release 93 | if: startsWith(github.ref, 'refs/tags/') 94 | uses: softprops/action-gh-release@v1 95 | with: 96 | tag_name: ${{ steps.getReleaseName.outputs.RELEASE_NAME }} 97 | name: Lunatic ${{ steps.getReleaseName.outputs.RELEASE_NAME }} 98 | body_path: RELEASE_NOTES.md 99 | draft: true 100 | files: ${{ matrix.asset_name }} 101 | -------------------------------------------------------------------------------- /.github/workflows/homebrew.yml: -------------------------------------------------------------------------------- 1 | name: Update lunatic homebrew version 2 | on: 3 | release: 4 | types: published 5 | jobs: 6 | update-version: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: "Update version" 10 | run: | 11 | export VERSION=$(echo ${GITHUB_REF/refs\/tags\//} | sed -E 's/.*v([[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+)/\1/') 12 | curl -X POST \ 13 | -H 'Authorization: token ${{ secrets.HOMEBREW }}' \ 14 | -H "Accept: application/vnd.github.v3+json" \ 15 | https://api.github.com/repos/lunatic-solutions/homebrew-lunatic/actions/workflows/ci.yml/dispatches \ 16 | -d '{"ref": "main", 17 | "inputs": { "version": "'$VERSION'" } 18 | }' 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | .vscode 4 | .idea 5 | debug.log 6 | notes-*.md 7 | publish.sh 8 | control_server.db 9 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # TODO -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [me@kolobara.com](mailto:bernard@lunatic.solutions). 59 | All complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [the contributor covenant code of conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html). 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Lunatic 2 | 3 | Thanks for contributing to Lunatic! 4 | 5 | Before continuing please read our [code of conduct][code-of-conduct] which all 6 | contributors are expected to adhere to. 7 | 8 | [code-of-conduct]: https://github.com/lunatic-solutions/lunatic/blob/main/CODE_OF_CONDUCT.md 9 | 10 | ## Contributing bug reports 11 | 12 | If you have found a bug in Lunatic please check to see if there is an open 13 | ticket for this problem on [our GitHub issue tracker][issues]. If you cannot 14 | find an existing ticket for the bug please open a new one. 15 | 16 | [issues]: https://github.com/lunatic-lang/lunatic/issues 17 | 18 | A bug may be a technical problem such as a compiler crash or an incorrect 19 | return value from a library function, or a user experience issue such as 20 | unclear or absent documentation. If you are unsure if your problem is a bug 21 | please open a ticket and we will work it out together. 22 | 23 | ## Contributing code changes 24 | 25 | Code changes to Lunatic are welcomed via the process below. 26 | 27 | 1. Find or open a GitHub issue relevant to the change you wish to make and 28 | comment saying that you wish to work on this issue. If the change 29 | introduces new functionality or behaviour this would be a good time to 30 | discuss the details of the change to ensure we are in agreement as to how 31 | the new functionality should work. 32 | 2. Please use `cargo fmt` and `cargo clippy` to check that code is properly 33 | formatted, and linted for potential problems. 34 | 3. Changes, adding and removing host functions require changes to the 35 | `wat/all_imports.wat` file. Every host function lunatic exposes requires an 36 | import directive to assert that end developers can import the function. 37 | 4. Open a GitHub pull request with your changes and ensure the tests and build 38 | pass on CI. 39 | 5. A Lunatic team member will review the changes and may provide feedback to 40 | work on. Depending on the change there may be multiple rounds of feedback. 41 | 6. Once the changes have been approved the code will be rebased into the 42 | `main` branch. 43 | 44 | ## Local development 45 | 46 | To build the project run: 47 | 48 | ```shell 49 | cargo build 50 | ``` 51 | 52 | or for release builds: 53 | 54 | ```shell 55 | cargo build --release 56 | ``` 57 | 58 | To run the tests: 59 | 60 | ```shell 61 | cargo test 62 | ``` 63 | 64 | ## Changelog generation 65 | 66 | The changelog is updated using the [git-cliff](https://git-cliff.org/) cli, 67 | which generates the changelog file from the [Git](https://git-scm.com/) history by utilizing [conventional commits](https://git-cliff.org/#conventional_commits). 68 | 69 | The changelog template is defined in [Cargo.toml](/Cargo.toml) under `[workspace.metadata.git-cliff.*]`. 70 | 71 | Updating the CHANGELOG.md file can be achieved with the following command: 72 | 73 | ```bash 74 | git cliff --config ./Cargo.toml --latest --prepend ./CHANGELOG.md 75 | ``` 76 | 77 | The commit types are as follows: 78 | 79 | * **feat**: A new feature 80 | * **fix**: A bug fix 81 | * **docs**: Documentation only changes 82 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 83 | * **refactor**: A code change that neither fixes a bug nor adds a feature 84 | * **perf**: A code change that improves performance 85 | * **test**: Adding missing or correcting existing tests 86 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation 87 | 88 | For more information, see the [git-cliff usage documentation](https://git-cliff.org/#usage). 89 | 90 | ## Useful resources 91 | - [Project Loom on virtual threads](https://cr.openjdk.org/~rpressler/loom/loom/sol1_part1.html) 92 | - [Erlang documentation](https://www.erlang.org/docs) - these explain some concepts that Lunatic implements 93 | - [Notes on distributed systems](http://cs-www.cs.yale.edu/homes/aspnes/classes/465/notes.pdf) - explains some distributed algorithms (possibly useful for working on distributed Lunatic) 94 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-runtime" 3 | version = "0.13.2" 4 | authors = ["Bernard Kolobara "] 5 | edition = "2018" 6 | description = "An actor platform built on WebAssembly" 7 | homepage = "https://lunatic.solutions" 8 | repository = "https://github.com/lunatic-solutions/lunatic" 9 | categories = ["wasm"] 10 | license = "Apache-2.0 OR MIT" 11 | readme = "README.md" 12 | default-run = "lunatic" 13 | 14 | [lib] 15 | name = "lunatic_runtime" 16 | path = "src/lib.rs" 17 | 18 | [[bin]] 19 | name = "lunatic" 20 | path = "src/main.rs" 21 | 22 | [[bin]] 23 | name = "cargo-lunatic" 24 | path = "src/cargo_lunatic.rs" 25 | 26 | [features] 27 | default = ["metrics"] 28 | metrics = [ 29 | "lunatic-process-api/metrics", 30 | "lunatic-process/metrics", 31 | "lunatic-registry-api/metrics", 32 | "lunatic-timer-api/metrics", 33 | "dep:lunatic-metrics-api", 34 | ] 35 | prometheus = ["dep:metrics-exporter-prometheus", "metrics"] 36 | 37 | [dependencies] 38 | hash-map-id = { workspace = true } 39 | lunatic-control = { workspace = true } 40 | lunatic-control-axum = { workspace = true } 41 | lunatic-distributed = { workspace = true } 42 | lunatic-distributed-api = { workspace = true } 43 | lunatic-error-api = { workspace = true } 44 | lunatic-messaging-api = { workspace = true } 45 | lunatic-networking-api = { workspace = true } 46 | lunatic-process = { workspace = true } 47 | lunatic-process-api = { workspace = true } 48 | lunatic-registry-api = { workspace = true } 49 | lunatic-stdout-capture = { workspace = true } 50 | lunatic-timer-api = { workspace = true } 51 | lunatic-version-api = { workspace = true } 52 | lunatic-metrics-api = { workspace = true, optional = true } 53 | lunatic-wasi-api = { workspace = true } 54 | lunatic-trap-api = { workspace = true } 55 | lunatic-sqlite-api = { workspace = true } 56 | 57 | anyhow = { workspace = true } 58 | async-ctrlc = "1.2.0" 59 | clap = { version = "4.0", features = ["cargo", "derive"] } 60 | dashmap = { workspace = true } 61 | dirs = "4.0.0" 62 | dotenvy = "0.15.7" 63 | env_logger = "0.9" 64 | log = { workspace = true } 65 | metrics-exporter-prometheus = { version = "0.11.0", optional = true } 66 | regex = "1.7" 67 | reqwest = { workspace = true } 68 | serde = { workspace = true, features = ["derive"] } 69 | serde_json = "1.0.89" 70 | tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net"] } 71 | toml = "0.5" 72 | url = "2.2.2" 73 | url_serde = "0.2.0" 74 | uuid = { workspace = true } 75 | wasmtime = { workspace = true } 76 | wasmtime-wasi = { workspace = true } 77 | walkdir = "2.3.3" 78 | zip = "0.6.6" 79 | 80 | [dev-dependencies] 81 | criterion = { version = "0.4", features = ["async_tokio"] } 82 | tokio = { workspace = true, features = ["rt-multi-thread"] } 83 | wat = "1.0" 84 | 85 | [[bench]] 86 | harness = false 87 | name = "benchmark" 88 | 89 | [workspace] 90 | members = [ 91 | "crates/hash-map-id", 92 | "crates/lunatic-common-api", 93 | "crates/lunatic-control", 94 | "crates/lunatic-control-axum", 95 | # "crates/lunatic-control-submillisecond", 96 | "crates/lunatic-distributed-api", 97 | "crates/lunatic-distributed", 98 | "crates/lunatic-error-api", 99 | "crates/lunatic-messaging-api", 100 | "crates/lunatic-process-api", 101 | "crates/lunatic-process", 102 | "crates/lunatic-registry-api", 103 | "crates/lunatic-stdout-capture", 104 | "crates/lunatic-timer-api", 105 | "crates/lunatic-version-api", 106 | "crates/lunatic-wasi-api", 107 | "crates/lunatic-trap-api", 108 | "crates/lunatic-sqlite-api", 109 | ] 110 | 111 | [workspace.dependencies] 112 | hash-map-id = { path = "crates/hash-map-id", version = "0.13" } 113 | lunatic-common-api = { path = "crates/lunatic-common-api", version = "0.13" } 114 | lunatic-control = { path = "crates/lunatic-control", version = "0.13" } 115 | lunatic-control-axum = { path = "crates/lunatic-control-axum", version = "0.13" } 116 | lunatic-control-submillisecond = { path = "crates/lunatic-control-submillisecond", version = "0.13" } 117 | lunatic-distributed = { path = "crates/lunatic-distributed", version = "0.13" } 118 | lunatic-distributed-api = { path = "crates/lunatic-distributed-api", version = "0.13" } 119 | lunatic-error-api = { path = "crates/lunatic-error-api", version = "0.13" } 120 | lunatic-messaging-api = { path = "crates/lunatic-messaging-api", version = "0.13" } 121 | lunatic-metrics-api = { path = "crates/lunatic-metrics-api", version = "0.13" } 122 | lunatic-networking-api = { path = "crates/lunatic-networking-api", version = "0.13" } 123 | lunatic-process = { path = "crates/lunatic-process", version = "0.13" } 124 | lunatic-process-api = { path = "crates/lunatic-process-api", version = "0.13" } 125 | lunatic-registry-api = { path = "crates/lunatic-registry-api", version = "0.13" } 126 | lunatic-sqlite-api = { path = "crates/lunatic-sqlite-api", version = "0.13" } 127 | lunatic-stdout-capture = { path = "crates/lunatic-stdout-capture", version = "0.13" } 128 | lunatic-timer-api = { path = "crates/lunatic-timer-api", version = "0.13" } 129 | lunatic-trap-api = { path = "crates/lunatic-trap-api", version = "0.13" } 130 | lunatic-version-api = { path = "crates/lunatic-version-api", version = "0.13" } 131 | lunatic-wasi-api = { path = "crates/lunatic-wasi-api", version = "0.13" } 132 | 133 | anyhow = "1.0" 134 | bincode = "1.3" 135 | dashmap = "5.4" 136 | log = "0.4" 137 | metrics = "0.20.1" 138 | reqwest = {version = "0.11.18", features = ["cookies", "multipart"]} 139 | rustls-pemfile = "1.0" 140 | serde = "1.0" 141 | tokio = "1.28" 142 | uuid = { version = "1.1", features = ["v4"] } 143 | wasi-common = "8" 144 | wasmtime = "8" 145 | wasmtime-wasi = "8" 146 | wiggle = "8" 147 | 148 | [workspace.metadata.git-cliff.changelog] 149 | header = """ 150 | # Lunatic Changelog 151 | 152 | """ 153 | 154 | body = """ 155 | {% if version %}\ 156 | ## [{{ version | trim_start_matches(pat="v") }}] 157 | 158 | Released {{ timestamp | date(format="%Y-%m-%d") }}. 159 | {% else %}\ 160 | ## [unreleased] 161 | {% endif %}\ 162 | {% for group, commits in commits | group_by(attribute="group") %} 163 | ### {{ group | upper_first }} 164 | {% for commit in commits %} 165 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }} [`{{ commit.id | truncate(length=7, end="") }}`](https://github.com/lunatic-solutions/lunatic/commit/{{ commit.id | urlencode }})\ 166 | {% endfor %} 167 | {% endfor %}\n 168 | """ 169 | 170 | footer = "" 171 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) Bernard Kolobara 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | lunatic logo 7 | 8 |

 

9 |
10 | 11 | Lunatic is a universal runtime for **fast**, **robust** and **scalable** server-side applications. 12 | It's inspired by Erlang and can be used from any language that compiles to [WebAssembly][1]. 13 | You can read more about the motivation behind Lunatic [here][2]. 14 | 15 | We currently provide libraries to take full advantage of Lunatic's features for: 16 | 17 | - [Rust][3] 18 | - [AssemblyScript][11] 19 | 20 | If you would like to see other languages supported or just follow the discussions around Lunatic, 21 | [join our discord server][4]. 22 | 23 | ## Supported features 24 | 25 | - [x] Creating, cancelling & waiting on processes 26 | - [x] Fine-grained process permissions 27 | - [x] Process supervision 28 | - [x] Channel based message passing 29 | - [x] TCP networking 30 | - [x] Filesystem access 31 | - [x] Distributed nodes 32 | - [ ] Hot reloading 33 | 34 | ## Installation 35 | 36 | If you have rust (cargo) installed, you can build and install the lunatic runtime with: 37 | 38 | ```bash 39 | cargo install lunatic-runtime 40 | ``` 41 | 42 | --- 43 | 44 | On **macOS** you can use [Homebrew][6] too: 45 | 46 | ```bash 47 | brew tap lunatic-solutions/lunatic 48 | brew install lunatic 49 | ``` 50 | 51 | --- 52 | 53 | We also provide pre-built binaries for **Windows**, **Linux** and **macOS** on the 54 | [releases page][5], that you can include in your `PATH`. 55 | 56 | --- 57 | 58 | And as always, you can also clone this repository and build it locally. The only dependency is 59 | [a rust compiler][7]: 60 | 61 | ```bash 62 | # Clone the repository 63 | git clone https://github.com/lunatic-solutions/lunatic.git 64 | # Jump into the cloned folder 65 | cd lunatic 66 | # Build and install lunatic 67 | cargo install --path . 68 | ``` 69 | 70 | ## Usage 71 | 72 | After installation, you can use the `lunatic` binary to run WASM modules. 73 | 74 | To learn how to build modules, check out language-specific bindings: 75 | 76 | - [Rust](https://github.com/lunatic-solutions/rust-lib) 77 | - [AssemblyScript](https://github.com/lunatic-solutions/as-lunatic) 78 | 79 | ## Architecture 80 | 81 | Lunatic's design is all about spawning _super lightweight_ processes, also known as green threads or 82 | [go-routines][8] in other runtimes. Lunatic's processes are fast to create, have a small memory footprint 83 | and a low scheduling overhead. They are designed for **massive** concurrency. It's not uncommon to have 84 | hundreds of thousands of such processes concurrently running in your app. 85 | 86 | Some common use cases for processes are: 87 | 88 | - HTTP request handling 89 | - Long running requests, like WebSocket connections 90 | - Long running background tasks, like email sending 91 | - Calling untrusted libraries in an sandboxed environment 92 | 93 | ### Isolation 94 | 95 | What makes the last use case possible are the sandboxing capabilities of [WebAssembly][1]. WebAssembly was 96 | originally developed to run in the browser and provides extremely strong sandboxing on multiple levels. 97 | Lunatic's processes inherit these properties. 98 | 99 | Each process has its own stack, heap, and even syscalls. If one process fails, it will not affect the rest 100 | of the system. This allows you to create very powerful and fault-tolerant abstraction. 101 | 102 | This is also true for some other runtimes, but Lunatic goes one step further and makes it possible to use C 103 | bindings directly in your app without any fear. If the C code contains any security vulnerabilities or crashes, 104 | those issues will only affect the process currently executing the code. The only requirement is that the C 105 | code can be compiled to WebAssembly. 106 | 107 | It's possible to give per process fine-grained access to resources (filesystem, memory, network connections, ...). 108 | This is enforced on the syscall level. 109 | 110 | ### Scheduling 111 | 112 | All processes running on Lunatic are preemptively scheduled and executed by a [work stealing async executor][9]. This 113 | gives you the freedom to write simple _blocking_ code, but the runtime is going to make sure it actually never blocks 114 | a thread if waiting on I/O. 115 | 116 | Even if you have an infinite loop somewhere in your code, the scheduling will always be fair and not permanently block 117 | the execution thread. The best part is that you don't need to do anything special to achieve this, the runtime will take 118 | care of it no matter which programming language you use. 119 | 120 | ### Compatibility 121 | 122 | We intend to eventually make Lunatic completely compatible with [WASI][10]. Ideally, you could take existing code, 123 | compile it to WebAssembly and run on top of Lunatic; creating the best developer experience possible. We're not 124 | quite there yet. 125 | 126 | ## License 127 | 128 | Licensed under either of 129 | 130 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 131 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 132 | 133 | at your option. 134 | 135 | [1]: https://webassembly.org/ 136 | [2]: https://kolobara.com/lunatic/index.html#motivation 137 | [3]: https://crates.io/crates/lunatic 138 | [4]: https://discord.gg/b7zDqpXpB4 139 | [5]: https://github.com/lunatic-solutions/lunatic/releases 140 | [6]: https://brew.sh/ 141 | [7]: https://rustup.rs/ 142 | [8]: https://golangbot.com/goroutines 143 | [9]: https://tokio.rs 144 | [10]: https://wasi.dev/ 145 | [11]: https://github.com/lunatic-solutions/as-lunatic 146 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /benches/benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | // TODO: Re-export this under lunatic_runtime 5 | use lunatic_process::{ 6 | env::LunaticEnvironment, 7 | runtimes::wasmtime::{default_config, WasmtimeRuntime}, 8 | }; 9 | use lunatic_runtime::{state::DefaultProcessState, DefaultProcessConfig}; 10 | use tokio::sync::RwLock; 11 | 12 | fn criterion_benchmark(c: &mut Criterion) { 13 | let rt = tokio::runtime::Runtime::new().unwrap(); 14 | 15 | let config = Arc::new(DefaultProcessConfig::default()); 16 | let wasmtime_config = default_config(); 17 | let runtime = WasmtimeRuntime::new(&wasmtime_config).unwrap(); 18 | 19 | let raw_module = wat::parse_file("./wat/hello.wat").unwrap(); 20 | let module = Arc::new( 21 | runtime 22 | .compile_module::(raw_module.into()) 23 | .unwrap(), 24 | ); 25 | 26 | let env = Arc::new(LunaticEnvironment::new(0)); 27 | c.bench_function("spawn process", |b| { 28 | b.to_async(&rt).iter(|| async { 29 | let registry = Arc::new(RwLock::new(HashMap::new())); 30 | let state = DefaultProcessState::new( 31 | env.clone(), 32 | None, 33 | runtime.clone(), 34 | module.clone(), 35 | config.clone(), 36 | registry, 37 | ) 38 | .unwrap(); 39 | lunatic_process::wasm::spawn_wasm( 40 | env.clone(), 41 | runtime.clone(), 42 | &module, 43 | state, 44 | "hello", 45 | Vec::new(), 46 | None, 47 | ) 48 | .await 49 | .unwrap() 50 | .0 51 | .await 52 | .unwrap() 53 | .ok(); 54 | }); 55 | }); 56 | } 57 | 58 | criterion_group!(benches, criterion_benchmark); 59 | criterion_main!(benches); 60 | -------------------------------------------------------------------------------- /crates/hash-map-id/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hash-map-id" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "HashMap wrapper with incremental ID (u64) as key" 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/hash-map-id" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /crates/hash-map-id/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{any::type_name, collections::HashMap, fmt::Debug}; 2 | 3 | /// HashMap wrapper with incremental ID (u64) assignment. 4 | pub struct HashMapId { 5 | id_seed: u64, 6 | store: HashMap, 7 | } 8 | 9 | impl HashMapId 10 | where 11 | T: Send + Sync, 12 | { 13 | pub fn new() -> Self { 14 | Self { 15 | id_seed: 0, 16 | store: HashMap::new(), 17 | } 18 | } 19 | 20 | pub fn add(&mut self, item: T) -> u64 { 21 | let id = self.id_seed; 22 | self.store.insert(id, item); 23 | self.id_seed += 1; 24 | id 25 | } 26 | 27 | pub fn remove(&mut self, id: u64) -> Option { 28 | self.store.remove(&id) 29 | } 30 | 31 | pub fn get_mut(&mut self, id: u64) -> Option<&mut T> { 32 | self.store.get_mut(&id) 33 | } 34 | 35 | pub fn get(&self, id: u64) -> Option<&T> { 36 | self.store.get(&id) 37 | } 38 | } 39 | 40 | impl Default for HashMapId 41 | where 42 | T: Send + Sync, 43 | { 44 | fn default() -> Self { 45 | Self::new() 46 | } 47 | } 48 | 49 | impl Debug for HashMapId { 50 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 51 | f.debug_struct("HashMapId") 52 | .field("id_seed", &self.id_seed) 53 | .field("type", &type_name::()) 54 | .finish() 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | 62 | // get 63 | 64 | #[test] 65 | fn get_returns_none_for_non_existent_item() { 66 | let hash: HashMapId = HashMapId::new(); 67 | let item = hash.get(10); 68 | assert!(item.is_none()); 69 | } 70 | 71 | #[test] 72 | fn get_returns_reference_to_item() { 73 | let mut hash: HashMapId = HashMapId::new(); 74 | let value = 10; 75 | let id = hash.add(value); 76 | let item = hash.get(id); 77 | assert_eq!(item, Some(&value)); 78 | } 79 | 80 | // get_mut 81 | 82 | #[test] 83 | fn get_mut_returns_mutable_reference() { 84 | let mut hash: HashMapId = HashMapId::new(); 85 | let value = 10; 86 | let id = hash.add(value); 87 | let item = hash.get_mut(id); 88 | assert_eq!(item, Some(&mut value.clone())); 89 | } 90 | 91 | #[test] 92 | fn get_mut_returns_none_for_non_existent_item() { 93 | let mut hash: HashMapId = HashMapId::new(); 94 | let item = hash.get_mut(0); 95 | assert!(item.is_none()); 96 | } 97 | 98 | // add 99 | 100 | #[test] 101 | fn add_adds_item_to_store() { 102 | let mut hash: HashMapId = HashMapId::new(); 103 | let item = 10; 104 | let id = hash.add(item); 105 | assert_eq!(hash.get(id), Some(&item)); 106 | } 107 | 108 | #[test] 109 | fn add_can_add_multiple_items() { 110 | let mut hash: HashMapId = HashMapId::new(); 111 | let item1 = 10; 112 | let item2 = 20; 113 | let id1 = hash.add(item1); 114 | let id2 = hash.add(item2); 115 | assert_ne!(id1, id2); 116 | assert_eq!(hash.get(id1), Some(&item1)); 117 | assert_eq!(hash.get(id2), Some(&item2)); 118 | } 119 | 120 | #[test] 121 | fn add_increments_id() { 122 | let mut hash: HashMapId = HashMapId::new(); 123 | let id1 = hash.add(10); 124 | let id2 = hash.add(20); 125 | assert_eq!(id2, id1 + 1); 126 | } 127 | 128 | #[test] 129 | #[should_panic] 130 | fn add_panics_on_integer_overflow() { 131 | let mut hash: HashMapId = HashMapId::new(); 132 | let item = 10; 133 | hash.id_seed = std::u64::MAX; 134 | hash.add(item); 135 | } 136 | 137 | // remove 138 | 139 | #[test] 140 | fn remove_handles_non_existent_id() { 141 | let mut hash: HashMapId = HashMapId::new(); 142 | let result = hash.remove(1); 143 | assert!(result.is_none()); 144 | } 145 | 146 | #[test] 147 | fn remove_returns_removed_value() { 148 | let mut hash: HashMapId = HashMapId::new(); 149 | let value = 10; 150 | let id = hash.add(value); 151 | let removed_value = hash.remove(id); 152 | assert_eq!(removed_value, Some(value)); 153 | } 154 | 155 | #[test] 156 | fn remove_removes_id() { 157 | let mut hash: HashMapId = HashMapId::new(); 158 | let id = hash.add(10); 159 | hash.remove(id); 160 | assert_eq!(hash.get(id), None); 161 | } 162 | 163 | #[test] 164 | fn remove_does_not_affect_other_items() { 165 | let mut hash: HashMapId = HashMapId::new(); 166 | 167 | let value1 = 10; 168 | let value2 = 20; 169 | let value3 = 30; 170 | 171 | let id1 = hash.add(value1); 172 | let id2 = hash.add(value2); 173 | let id3 = hash.add(value3); 174 | 175 | hash.remove(id2); 176 | 177 | assert_eq!(hash.get(id1), Some(&value1)); 178 | assert!(hash.get(id2).is_none()); 179 | assert_eq!(hash.get(id3), Some(&value3)); 180 | } 181 | 182 | // impl 183 | 184 | #[test] 185 | fn default_creates_new_hashmapid() { 186 | let hash: HashMapId = HashMapId::default(); 187 | assert_eq!(hash.id_seed, 0); 188 | assert!(hash.store.is_empty()); 189 | } 190 | 191 | // fmt 192 | 193 | #[test] 194 | fn fmt_formats_the_hashmapid() { 195 | let mut hash: HashMapId = HashMapId::default(); 196 | hash.add(10); 197 | 198 | let expected = format!("HashMapId {{ id_seed: {}, type: \"i32\" }}", hash.id_seed); 199 | let result = format!("{:?}", hash); 200 | 201 | assert_eq!(result, expected); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /crates/lunatic-common-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-common-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Common functionality for building lunatic host function APIs." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-common-api" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | anyhow = { workspace = true } 12 | wasmtime = { workspace = true } 13 | -------------------------------------------------------------------------------- /crates/lunatic-common-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use std::{fmt::Display, future::Future, io::Write, pin::Pin}; 3 | use wasmtime::{Caller, Memory, Val}; 4 | 5 | const ALLOCATOR_FUNCTION_NAME: &str = "lunatic_alloc"; 6 | const FREEING_FUNCTION_NAME: &str = "lunatic_free"; 7 | 8 | // Get exported memory 9 | pub fn get_memory(caller: &mut Caller) -> Result { 10 | caller 11 | .get_export("memory") 12 | .or_trap("No export `memory` found")? 13 | .into_memory() 14 | .or_trap("Export `memory` is not a memory") 15 | } 16 | 17 | // Call guest to allocate a Vec of size `size` 18 | pub fn allocate_guest_memory<'a, T: Send>( 19 | caller: &'a mut Caller, 20 | size: u32, 21 | ) -> Pin> + Send + 'a>> { 22 | Box::pin(async move { 23 | let mut results = [Val::I32(0)]; 24 | caller 25 | .get_export(ALLOCATOR_FUNCTION_NAME) 26 | .or_trap(format!("no export named {ALLOCATOR_FUNCTION_NAME} found"))? 27 | .into_func() 28 | .or_trap("cannot turn export into func")? 29 | .call_async(caller, &[Val::I32(size as i32)], &mut results) 30 | .await 31 | .or_trap(format!("failed to call {ALLOCATOR_FUNCTION_NAME}"))?; 32 | 33 | Ok(results[0] 34 | .i32() 35 | .or_trap(format!("result of {ALLOCATOR_FUNCTION_NAME} is not i32"))? as u32) 36 | }) 37 | } 38 | 39 | // Call guest to free a slice of memory at location ptr 40 | pub fn free_guest_memory<'a, T: Send>( 41 | caller: &'a mut Caller, 42 | ptr: u32, 43 | ) -> Pin> + Send + 'a>> { 44 | Box::pin(async move { 45 | let mut results = []; 46 | let result = caller 47 | .get_export(FREEING_FUNCTION_NAME) 48 | .or_trap(format!("no export named {FREEING_FUNCTION_NAME} found"))? 49 | .into_func() 50 | .or_trap("cannot turn export into func")? 51 | .call_async(caller, &[Val::I32(ptr as i32)], &mut results) 52 | .await; 53 | 54 | result.or_trap(format!("failed to call {FREEING_FUNCTION_NAME}"))?; 55 | Ok(()) 56 | }) 57 | } 58 | 59 | // Allocates and writes data to guest memory, updating the len_ptr and returning the allocated ptr. 60 | pub async fn write_to_guest_vec( 61 | caller: &mut Caller<'_, T>, 62 | memory: &Memory, 63 | data: &[u8], 64 | len_ptr: u32, 65 | ) -> Result { 66 | let alloc_len = data.len(); 67 | let alloc_ptr = allocate_guest_memory(caller, alloc_len as u32).await?; 68 | 69 | let (memory_slice, _) = memory.data_and_store_mut(&mut (*caller)); 70 | let mut alloc_vec = memory_slice 71 | .get_mut(alloc_ptr as usize..(alloc_ptr as usize + alloc_len)) 72 | .context("allocated memory does not exist")?; 73 | 74 | alloc_vec.write_all(data)?; 75 | 76 | memory.write(caller, len_ptr as usize, &alloc_len.to_le_bytes())?; 77 | 78 | Ok(alloc_ptr) 79 | } 80 | 81 | pub trait IntoTrap { 82 | fn or_trap(self, info: S) -> Result; 83 | } 84 | 85 | impl IntoTrap for Result { 86 | fn or_trap(self, info: S) -> Result { 87 | match self { 88 | Ok(result) => Ok(result), 89 | Err(error) => Err(anyhow!( 90 | "Trap raised during host call: {} ({}).", 91 | error, 92 | info 93 | )), 94 | } 95 | } 96 | } 97 | 98 | impl IntoTrap for Option { 99 | fn or_trap(self, info: S) -> Result { 100 | match self { 101 | Some(result) => Ok(result), 102 | None => Err(anyhow!( 103 | "Trap raised during host call: Expected `Some({})` got `None` ({}).", 104 | std::any::type_name::(), 105 | info 106 | )), 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /crates/lunatic-control-axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-control-axum" 3 | version = "0.13.3" 4 | edition = "2021" 5 | description = "TBD" 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | lunatic-control = { workspace = true } 12 | lunatic-distributed = { workspace = true } 13 | 14 | anyhow = { workspace = true } 15 | axum = { version = "0.6", features = ["json", "query", "macros"] } 16 | tower-http = { version = "0.3.0", features = ["limit"] } 17 | base64-url = "2.0" 18 | chrono = { version = "0.4.23", default-features = false, features = ["clock", "std"] } 19 | dashmap = { workspace = true } 20 | getrandom = "0.2.8" 21 | http = "0.2.8" 22 | log = { workspace = true } 23 | rcgen = "0.10" 24 | asn1-rs = "0.5.2" 25 | serde = { workspace = true } 26 | serde_json = "1.0.89" 27 | tokio = { workspace = true, features = ["io-util", "rt", "sync", "time", "fs"] } 28 | uuid = { workspace = true } 29 | -------------------------------------------------------------------------------- /crates/lunatic-control-axum/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod routes; 3 | pub mod server; 4 | -------------------------------------------------------------------------------- /crates/lunatic-control-axum/src/routes.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use asn1_rs::ToDer; 4 | use axum::{ 5 | body::Bytes, 6 | extract::{DefaultBodyLimit, Query}, 7 | routing::{get, post}, 8 | Extension, Json, Router, 9 | }; 10 | use lunatic_control::{api::*, NodeInfo}; 11 | use lunatic_distributed::{control::cert::TEST_ROOT_CERT, CertAttrs, SUBJECT_DIR_ATTRS}; 12 | use rcgen::{CertificateSigningRequest, CustomExtension}; 13 | use tower_http::limit::RequestBodyLimitLayer; 14 | 15 | use crate::{ 16 | api::{ok, ApiError, ApiResponse, HostExtractor, JsonExtractor, NodeAuth, PathExtractor}, 17 | server::ControlServer, 18 | }; 19 | 20 | pub async fn register( 21 | control: Extension>, 22 | HostExtractor(host): HostExtractor, 23 | JsonExtractor(reg): JsonExtractor, 24 | ) -> ApiResponse { 25 | log::info!("Registration for node name {}", reg.node_name); 26 | 27 | let control = control.as_ref(); 28 | 29 | let mut sign_request = CertificateSigningRequest::from_pem(®.csr_pem).map_err(|e| { 30 | ApiError::custom( 31 | "sign_error", 32 | format!("Certificate Signing Request invalid pem format: {}", e), 33 | ) 34 | })?; 35 | // Add json to custom certificate extension 36 | sign_request 37 | .params 38 | .custom_extensions 39 | .push(CustomExtension::from_oid_content( 40 | &SUBJECT_DIR_ATTRS, 41 | serde_json::to_string(&CertAttrs { 42 | allowed_envs: vec![], 43 | is_privileged: true, 44 | }) 45 | .unwrap() 46 | .to_der_vec() 47 | .map_err(|e| ApiError::log_internal("Error serializing allowed envs to der", e))?, 48 | )); 49 | let cert_pem = sign_request 50 | .serialize_pem_with_signer(&control.ca_cert) 51 | .map_err(|e| ApiError::custom("sign_error", e.to_string()))?; 52 | 53 | let mut authentication_token = [0u8; 32]; 54 | getrandom::getrandom(&mut authentication_token) 55 | .map_err(|e| ApiError::log_internal("Error generating random token for registration", e))?; 56 | let authentication_token = base64_url::encode(&authentication_token); 57 | 58 | control.register(®, &cert_pem, &authentication_token); 59 | 60 | ok(Registration { 61 | node_name: reg.node_name, 62 | cert_pem_chain: vec![cert_pem], 63 | authentication_token, 64 | root_cert: TEST_ROOT_CERT.into(), 65 | urls: ControlUrls { 66 | api_base: format!("http://{host}/"), 67 | nodes: format!("http://{host}/nodes"), 68 | node_started: format!("http://{host}/started"), 69 | node_stopped: format!("http://{host}/stopped"), 70 | get_module: format!("http://{host}/module/{{id}}"), 71 | add_module: format!("http://{host}/module"), 72 | get_nodes: format!("http://{host}/nodes"), 73 | }, 74 | envs: Vec::new(), 75 | is_privileged: true, 76 | }) 77 | } 78 | 79 | pub async fn node_stopped( 80 | node_auth: NodeAuth, 81 | control: Extension>, 82 | ) -> ApiResponse<()> { 83 | log::info!("Node {} stopped", node_auth.node_name); 84 | 85 | let control = control.as_ref(); 86 | control.stop_node(node_auth.registration_id as u64); 87 | 88 | ok(()) 89 | } 90 | 91 | pub async fn node_started( 92 | node_auth: NodeAuth, 93 | control: Extension>, 94 | Json(data): Json, 95 | ) -> ApiResponse { 96 | let control = control.as_ref(); 97 | control.stop_node(node_auth.registration_id as u64); 98 | 99 | let (node_id, _node_address) = control.start_node(node_auth.registration_id as u64, data); 100 | 101 | log::info!("Node {} started with id {}", node_auth.node_name, node_id); 102 | 103 | // TODO spawn all modules on node 104 | 105 | ok(NodeStarted { 106 | node_id: node_id as i64, 107 | }) 108 | } 109 | 110 | pub async fn list_nodes( 111 | _node_auth: NodeAuth, 112 | Query(query): Query>, 113 | control: Extension>, 114 | ) -> ApiResponse { 115 | let control = control.as_ref(); 116 | let nds: Vec<_> = control 117 | .nodes 118 | .iter() 119 | .filter(|n| n.status < 2 && !n.node_address.is_empty()) 120 | .collect(); 121 | // Filter nodes based on query params and node attributes 122 | let nds: Vec<_> = if !query.is_empty() { 123 | nds.into_iter() 124 | .filter(|node| query.iter().all(|(k, v)| node.attributes.get(k) == Some(v))) 125 | .collect() 126 | } else { 127 | nds 128 | }; 129 | let nodes: Vec<_> = control 130 | .registrations 131 | .iter() 132 | .filter_map(|r| { 133 | nds.iter() 134 | .find(|n| n.registration_id == *r.key()) 135 | .map(|n| NodeInfo { 136 | id: *n.key(), 137 | address: n.node_address.parse().unwrap(), 138 | name: r.node_name.to_string(), 139 | }) 140 | }) 141 | .collect(); 142 | 143 | ok(NodesList { nodes }) 144 | } 145 | 146 | pub async fn add_module( 147 | node_auth: NodeAuth, 148 | control: Extension>, 149 | body: Bytes, 150 | ) -> ApiResponse { 151 | log::info!("Node {} add_module", node_auth.node_name); 152 | 153 | let control = control.as_ref(); 154 | let module_id = control.add_module(body.to_vec()); 155 | ok(ModuleId { module_id }) 156 | } 157 | 158 | pub async fn get_module( 159 | node_auth: NodeAuth, 160 | PathExtractor(id): PathExtractor, 161 | control: Extension>, 162 | ) -> ApiResponse { 163 | log::info!("Node {} get_module {}", node_auth.node_name, id); 164 | 165 | let bytes = control 166 | .modules 167 | .iter() 168 | .find(|m| m.key() == &id) 169 | .map(|m| m.value().clone()) 170 | .ok_or_else(|| ApiError::custom_code("error_reading_bytes"))?; 171 | 172 | ok(ModuleBytes { bytes }) 173 | } 174 | 175 | pub fn init_routes() -> Router { 176 | Router::new() 177 | .route("/", post(register)) 178 | .route("/stopped", post(node_stopped)) 179 | .route("/started", post(node_started)) 180 | .route("/nodes", get(list_nodes)) 181 | .route("/module", post(add_module)) 182 | .route("/module/:id", get(get_module)) 183 | .layer(DefaultBodyLimit::disable()) 184 | .layer(RequestBodyLimitLayer::new(50 * 1024 * 1024)) // 50 mb 185 | } 186 | -------------------------------------------------------------------------------- /crates/lunatic-control-axum/src/server.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | net::{SocketAddr, TcpListener}, 4 | sync::{ 5 | atomic::{self, AtomicU64}, 6 | Arc, 7 | }, 8 | }; 9 | 10 | use anyhow::Result; 11 | use axum::{Extension, Router}; 12 | use chrono::{DateTime, Utc}; 13 | use dashmap::DashMap; 14 | use lunatic_control::api::{NodeStart, Register}; 15 | use rcgen::Certificate; 16 | use uuid::Uuid; 17 | 18 | use crate::routes; 19 | 20 | pub struct ControlServer { 21 | pub ca_cert: Certificate, 22 | pub quic_client: lunatic_distributed::quic::Client, 23 | pub registrations: DashMap, 24 | pub nodes: DashMap, 25 | pub modules: DashMap>, 26 | next_registration_id: AtomicU64, 27 | next_node_id: AtomicU64, 28 | next_module_id: AtomicU64, 29 | } 30 | 31 | #[derive(Clone)] 32 | pub struct Registered { 33 | pub node_name: Uuid, 34 | pub csr_pem: String, 35 | pub cert_pem: String, 36 | pub authentication_token: String, 37 | } 38 | 39 | pub struct NodeDetails { 40 | pub registration_id: u64, 41 | pub status: i16, 42 | pub created_at: DateTime, 43 | pub stopped_at: Option>, 44 | pub node_address: String, 45 | pub attributes: HashMap, 46 | } 47 | 48 | impl ControlServer { 49 | pub fn new(ca_cert: Certificate, quic_client: lunatic_distributed::quic::Client) -> Self { 50 | Self { 51 | ca_cert, 52 | quic_client, 53 | registrations: DashMap::new(), 54 | nodes: DashMap::new(), 55 | modules: DashMap::new(), 56 | next_registration_id: AtomicU64::new(1), 57 | next_node_id: AtomicU64::new(1), 58 | next_module_id: AtomicU64::new(1), 59 | } 60 | } 61 | 62 | pub fn register(&self, reg: &Register, cert_pem: &str, authentication_token: &str) { 63 | let id = self 64 | .next_registration_id 65 | .fetch_add(1, atomic::Ordering::Relaxed); 66 | let registered = Registered { 67 | node_name: reg.node_name, 68 | csr_pem: reg.csr_pem.clone(), 69 | cert_pem: cert_pem.to_owned(), 70 | authentication_token: authentication_token.to_owned(), 71 | }; 72 | self.registrations.insert(id, registered); 73 | } 74 | 75 | pub fn start_node(&self, registration_id: u64, data: NodeStart) -> (u64, String) { 76 | let id = self.next_node_id.fetch_add(1, atomic::Ordering::Relaxed); 77 | let details = NodeDetails { 78 | registration_id, 79 | status: 0, 80 | created_at: Utc::now(), 81 | stopped_at: None, 82 | node_address: data.node_address.to_string(), 83 | attributes: data.attributes, 84 | }; 85 | self.nodes.insert(id, details); 86 | (id, data.node_address.to_string()) 87 | } 88 | 89 | pub fn stop_node(&self, reg_id: u64) { 90 | if let Some(mut node) = self.nodes.get_mut(®_id) { 91 | node.status = 2; 92 | node.stopped_at = Some(Utc::now()); 93 | } 94 | } 95 | 96 | pub fn add_module(&self, bytes: Vec) -> u64 { 97 | let id = self.next_module_id.fetch_add(1, atomic::Ordering::Relaxed); 98 | self.modules.insert(id, bytes); 99 | id 100 | } 101 | } 102 | 103 | fn prepare_app() -> Result { 104 | let ca_cert_str = lunatic_distributed::distributed::server::test_root_cert(); 105 | let ca_cert = lunatic_distributed::control::cert::test_root_cert()?; 106 | let (ctrl_cert, ctrl_pk) = 107 | lunatic_distributed::control::cert::default_server_certificates(&ca_cert)?; 108 | let quic_client = 109 | lunatic_distributed::quic::new_quic_client(&ca_cert_str, &ctrl_cert, &ctrl_pk)?; 110 | let control = Arc::new(ControlServer::new(ca_cert, quic_client)); 111 | let app = Router::new() 112 | .nest("/", routes::init_routes()) 113 | .layer(Extension(control)); 114 | Ok(app) 115 | } 116 | 117 | pub async fn control_server(http_socket: SocketAddr) -> Result<()> { 118 | control_server_from_tcp(TcpListener::bind(http_socket)?).await 119 | } 120 | 121 | pub async fn control_server_from_tcp(listener: TcpListener) -> Result<()> { 122 | let app = prepare_app()?; 123 | 124 | axum::Server::from_tcp(listener)? 125 | .serve(app.into_make_service()) 126 | .await?; 127 | Ok(()) 128 | } 129 | -------------------------------------------------------------------------------- /crates/lunatic-control-submillisecond/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasi" 3 | 4 | [target.wasm32-wasi] 5 | runner = "lunatic run" 6 | -------------------------------------------------------------------------------- /crates/lunatic-control-submillisecond/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/lunatic-control-submillisecond/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-control-submillisecond" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "TBD" 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | lunatic-control = { path = "../lunatic-control" } 12 | 13 | anyhow = "1.0" 14 | base64-url = "2.0.0" 15 | bincode = "1.3" 16 | chrono = { version = "0.4.23", default-features = false, features = ["clock", "serde", "std"] } 17 | getrandom = "0.2.8" 18 | lunatic = { version = "0.13.1", features = ["sqlite"]} 19 | lunatic-log = { git = "https://github.com/lunatic-solutions/lunatic-log-rs"} 20 | serde = "1.0" 21 | serde_json = "1.0.89" 22 | submillisecond = { git = "https://github.com/lunatic-solutions/submillisecond", features = ["json", "query"] } 23 | uuid = "1.3" 24 | 25 | [workspace] 26 | -------------------------------------------------------------------------------- /crates/lunatic-control-submillisecond/README.md: -------------------------------------------------------------------------------- 1 | # Lunatic Control Server 2 | 3 | Lunatic Control Server is an HTTP server designed for managing Lunatic nodes. 4 | It is built with Submillisecond and compiles to WebAssembly to run with Lunatic. 5 | 6 | ### Running the Server Locally 7 | 8 | Before running the server locally, you need to build the Lunatic runtime by running the following command: 9 | 10 | ```bash 11 | cargo build --release 12 | ``` 13 | 14 | Next, follow the steps below to build and run the control server: 15 | 16 | 1. Navigate to the `./crates/lunatic-control-submillisecond` directory. 17 | 2. Build the control server using the following command: 18 | ```bash 19 | cargo build --target wasm32-wasi 20 | ``` 21 | 3. Finally, run the control server using the Lunatic runtime by executing the following command: 22 | ```bash 23 | ../../target/release/lunatic ./target/wasm32-wasi/debug/lunatic-control-submillisecond.wasm 24 | ``` 25 | Please note that the command above assumes that you are still in the `./crates/lunatic-control-submillisecond directory.` 26 | If you are in a different directory, you will need to adjust the relative paths accordingly. 27 | -------------------------------------------------------------------------------- /crates/lunatic-control-submillisecond/src/host.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | mod api { 4 | #[link(wasm_import_module = "lunatic::distributed")] 5 | extern "C" { 6 | pub fn test_root_cert(len_ptr: *mut u32) -> u32; 7 | pub fn default_server_certificates( 8 | cert_pem_ptr: *const u8, 9 | cert_pem_len: u32, 10 | key_pair_pem_ptr: *const u8, 11 | key_pair_pem_len: u32, 12 | len_ptr: *mut u32, 13 | ) -> u32; 14 | pub fn sign_node( 15 | cert_pem_ptr: *const u8, 16 | cert_pem_len: u32, 17 | key_pair_pem_ptr: *const u8, 18 | key_pair_pem_len: u32, 19 | csr_pem_ptr: *const u8, 20 | csr_pem_len: u32, 21 | len_ptr: *mut u32, 22 | ) -> u32; 23 | } 24 | } 25 | 26 | #[derive(Clone, Debug, Serialize, Deserialize)] 27 | pub struct CertPk { 28 | pub cert: String, 29 | pub pk: String, 30 | } 31 | 32 | pub fn test_root_cert() -> CertPk { 33 | let (cert, pk) = call_host_alloc(|len_ptr| unsafe { api::test_root_cert(len_ptr) }).unwrap(); 34 | CertPk { cert, pk } 35 | } 36 | 37 | pub fn default_server_certificates(cert_pem: &str, pk_pem: &str) -> CertPk { 38 | let (cert, pk) = call_host_alloc(|len_ptr| unsafe { 39 | api::default_server_certificates( 40 | cert_pem.as_ptr(), 41 | cert_pem.len() as u32, 42 | pk_pem.as_ptr(), 43 | pk_pem.len() as u32, 44 | len_ptr, 45 | ) 46 | }) 47 | .unwrap(); 48 | CertPk { cert, pk } 49 | } 50 | 51 | pub fn sign_node(cert_pem: &str, pk_pem: &str, csr_pem: &str) -> String { 52 | call_host_alloc(|len_ptr| unsafe { 53 | api::sign_node( 54 | cert_pem.as_ptr(), 55 | cert_pem.len() as u32, 56 | pk_pem.as_ptr(), 57 | pk_pem.len() as u32, 58 | csr_pem.as_ptr(), 59 | csr_pem.len() as u32, 60 | len_ptr, 61 | ) 62 | }) 63 | .unwrap() 64 | } 65 | 66 | fn call_host_alloc(f: impl Fn(*mut u32) -> u32) -> bincode::Result 67 | where 68 | T: for<'de> Deserialize<'de>, 69 | { 70 | let mut len = 0_u32; 71 | let len_ptr = &mut len as *mut u32; 72 | let ptr = f(len_ptr); 73 | let data_vec = unsafe { Vec::from_raw_parts(ptr as *mut u8, len as usize, len as usize) }; 74 | bincode::deserialize(&data_vec) 75 | } 76 | -------------------------------------------------------------------------------- /crates/lunatic-control-submillisecond/src/main.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod host; 3 | mod routes; 4 | mod server; 5 | 6 | use std::net::ToSocketAddrs; 7 | 8 | use api::RequestBodyLimit; 9 | use lunatic::AbstractProcess; 10 | use submillisecond::{router, Application}; 11 | 12 | use crate::routes::{add_module, get_module, list_nodes, node_started, node_stopped, register}; 13 | use crate::server::{ControlServer, ControlServerProcess}; 14 | 15 | fn main() -> anyhow::Result<()> { 16 | let root_cert = host::test_root_cert(); 17 | let ca_cert = host::default_server_certificates(&root_cert.cert, &root_cert.pk); 18 | 19 | ControlServer::link() 20 | .start_as(&ControlServerProcess, ca_cert) 21 | .unwrap(); 22 | 23 | let addrs: Vec<_> = (3030..3999_u16) 24 | .flat_map(|port| ("127.0.0.1", port).to_socket_addrs().unwrap()) 25 | .collect(); 26 | 27 | Application::new(router! { 28 | with RequestBodyLimit::new(50 * 1024 * 1024); // 50 mb 29 | 30 | POST "/" => register 31 | POST "/stopped" => node_stopped 32 | POST "/started" => node_started 33 | GET "/nodes" => list_nodes 34 | POST "/module" => add_module 35 | GET "/module/:id" => get_module 36 | }) 37 | .serve(addrs.as_slice())?; 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /crates/lunatic-control-submillisecond/src/routes.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use lunatic_control::{ 4 | api::{ 5 | ControlUrls, ModuleBytes, ModuleId, NodeStart, NodeStarted, NodesList, Register, 6 | Registration, 7 | }, 8 | NodeInfo, 9 | }; 10 | use lunatic_log::info; 11 | use submillisecond::extract::Query; 12 | 13 | use crate::{ 14 | api::{ 15 | ok, ApiError, ApiResponse, ControlServerExtractor, HostExtractor, JsonExtractor, NodeAuth, 16 | PathExtractor, 17 | }, 18 | server::{ControlServerMessages, ControlServerRequests}, 19 | }; 20 | 21 | pub fn register( 22 | ControlServerExtractor(control): ControlServerExtractor, 23 | HostExtractor(host): HostExtractor, 24 | JsonExtractor(reg): JsonExtractor, 25 | ) -> ApiResponse { 26 | info!("Registration for node name {}", reg.node_name); 27 | 28 | let cert_pem = control.sign_node(reg.csr_pem.clone()); 29 | 30 | let mut authentication_token = [0u8; 32]; 31 | getrandom::getrandom(&mut authentication_token).map_err(|err| { 32 | ApiError::log_internal_err("Error generating random token for registration", err) 33 | })?; 34 | let authentication_token = base64_url::encode(&authentication_token); 35 | 36 | ControlServerMessages::register( 37 | &control, 38 | reg.clone(), 39 | cert_pem.clone(), 40 | authentication_token.clone(), 41 | ); 42 | 43 | ok(Registration { 44 | node_name: reg.node_name, 45 | cert_pem_chain: vec![cert_pem], 46 | authentication_token, 47 | root_cert: control.root_cert(), 48 | urls: ControlUrls { 49 | api_base: format!("http://{host}/"), 50 | nodes: format!("http://{host}/nodes"), 51 | node_started: format!("http://{host}/started"), 52 | node_stopped: format!("http://{host}/stopped"), 53 | get_module: format!("http://{host}/module/{{id}}"), 54 | add_module: format!("http://{host}/module"), 55 | get_nodes: format!("http://{host}/nodes"), 56 | }, 57 | envs: Vec::new(), 58 | is_privileged: true, 59 | }) 60 | } 61 | 62 | pub fn node_stopped( 63 | node_auth: NodeAuth, 64 | ControlServerExtractor(control): ControlServerExtractor, 65 | ) -> ApiResponse<()> { 66 | info!("Node {} stopped", node_auth.node_name); 67 | 68 | control.stop_node(node_auth.registration_id as u64); 69 | 70 | ok(()) 71 | } 72 | 73 | pub fn node_started( 74 | node_auth: NodeAuth, 75 | ControlServerExtractor(control): ControlServerExtractor, 76 | JsonExtractor(data): JsonExtractor, 77 | ) -> ApiResponse { 78 | control.stop_node(node_auth.registration_id as u64); 79 | 80 | let (node_id, _node_address) = control.start_node(node_auth.registration_id as u64, data); 81 | 82 | info!("Node {} started with id {}", node_auth.node_name, node_id); 83 | 84 | // TODO spawn all modules on node 85 | 86 | ok(NodeStarted { 87 | node_id: node_id as i64, 88 | }) 89 | } 90 | 91 | pub fn list_nodes( 92 | _node_auth: NodeAuth, 93 | Query(query): Query>, 94 | ControlServerExtractor(control): ControlServerExtractor, 95 | ) -> ApiResponse { 96 | let all_nodes = control.get_nodes(); 97 | let nds: Vec<_> = all_nodes 98 | .into_values() 99 | .filter(|n| n.status < 2 && !n.node_address.is_empty()) 100 | .collect(); 101 | let nds: Vec<_> = if !query.is_empty() { 102 | nds.into_iter() 103 | .filter(|node| query.iter().all(|(k, v)| node.attributes.get(k) == Some(v))) 104 | .collect() 105 | } else { 106 | nds 107 | }; 108 | 109 | let nodes: Vec<_> = control 110 | .get_registrations() 111 | .into_iter() 112 | .filter_map(|(k, r)| { 113 | nds.iter() 114 | .find(|n| n.registration_id == k) 115 | .map(|n| NodeInfo { 116 | id: k, 117 | address: n.node_address.parse().unwrap(), 118 | name: r.node_name.to_string(), 119 | }) 120 | }) 121 | .collect(); 122 | 123 | ok(NodesList { nodes }) 124 | } 125 | 126 | pub fn add_module( 127 | body: Vec, 128 | node_auth: NodeAuth, 129 | ControlServerExtractor(control): ControlServerExtractor, 130 | ) -> ApiResponse { 131 | info!("Node {} add_module", node_auth.node_name); 132 | 133 | let module_id = control.add_module(body); 134 | ok(ModuleId { module_id }) 135 | } 136 | 137 | pub fn get_module( 138 | node_auth: NodeAuth, 139 | PathExtractor(id): PathExtractor, 140 | ControlServerExtractor(control): ControlServerExtractor, 141 | ) -> ApiResponse { 142 | info!("Node {} get_module {}", node_auth.node_name, id); 143 | 144 | let all_modules = control.get_modules(); 145 | let bytes = all_modules 146 | .into_iter() 147 | .find(|(k, _)| k == &id) 148 | .map(|(_, m)| m) 149 | .ok_or_else(|| ApiError::custom_code("error_reading_bytes"))?; 150 | 151 | ok(ModuleBytes { bytes }) 152 | } 153 | -------------------------------------------------------------------------------- /crates/lunatic-control-submillisecond/src/server.rs: -------------------------------------------------------------------------------- 1 | mod store; 2 | 3 | use std::{ 4 | collections::HashMap, 5 | ops::{Deref, DerefMut}, 6 | }; 7 | 8 | use anyhow::Result; 9 | use chrono::{DateTime, Utc}; 10 | use lunatic::{ 11 | abstract_process, 12 | ap::{Config, ProcessRef}, 13 | ProcessName, 14 | }; 15 | use lunatic_control::api::{NodeStart, Register}; 16 | use serde::{Deserialize, Serialize}; 17 | use uuid::Uuid; 18 | 19 | use crate::host::{self, CertPk}; 20 | 21 | use self::store::ControlServerStore; 22 | 23 | #[derive(ProcessName)] 24 | pub struct ControlServerProcess; 25 | 26 | #[derive(Clone, Debug)] 27 | pub struct ControlServer { 28 | ca_cert: CertPk, 29 | store: ControlServerStore, 30 | registrations: HashMap, 31 | nodes: HashMap, 32 | modules: HashMap>, 33 | next_registration_id: u64, 34 | next_node_id: u64, 35 | next_module_id: u64, 36 | } 37 | 38 | #[derive(Clone, Debug, Serialize, Deserialize)] 39 | pub struct Registered { 40 | pub node_name: Uuid, 41 | pub csr_pem: String, 42 | pub cert_pem: String, 43 | pub auth_token: String, 44 | } 45 | 46 | #[derive(Clone, Debug, Serialize, Deserialize)] 47 | pub struct NodeDetails { 48 | pub registration_id: u64, 49 | pub status: i16, 50 | pub created_at: DateTime, 51 | pub stopped_at: Option>, 52 | pub node_address: String, 53 | pub attributes: HashMap, 54 | } 55 | 56 | impl ControlServer { 57 | pub fn lookup() -> Option> { 58 | ProcessRef::lookup(&ControlServerProcess) 59 | } 60 | } 61 | 62 | #[abstract_process(visibility = pub)] 63 | impl ControlServer { 64 | #[init] 65 | fn init(_: Config, ca_cert: CertPk) -> Result { 66 | Self::init_new(ca_cert).map_err(|err| err.to_string()) 67 | } 68 | 69 | fn init_new(ca_cert: CertPk) -> anyhow::Result { 70 | let store = ControlServerStore::connect("control_server.db")?; 71 | 72 | store.init()?; 73 | 74 | let registrations = store.load_registrations()?; 75 | let nodes = store.load_nodes()?; 76 | let modules = store.load_modules()?; 77 | 78 | let next_registration_id = registrations.keys().fold(1, |max, k| max.max(k + 1)); 79 | let next_node_id = nodes.keys().fold(1, |max, k| max.max(k + 1)); 80 | let next_module_id = modules.keys().fold(1, |max, k| max.max(k + 1)); 81 | 82 | Ok(ControlServer { 83 | ca_cert, 84 | store, 85 | registrations, 86 | nodes, 87 | modules, 88 | next_registration_id, 89 | next_node_id, 90 | next_module_id, 91 | }) 92 | } 93 | 94 | #[handle_message] 95 | pub fn register(&mut self, reg: Register, cert_pem: String, auth_token: String) { 96 | let id = self.next_registration_id; 97 | self.next_registration_id += 1; 98 | let registered = Registered { 99 | node_name: reg.node_name, 100 | csr_pem: reg.csr_pem, 101 | cert_pem, 102 | auth_token, 103 | }; 104 | self.store.add_registration(id, ®istered); 105 | self.registrations.insert(id, registered); 106 | } 107 | 108 | #[handle_request] 109 | pub fn start_node(&mut self, registration_id: u64, data: NodeStart) -> (u64, String) { 110 | let id = self.next_node_id; 111 | self.next_node_id += 1; 112 | let details = NodeDetails { 113 | registration_id, 114 | status: 0, 115 | created_at: Utc::now(), 116 | stopped_at: None, 117 | node_address: data.node_address.to_string(), 118 | attributes: data.attributes, 119 | }; 120 | self.store.add_node(id, &details); 121 | self.nodes.insert(id, details); 122 | (id, data.node_address.to_string()) 123 | } 124 | 125 | #[handle_message] 126 | pub fn stop_node(&mut self, reg_id: u64) { 127 | if let Some(node) = self.nodes.get_mut(®_id) { 128 | node.status = 2; 129 | node.stopped_at = Some(Utc::now()); 130 | self.store.add_node(reg_id, node); 131 | } 132 | } 133 | 134 | #[handle_request] 135 | pub fn add_module(&mut self, bytes: Vec) -> u64 { 136 | let id = self.next_module_id; 137 | self.next_module_id += 1; 138 | self.store.add_module(id, bytes.clone()); 139 | self.modules.insert(id, bytes); 140 | id 141 | } 142 | 143 | #[handle_request] 144 | pub fn get_nodes(&self) -> HashMap { 145 | self.nodes.clone() 146 | } 147 | 148 | #[handle_request] 149 | pub fn get_registrations(&self) -> HashMap { 150 | self.registrations.clone() 151 | } 152 | 153 | #[handle_request] 154 | pub fn get_modules(&self) -> HashMap> { 155 | self.modules.clone() 156 | } 157 | 158 | #[handle_request] 159 | pub fn root_cert(&self) -> String { 160 | self.ca_cert.cert.clone() 161 | } 162 | 163 | #[handle_request] 164 | pub fn sign_node(&self, csr_pem: String) -> String { 165 | host::sign_node(&self.ca_cert.cert, &self.ca_cert.pk, &csr_pem) 166 | } 167 | } 168 | 169 | #[derive(Clone, Debug, PartialEq, Eq)] 170 | pub struct BincodeJsonValue(pub serde_json::Value); 171 | 172 | impl From for BincodeJsonValue { 173 | fn from(value: serde_json::Value) -> Self { 174 | BincodeJsonValue(value) 175 | } 176 | } 177 | 178 | impl From for serde_json::Value { 179 | fn from(value: BincodeJsonValue) -> Self { 180 | value.0 181 | } 182 | } 183 | 184 | impl Deref for BincodeJsonValue { 185 | type Target = serde_json::Value; 186 | 187 | fn deref(&self) -> &Self::Target { 188 | &self.0 189 | } 190 | } 191 | 192 | impl DerefMut for BincodeJsonValue { 193 | fn deref_mut(&mut self) -> &mut Self::Target { 194 | &mut self.0 195 | } 196 | } 197 | 198 | impl Serialize for BincodeJsonValue { 199 | fn serialize(&self, serializer: S) -> std::result::Result 200 | where 201 | S: serde::Serializer, 202 | { 203 | let bytes = serde_json::to_vec(&self.0) 204 | .map_err(|err| ::custom(err.to_string()))?; 205 | 206 | serializer.serialize_bytes(&bytes) 207 | } 208 | } 209 | 210 | impl<'de> Deserialize<'de> for BincodeJsonValue { 211 | fn deserialize(deserializer: D) -> std::result::Result 212 | where 213 | D: serde::Deserializer<'de>, 214 | { 215 | struct BincodeJsonValueVisitor; 216 | 217 | impl<'de> serde::de::Visitor<'de> for BincodeJsonValueVisitor { 218 | type Value = BincodeJsonValue; 219 | 220 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 221 | write!(formatter, "a byte slice of json") 222 | } 223 | 224 | fn visit_bytes(self, v: &[u8]) -> std::result::Result 225 | where 226 | E: serde::de::Error, 227 | { 228 | serde_json::from_slice(v) 229 | .map(BincodeJsonValue) 230 | .map_err(|err| E::custom(err.to_string())) 231 | } 232 | } 233 | 234 | deserializer.deserialize_bytes(BincodeJsonValueVisitor) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /crates/lunatic-control/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-control" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "TBD" 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | serde = { workspace = true, features = ["derive"] } 12 | uuid = { version = "1.0", features = ["serde", "v4"] } 13 | -------------------------------------------------------------------------------- /crates/lunatic-control/src/api.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, net::SocketAddr}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::NodeInfo; 6 | 7 | #[derive(Clone, Debug, Serialize, Deserialize)] 8 | pub struct Register { 9 | pub node_name: uuid::Uuid, 10 | pub csr_pem: String, 11 | } 12 | 13 | #[derive(Clone, Debug, Serialize, Deserialize)] 14 | pub struct Registration { 15 | pub node_name: uuid::Uuid, 16 | pub cert_pem_chain: Vec, 17 | pub authentication_token: String, 18 | pub root_cert: String, 19 | pub urls: ControlUrls, 20 | pub envs: Vec, 21 | pub is_privileged: bool, 22 | } 23 | 24 | #[derive(Clone, Debug, Serialize, Deserialize)] 25 | pub struct ControlUrls { 26 | pub api_base: String, 27 | pub nodes: String, 28 | pub node_started: String, 29 | pub node_stopped: String, 30 | pub get_module: String, 31 | pub add_module: String, 32 | pub get_nodes: String, 33 | } 34 | 35 | #[derive(Clone, Debug, Serialize, Deserialize)] 36 | pub struct NodeStart { 37 | pub node_address: SocketAddr, 38 | pub attributes: HashMap, 39 | } 40 | 41 | #[derive(Clone, Debug, Serialize, Deserialize)] 42 | pub struct NodeStarted { 43 | // TODO u64 ids should be JSON string but parsed into u64? 44 | pub node_id: i64, 45 | } 46 | 47 | #[derive(Clone, Debug, Serialize, Deserialize)] 48 | pub struct NodesList { 49 | pub nodes: Vec, 50 | } 51 | 52 | #[derive(Clone, Debug, Serialize, Deserialize)] 53 | pub struct ModuleBytes { 54 | pub bytes: Vec, 55 | } 56 | 57 | #[derive(Clone, Debug, Serialize, Deserialize)] 58 | pub struct AddModule { 59 | pub bytes: Vec, 60 | } 61 | 62 | #[derive(Clone, Debug, Serialize, Deserialize)] 63 | pub struct ModuleId { 64 | pub module_id: u64, 65 | } 66 | -------------------------------------------------------------------------------- /crates/lunatic-control/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | 3 | use std::net::SocketAddr; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct NodeInfo { 9 | pub id: u64, 10 | pub address: SocketAddr, 11 | pub name: String, 12 | } 13 | -------------------------------------------------------------------------------- /crates/lunatic-distributed-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-distributed-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "A simple control server implementation" 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | lunatic-common-api = { workspace = true } 12 | lunatic-distributed = { workspace = true } 13 | lunatic-error-api = { workspace = true } 14 | lunatic-process = { workspace = true } 15 | lunatic-process-api = { workspace = true } 16 | 17 | asn1-rs = "0.5.2" 18 | anyhow = { workspace = true } 19 | bincode = { workspace = true } 20 | rcgen = { version = "0.10", features = ["pem", "x509-parser"] } 21 | rmp-serde = "1.1.1" 22 | log = { workspace = true } 23 | serde_json = "1.0.89" 24 | tokio = { workspace = true, features = ["time"] } 25 | wasmtime = { workspace = true } 26 | -------------------------------------------------------------------------------- /crates/lunatic-distributed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-distributed" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Node to node communication" 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | lunatic-control = { workspace = true } 12 | lunatic-process = { workspace = true } 13 | 14 | anyhow = { workspace = true } 15 | async_cell = "0.2.1" 16 | rmp-serde = "1.1.1" 17 | bytes = "1" 18 | dashmap = { workspace = true } 19 | log = { workspace = true } 20 | quinn = { version = "0.10.2" } 21 | rcgen = { version = "0.10", features = ["pem", "x509-parser"] } 22 | reqwest = { workspace = true, features = ["json"] } 23 | rustls = { version = "0.21.6" } 24 | rustls-pemfile = { workspace = true } 25 | serde = { workspace = true, features = ["derive"] } 26 | serde_json = "1.0.89" 27 | tokio = { workspace = true, features = ["io-util", "rt", "sync", "time"] } 28 | uuid = { version = "1.0", features = ["serde", "v4"] } 29 | wasmtime = { workspace = true } 30 | x509-parser = "0.14.0" 31 | -------------------------------------------------------------------------------- /crates/lunatic-distributed/src/control/cert.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use rcgen::*; 5 | 6 | pub static TEST_ROOT_CERT: &str = r#""" 7 | -----BEGIN CERTIFICATE----- 8 | MIIBnDCCAUGgAwIBAgIIR5Hk+O5RdOgwCgYIKoZIzj0EAwIwKTEQMA4GA1UEAwwH 9 | Um9vdCBDQTEVMBMGA1UECgwMTHVuYXRpYyBJbmMuMCAXDTc1MDEwMTAwMDAwMFoY 10 | DzQwOTYwMTAxMDAwMDAwWjApMRAwDgYDVQQDDAdSb290IENBMRUwEwYDVQQKDAxM 11 | dW5hdGljIEluYy4wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARlVNxYAwsmmFNc 12 | 2EMBbZZVwL8GBtnnu8IROdDd68ixc0VBjfrV0zAM344lKJcs9slsMTEofoYvMCpI 13 | BhnSGyAFo1EwTzAdBgNVHREEFjAUghJyb290Lmx1bmF0aWMuY2xvdWQwHQYDVR0O 14 | BBYEFOh0Ue745JFH76xErjqkW2/SbHhAMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZI 15 | zj0EAwIDSQAwRgIhAJKPv4XUZ9ej+CVgsJ+9x/CmJEcnebyWh2KntJri97nxAiEA 16 | /KvaQE6GtYZPGFv/WYM3YEmTQ7hoOvaaAuvD27cHkaw= 17 | -----END CERTIFICATE----- 18 | """#; 19 | 20 | pub static CTRL_SERVER_NAME: &str = "ctrl.lunatic.cloud"; 21 | 22 | static TEST_ROOT_KEYS: &str = r#""" 23 | -----BEGIN PRIVATE KEY----- 24 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9ferf0du4h975Jhu 25 | boMyGfdI+xwp7ewOulGvpTcvdpehRANCAARlVNxYAwsmmFNc2EMBbZZVwL8GBtnn 26 | u8IROdDd68ixc0VBjfrV0zAM344lKJcs9slsMTEofoYvMCpIBhnSGyAF 27 | -----END PRIVATE KEY-----"""#; 28 | 29 | pub fn test_root_cert() -> Result { 30 | let key_pair = KeyPair::from_pem(TEST_ROOT_KEYS)?; 31 | let root_params = CertificateParams::from_ca_cert_pem(TEST_ROOT_CERT, key_pair)?; 32 | let root_cert = Certificate::from_params(root_params)?; 33 | Ok(root_cert) 34 | } 35 | 36 | pub fn root_cert(ca_cert: &str, ca_keys: &str) -> Result { 37 | let ca_cert_pem = std::fs::read(Path::new(ca_cert))?; 38 | let ca_keys_pem = std::fs::read(Path::new(ca_keys))?; 39 | let key_pair = KeyPair::from_pem(std::str::from_utf8(&ca_keys_pem)?)?; 40 | let root_params = 41 | CertificateParams::from_ca_cert_pem(std::str::from_utf8(&ca_cert_pem)?, key_pair)?; 42 | let root_cert = Certificate::from_params(root_params)?; 43 | Ok(root_cert) 44 | } 45 | 46 | fn ctrl_cert() -> Result { 47 | let mut ctrl_params = CertificateParams::new(vec![CTRL_SERVER_NAME.into()]); 48 | ctrl_params 49 | .distinguished_name 50 | .push(DnType::OrganizationName, "Lunatic Inc."); 51 | ctrl_params 52 | .distinguished_name 53 | .push(DnType::CommonName, "Control CA"); 54 | Ok(Certificate::from_params(ctrl_params)?) 55 | } 56 | 57 | pub fn default_server_certificates(root_cert: &Certificate) -> Result<(String, String)> { 58 | let ctrl_cert = ctrl_cert()?; 59 | let cert_pem = ctrl_cert.serialize_pem_with_signer(root_cert)?; 60 | let key_pem = ctrl_cert.serialize_private_key_pem(); 61 | Ok((cert_pem, key_pem)) 62 | } 63 | -------------------------------------------------------------------------------- /crates/lunatic-distributed/src/control/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | //pub mod server; 3 | pub mod cert; 4 | 5 | pub use client::Client; 6 | -------------------------------------------------------------------------------- /crates/lunatic-distributed/src/control/server.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::SocketAddr, 3 | path::Path, 4 | sync::{ 5 | atomic::{self, AtomicU64}, 6 | Arc, 7 | }, 8 | }; 9 | 10 | use anyhow::Result; 11 | use bytes::Bytes; 12 | use dashmap::DashMap; 13 | 14 | use super::parser::Parser; 15 | 16 | -------------------------------------------------------------------------------- /crates/lunatic-distributed/src/distributed/message.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, Serialize, Deserialize)] 5 | pub enum Request { 6 | Spawn(Spawn), 7 | Message { 8 | node_id: u64, 9 | environment_id: u64, 10 | process_id: u64, 11 | tag: Option, 12 | data: Vec, 13 | }, 14 | Response(Response), 15 | } 16 | 17 | impl Request { 18 | pub fn kind(&self) -> &'static str { 19 | match self { 20 | Request::Spawn(_) => "Spawn", 21 | Request::Message { .. } => "Message", 22 | Request::Response(_) => "Response", 23 | } 24 | } 25 | } 26 | 27 | #[derive(Clone, Debug, Serialize, Deserialize)] 28 | pub struct Spawn { 29 | pub response_node_id: u64, 30 | pub environment_id: u64, 31 | pub module_id: u64, 32 | pub function: String, 33 | pub params: Vec, 34 | pub config: Vec, 35 | } 36 | 37 | #[derive(Clone, Debug, Serialize, Deserialize)] 38 | pub enum ClientError { 39 | Unexpected(String), 40 | Connection(String), 41 | NodeNotFound, 42 | ModuleNotFound, 43 | ProcessNotFound, 44 | } 45 | 46 | impl Default for ClientError { 47 | fn default() -> Self { 48 | Self::Unexpected("Unexpected error.".to_string()) 49 | } 50 | } 51 | 52 | #[derive(Clone, Debug, Serialize, Deserialize)] 53 | pub struct Response { 54 | pub message_id: u64, 55 | pub content: ResponseContent, 56 | } 57 | 58 | #[derive(Clone, Debug, Serialize, Deserialize)] 59 | pub enum ResponseContent { 60 | Spawned(u64), 61 | Sent, 62 | Linked, 63 | Error(ClientError), 64 | } 65 | 66 | impl Response { 67 | pub fn kind(&self) -> &'static str { 68 | match self.content { 69 | ResponseContent::Spawned(_) => "Spawned", 70 | ResponseContent::Sent => "Sent", 71 | ResponseContent::Linked => "Linked", 72 | ResponseContent::Error(_) => "Error", 73 | } 74 | } 75 | } 76 | 77 | #[derive(Clone, Debug, Serialize, Deserialize)] 78 | pub enum Val { 79 | I32(i32), 80 | I64(i64), 81 | V128(u128), 82 | } 83 | 84 | #[allow(clippy::from_over_into)] 85 | impl Into for Val { 86 | fn into(self) -> wasmtime::Val { 87 | match self { 88 | Val::I32(v) => wasmtime::Val::I32(v), 89 | Val::I64(v) => wasmtime::Val::I64(v), 90 | Val::V128(v) => wasmtime::Val::V128(v), 91 | } 92 | } 93 | } 94 | 95 | pub fn pack_response(msg_id: u64, resp: Response) -> [Bytes; 2] { 96 | let data = rmp_serde::to_vec(&(msg_id, resp)).unwrap(); 97 | let size = (data.len() as u32).to_le_bytes(); 98 | let size: Bytes = Bytes::copy_from_slice(&size[..]); 99 | let bytes: Bytes = data.into(); 100 | [size, bytes] 101 | } 102 | -------------------------------------------------------------------------------- /crates/lunatic-distributed/src/distributed/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod message; 3 | pub mod server; 4 | 5 | pub use client::Client; 6 | -------------------------------------------------------------------------------- /crates/lunatic-distributed/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod congestion; 2 | pub mod control; 3 | pub mod distributed; 4 | pub mod quic; 5 | 6 | use anyhow::Result; 7 | use lunatic_process::{ 8 | env::Environment, 9 | runtimes::wasmtime::{WasmtimeCompiledModule, WasmtimeRuntime}, 10 | state::ProcessState, 11 | }; 12 | use serde::{Deserialize, Serialize}; 13 | use std::sync::Arc; 14 | 15 | pub trait DistributedCtx: ProcessState + Sized { 16 | fn new_dist_state( 17 | environment: Arc, 18 | distributed: DistributedProcessState, 19 | runtime: WasmtimeRuntime, 20 | module: Arc>, 21 | config: Arc, 22 | ) -> Result; 23 | fn distributed(&self) -> Result<&DistributedProcessState>; 24 | fn distributed_mut(&mut self) -> Result<&mut DistributedProcessState>; 25 | fn module_id(&self) -> u64; 26 | fn environment_id(&self) -> u64; 27 | fn can_spawn(&self) -> bool; 28 | } 29 | 30 | #[derive(Clone)] 31 | pub struct DistributedProcessState { 32 | node_id: u64, 33 | pub control: control::Client, 34 | pub node_client: distributed::Client, 35 | } 36 | 37 | impl DistributedProcessState { 38 | pub async fn new( 39 | node_id: u64, 40 | control_client: control::Client, 41 | node_client: distributed::Client, 42 | ) -> Result { 43 | Ok(Self { 44 | node_id, 45 | control: control_client, 46 | node_client, 47 | }) 48 | } 49 | 50 | pub fn node_id(&self) -> u64 { 51 | self.node_id 52 | } 53 | } 54 | 55 | pub const SUBJECT_DIR_ATTRS: [u64; 4] = [2, 5, 29, 9]; 56 | 57 | #[derive(Debug, Serialize, Deserialize)] 58 | pub struct CertAttrs { 59 | pub allowed_envs: Vec, 60 | pub is_privileged: bool, 61 | } 62 | -------------------------------------------------------------------------------- /crates/lunatic-distributed/src/quic/mod.rs: -------------------------------------------------------------------------------- 1 | mod quin; 2 | pub use quin::*; 3 | -------------------------------------------------------------------------------- /crates/lunatic-error-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-error-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions that make dealing with Anyhow errors simpler." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | hash-map-id = { workspace = true } 12 | lunatic-common-api = { workspace = true } 13 | 14 | anyhow = { workspace = true } 15 | wasmtime = { workspace = true } 16 | -------------------------------------------------------------------------------- /crates/lunatic-error-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use hash_map_id::HashMapId; 3 | use lunatic_common_api::{get_memory, IntoTrap}; 4 | use wasmtime::{Caller, Linker}; 5 | 6 | pub type ErrorResource = HashMapId; 7 | 8 | pub trait ErrorCtx { 9 | fn error_resources(&self) -> &ErrorResource; 10 | fn error_resources_mut(&mut self) -> &mut ErrorResource; 11 | } 12 | 13 | // Register the error APIs to the linker 14 | pub fn register(linker: &mut Linker) -> Result<()> { 15 | linker.func_wrap("lunatic::error", "string_size", string_size)?; 16 | linker.func_wrap("lunatic::error", "to_string", to_string)?; 17 | linker.func_wrap("lunatic::error", "drop", drop)?; 18 | Ok(()) 19 | } 20 | 21 | // Returns the size of the string representation of the error. 22 | // 23 | // Traps: 24 | // * If the error ID doesn't exist. 25 | fn string_size(caller: Caller, error_id: u64) -> Result { 26 | let error = caller 27 | .data() 28 | .error_resources() 29 | .get(error_id) 30 | .or_trap("lunatic::error::string_size")?; 31 | Ok(error.to_string().len() as u32) 32 | } 33 | 34 | // Writes the string representation of the error to the guest memory. 35 | // `lunatic::error::string_size` can be used to get the string size. 36 | // 37 | // Traps: 38 | // * If the error ID doesn't exist. 39 | // * If any memory outside the guest heap space is referenced. 40 | fn to_string(mut caller: Caller, error_id: u64, error_str_ptr: u32) -> Result<()> { 41 | let error = caller 42 | .data() 43 | .error_resources() 44 | .get(error_id) 45 | .or_trap("lunatic::error::string_size")?; 46 | let error_str = error.to_string(); 47 | let memory = get_memory(&mut caller)?; 48 | memory 49 | .write(&mut caller, error_str_ptr as usize, error_str.as_ref()) 50 | .or_trap("lunatic::error::string_size")?; 51 | Ok(()) 52 | } 53 | 54 | // Drops the error resource. 55 | // 56 | // Traps: 57 | // * If the error ID doesn't exist. 58 | fn drop(mut caller: Caller, error_id: u64) -> Result<()> { 59 | caller 60 | .data_mut() 61 | .error_resources_mut() 62 | .remove(error_id) 63 | .or_trap("lunatic::error::drop")?; 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /crates/lunatic-messaging-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-messaging-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions for message sending." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-messaging-api" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | lunatic-common-api = { workspace = true } 12 | lunatic-networking-api = { workspace = true } 13 | lunatic-process = { workspace = true } 14 | lunatic-process-api = { workspace = true } 15 | 16 | anyhow = { workspace = true } 17 | tokio = { workspace = true, features = ["time"] } 18 | wasmtime = { workspace = true } 19 | -------------------------------------------------------------------------------- /crates/lunatic-metrics-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-metrics-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions for metrics" 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-metrics" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | anyhow = { workspace = true } 12 | wasmtime = { workspace = true } 13 | metrics = { workspace = true } 14 | lunatic-common-api = { workspace = true } 15 | log = { workspace = true } 16 | -------------------------------------------------------------------------------- /crates/lunatic-metrics-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use lunatic_common_api::{get_memory, IntoTrap}; 3 | use metrics::{counter, decrement_gauge, gauge, histogram, increment_counter, increment_gauge}; 4 | use wasmtime::{Caller, Linker}; 5 | 6 | /// Links the [Metrics](https://crates.io/crates/metrics) APIs 7 | pub fn register(linker: &mut Linker) -> anyhow::Result<()> { 8 | linker.func_wrap("lunatic::metrics", "counter", counter)?; 9 | linker.func_wrap("lunatic::metrics", "increment_counter", increment_counter)?; 10 | linker.func_wrap("lunatic::metrics", "gauge", gauge)?; 11 | linker.func_wrap("lunatic::metrics", "increment_gauge", increment_gauge)?; 12 | linker.func_wrap("lunatic::metrics", "decrement_gauge", decrement_gauge)?; 13 | linker.func_wrap("lunatic::metrics", "histogram", histogram)?; 14 | Ok(()) 15 | } 16 | 17 | fn get_string_arg( 18 | caller: &mut Caller, 19 | name_str_ptr: u32, 20 | name_str_len: u32, 21 | func_name: &str, 22 | ) -> Result { 23 | let memory = get_memory(caller)?; 24 | let memory_slice = memory.data(caller); 25 | let name = memory_slice 26 | .get(name_str_ptr as usize..(name_str_ptr + name_str_len) as usize) 27 | .or_trap(func_name)?; 28 | let name = String::from_utf8(name.to_vec()).or_trap(func_name)?; 29 | Ok(name) 30 | } 31 | 32 | /// Sets a counter. 33 | /// 34 | /// Traps: 35 | /// * If the name is not a valid utf8 string. 36 | /// * If any memory outside the guest heap space is referenced. 37 | fn counter( 38 | mut caller: Caller<'_, T>, 39 | name_str_ptr: u32, 40 | name_str_len: u32, 41 | value: u64, 42 | ) -> Result<()> { 43 | let name = get_string_arg( 44 | &mut caller, 45 | name_str_ptr, 46 | name_str_len, 47 | "lunatic::metrics::counter", 48 | )?; 49 | 50 | counter!(name, value); 51 | Ok(()) 52 | } 53 | 54 | /// Increments a counter. 55 | /// 56 | /// Traps: 57 | /// * If the name is not a valid utf8 string. 58 | /// * If any memory outside the guest heap space is referenced. 59 | fn increment_counter( 60 | mut caller: Caller<'_, T>, 61 | name_str_ptr: u32, 62 | name_str_len: u32, 63 | ) -> Result<()> { 64 | let name = get_string_arg( 65 | &mut caller, 66 | name_str_ptr, 67 | name_str_len, 68 | "lunatic::metrics::increment_counter", 69 | )?; 70 | 71 | increment_counter!(name); 72 | Ok(()) 73 | } 74 | 75 | /// Sets a gauge. 76 | /// 77 | /// Traps: 78 | /// * If the name is not a valid utf8 string. 79 | /// * If any memory outside the guest heap space is referenced. 80 | fn gauge( 81 | mut caller: Caller<'_, T>, 82 | name_str_ptr: u32, 83 | name_str_len: u32, 84 | value: f64, 85 | ) -> Result<()> { 86 | let name = get_string_arg( 87 | &mut caller, 88 | name_str_ptr, 89 | name_str_len, 90 | "lunatic::metrics::gauge", 91 | )?; 92 | 93 | gauge!(name, value); 94 | Ok(()) 95 | } 96 | 97 | /// Increments a gauge. 98 | /// 99 | /// Traps: 100 | /// * If the name is not a valid utf8 string. 101 | /// * If any memory outside the guest heap space is referenced. 102 | fn increment_gauge( 103 | mut caller: Caller<'_, T>, 104 | name_str_ptr: u32, 105 | name_str_len: u32, 106 | value: f64, 107 | ) -> Result<()> { 108 | let name = get_string_arg( 109 | &mut caller, 110 | name_str_ptr, 111 | name_str_len, 112 | "lunatic::metrics::increment_gauge", 113 | )?; 114 | 115 | increment_gauge!(name, value); 116 | Ok(()) 117 | } 118 | 119 | /// Decrements a gauge. 120 | /// 121 | /// Traps: 122 | /// * If the name is not a valid utf8 string. 123 | /// * If any memory outside the guest heap space is referenced. 124 | fn decrement_gauge( 125 | mut caller: Caller<'_, T>, 126 | name_str_ptr: u32, 127 | name_str_len: u32, 128 | value: f64, 129 | ) -> Result<()> { 130 | let name = get_string_arg( 131 | &mut caller, 132 | name_str_ptr, 133 | name_str_len, 134 | "lunatic::metrics::decrement_gauge", 135 | )?; 136 | 137 | decrement_gauge!(name, value); 138 | Ok(()) 139 | } 140 | 141 | /// Sets a histogram. 142 | /// 143 | /// Traps: 144 | /// * If the name is not a valid utf8 string. 145 | /// * If any memory outside the guest heap space is referenced. 146 | fn histogram( 147 | mut caller: Caller<'_, T>, 148 | name_str_ptr: u32, 149 | name_str_len: u32, 150 | value: f64, 151 | ) -> Result<()> { 152 | let name = get_string_arg( 153 | &mut caller, 154 | name_str_ptr, 155 | name_str_len, 156 | "lunatic::metrics::histogram", 157 | )?; 158 | 159 | histogram!(name, value); 160 | Ok(()) 161 | } 162 | -------------------------------------------------------------------------------- /crates/lunatic-networking-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-networking-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions for tcp and udp networking." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-networking-api" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | hash-map-id = { workspace = true } 12 | lunatic-common-api = { workspace = true } 13 | lunatic-error-api = { workspace = true } 14 | 15 | anyhow = { workspace = true } 16 | rustls-pemfile = { workspace = true } 17 | tokio = { workspace = true, features = ["io-util", "net", "sync", "time"] } 18 | tokio-rustls = "0.24.1" 19 | wasmtime = { workspace = true } 20 | webpki-roots = "0.25.2" 21 | rustls-webpki = "0.101.4" 22 | -------------------------------------------------------------------------------- /crates/lunatic-networking-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod dns; 2 | mod tcp; 3 | mod tls_tcp; 4 | mod udp; 5 | 6 | use std::convert::TryInto; 7 | use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; 8 | use std::sync::Arc; 9 | use std::time::Duration; 10 | 11 | use anyhow::Result; 12 | use hash_map_id::HashMapId; 13 | use lunatic_error_api::ErrorCtx; 14 | use tokio::io::{split, ReadHalf, WriteHalf}; 15 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 16 | use tokio::sync::Mutex; 17 | 18 | use anyhow::anyhow; 19 | use tokio::net::{TcpListener, TcpStream, UdpSocket}; 20 | use tokio_rustls::rustls::{Certificate, PrivateKey}; 21 | use tokio_rustls::TlsStream; 22 | use wasmtime::Memory; 23 | use wasmtime::{Caller, Linker}; 24 | 25 | use lunatic_common_api::IntoTrap; 26 | 27 | pub use dns::DnsIterator; 28 | 29 | pub struct TcpConnection { 30 | pub reader: Mutex, 31 | pub writer: Mutex, 32 | pub read_timeout: Mutex>, 33 | pub write_timeout: Mutex>, 34 | pub peek_timeout: Mutex>, 35 | } 36 | 37 | /// This encapsulates the TCP-level connection, some connection 38 | /// state, and the underlying TLS-level session. 39 | pub struct TlsConnection { 40 | pub reader: Mutex>>, 41 | pub writer: Mutex>>, 42 | pub closing: bool, 43 | pub clean_closure: bool, 44 | pub read_timeout: Mutex>, 45 | pub write_timeout: Mutex>, 46 | pub peek_timeout: Mutex>, 47 | } 48 | 49 | pub struct TlsListener { 50 | pub listener: TcpListener, 51 | pub certs: Certificate, 52 | pub keys: PrivateKey, 53 | } 54 | 55 | impl TlsConnection { 56 | pub fn new(sock: TlsStream) -> TlsConnection { 57 | let (read_half, write_half) = split(sock); 58 | TlsConnection { 59 | reader: Mutex::new(read_half), 60 | writer: Mutex::new(write_half), 61 | closing: false, 62 | clean_closure: false, 63 | read_timeout: Mutex::new(None), 64 | write_timeout: Mutex::new(None), 65 | peek_timeout: Mutex::new(None), 66 | } 67 | } 68 | } 69 | 70 | impl TcpConnection { 71 | pub fn new(stream: TcpStream) -> Self { 72 | let (read_half, write_half) = stream.into_split(); 73 | TcpConnection { 74 | reader: Mutex::new(read_half), 75 | writer: Mutex::new(write_half), 76 | read_timeout: Mutex::new(None), 77 | write_timeout: Mutex::new(None), 78 | peek_timeout: Mutex::new(None), 79 | } 80 | } 81 | } 82 | 83 | pub type TcpListenerResources = HashMapId; 84 | pub type TlsListenerResources = HashMapId; 85 | pub type TcpStreamResources = HashMapId>; 86 | pub type TlsStreamResources = HashMapId>; 87 | pub type UdpResources = HashMapId>; 88 | pub type DnsResources = HashMapId; 89 | 90 | pub trait NetworkingCtx { 91 | fn tcp_listener_resources(&self) -> &TcpListenerResources; 92 | fn tcp_listener_resources_mut(&mut self) -> &mut TcpListenerResources; 93 | fn tcp_stream_resources(&self) -> &TcpStreamResources; 94 | fn tcp_stream_resources_mut(&mut self) -> &mut TcpStreamResources; 95 | fn tls_listener_resources(&self) -> &TlsListenerResources; 96 | fn tls_listener_resources_mut(&mut self) -> &mut TlsListenerResources; 97 | fn tls_stream_resources(&self) -> &TlsStreamResources; 98 | fn tls_stream_resources_mut(&mut self) -> &mut TlsStreamResources; 99 | fn udp_resources(&self) -> &UdpResources; 100 | fn udp_resources_mut(&mut self) -> &mut UdpResources; 101 | fn dns_resources(&self) -> &DnsResources; 102 | fn dns_resources_mut(&mut self) -> &mut DnsResources; 103 | } 104 | 105 | // Register the networking APIs to the linker 106 | pub fn register( 107 | linker: &mut Linker, 108 | ) -> Result<()> { 109 | dns::register(linker)?; 110 | tcp::register(linker)?; 111 | tls_tcp::register(linker)?; 112 | udp::register(linker)?; 113 | Ok(()) 114 | } 115 | 116 | fn socket_address( 117 | caller: &Caller, 118 | memory: &Memory, 119 | addr_type: u32, 120 | addr_u8_ptr: u32, 121 | port: u32, 122 | flow_info: u32, 123 | scope_id: u32, 124 | ) -> Result { 125 | Ok(match addr_type { 126 | 4 => { 127 | let ip = memory 128 | .data(caller) 129 | .get(addr_u8_ptr as usize..(addr_u8_ptr + 4) as usize) 130 | .or_trap("lunatic::network::socket_address*")?; 131 | let addr = >::from(ip.try_into().expect("exactly 4 bytes")); 132 | SocketAddrV4::new(addr, port as u16).into() 133 | } 134 | 6 => { 135 | let ip = memory 136 | .data(caller) 137 | .get(addr_u8_ptr as usize..(addr_u8_ptr + 16) as usize) 138 | .or_trap("lunatic::network::socket_address*")?; 139 | let addr = >::from(ip.try_into().expect("exactly 16 bytes")); 140 | SocketAddrV6::new(addr, port as u16, flow_info, scope_id).into() 141 | } 142 | _ => return Err(anyhow!("Unsupported address type in socket_address*")), 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /crates/lunatic-process-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-process-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions for working with processes." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-process-api" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [features] 11 | metrics = ["dep:metrics"] 12 | 13 | [dependencies] 14 | hash-map-id = { workspace = true } 15 | lunatic-common-api = { workspace = true } 16 | lunatic-error-api = { workspace = true } 17 | lunatic-process = { workspace = true } 18 | lunatic-wasi-api = { workspace = true } 19 | lunatic-distributed = { workspace = true } 20 | 21 | anyhow = { workspace = true } 22 | metrics = { workspace = true, optional = true } 23 | tokio = { workspace = true, features = ["time"] } 24 | wasmtime = { workspace = true } 25 | -------------------------------------------------------------------------------- /crates/lunatic-process/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-process" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic's core process, mailbox and message abstraction'" 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-process" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [features] 11 | metrics = ["dep:metrics"] 12 | 13 | # Disabled by default as it will usually lead to giant metrics exports 14 | detailed_metrics = ["metrics"] 15 | 16 | [dependencies] 17 | hash-map-id = { workspace = true } 18 | lunatic-networking-api = { workspace = true } 19 | 20 | async-trait = "0.1.58" 21 | anyhow = { workspace = true } 22 | dashmap = { workspace = true } 23 | log = { workspace = true } 24 | metrics = { workspace = true, optional = true } 25 | serde = { workspace = true } 26 | smallvec = "1.10" 27 | tokio = { workspace = true, features = [ 28 | "macros", 29 | "rt-multi-thread", 30 | "sync", 31 | "net", 32 | ] } 33 | wasmtime = { workspace = true } 34 | wasmtime-wasi = { workspace = true } 35 | -------------------------------------------------------------------------------- /crates/lunatic-process/src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{de::DeserializeOwned, Serialize}; 2 | 3 | // One unit of fuel represents around 100k instructions. 4 | pub const UNIT_OF_COMPUTE_IN_INSTRUCTIONS: u64 = 100_000; 5 | 6 | /// Common process configuration. 7 | /// 8 | /// Each process in lunatic can have specific limits and permissions. These properties are set 9 | /// through a process configuration that is used when a process is spawned. Once the process is 10 | /// spawned the configuration can't be changed anymore. The process configuration heavily depends 11 | /// on the [`ProcessState`](crate::state::ProcessState) that defines host functions available to 12 | /// the process. This host functions are the ones that consider specific configuration while 13 | /// performing operations. 14 | /// 15 | /// However, two properties of a process are enforced by the runtime (maximum memory and maximum 16 | /// fuel usage). This two properties need to be part of every configuration. 17 | /// 18 | /// `ProcessConfig` must be serializable in case it is used to spawn processes on other nodes. 19 | pub trait ProcessConfig: Clone + Serialize + DeserializeOwned { 20 | fn set_max_fuel(&mut self, max_fuel: Option); 21 | fn get_max_fuel(&self) -> Option; 22 | fn set_max_memory(&mut self, max_memory: usize); 23 | fn get_max_memory(&self) -> usize; 24 | } 25 | -------------------------------------------------------------------------------- /crates/lunatic-process/src/env.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_trait::async_trait; 3 | use dashmap::DashMap; 4 | use std::sync::{ 5 | atomic::{AtomicU64, Ordering}, 6 | Arc, 7 | }; 8 | 9 | use crate::{Process, Signal}; 10 | 11 | #[async_trait] 12 | pub trait Environment: Send + Sync { 13 | fn id(&self) -> u64; 14 | fn get_next_process_id(&self) -> u64; 15 | fn get_process(&self, id: u64) -> Option>; 16 | fn add_process(&self, id: u64, proc: Arc); 17 | fn remove_process(&self, id: u64); 18 | fn process_count(&self) -> usize; 19 | async fn can_spawn_next_process(&self) -> Result>; 20 | fn send(&self, id: u64, signal: Signal); 21 | } 22 | 23 | #[async_trait] 24 | pub trait Environments: Send + Sync { 25 | type Env: Environment; 26 | 27 | async fn create(&self, id: u64) -> Result>; 28 | async fn get(&self, id: u64) -> Option>; 29 | } 30 | 31 | #[derive(Clone)] 32 | pub struct LunaticEnvironment { 33 | environment_id: u64, 34 | next_process_id: Arc, 35 | processes: Arc>>, 36 | } 37 | 38 | impl LunaticEnvironment { 39 | pub fn new(id: u64) -> Self { 40 | Self { 41 | environment_id: id, 42 | processes: Arc::new(DashMap::new()), 43 | next_process_id: Arc::new(AtomicU64::new(1)), 44 | } 45 | } 46 | } 47 | 48 | #[async_trait] 49 | impl Environment for LunaticEnvironment { 50 | fn get_process(&self, id: u64) -> Option> { 51 | self.processes.get(&id).map(|x| x.clone()) 52 | } 53 | 54 | fn add_process(&self, id: u64, proc: Arc) { 55 | self.processes.insert(id, proc); 56 | #[cfg(all(feature = "metrics", not(feature = "detailed_metrics")))] 57 | let labels: [(String, String); 0] = []; 58 | #[cfg(all(feature = "metrics", feature = "detailed_metrics"))] 59 | let labels = [("environment_id", self.id().to_string())]; 60 | #[cfg(feature = "metrics")] 61 | metrics::gauge!( 62 | "lunatic.process.environment.process.count", 63 | self.processes.len() as f64, 64 | &labels 65 | ); 66 | } 67 | 68 | fn remove_process(&self, id: u64) { 69 | self.processes.remove(&id); 70 | #[cfg(all(feature = "metrics", not(feature = "detailed_metrics")))] 71 | let labels: [(String, String); 0] = []; 72 | #[cfg(all(feature = "metrics", feature = "detailed_metrics"))] 73 | let labels = [("environment_id", self.id().to_string())]; 74 | #[cfg(feature = "metrics")] 75 | metrics::gauge!( 76 | "lunatic.process.environment.process.count", 77 | self.processes.len() as f64, 78 | &labels 79 | ); 80 | } 81 | 82 | fn process_count(&self) -> usize { 83 | self.processes.len() 84 | } 85 | 86 | fn send(&self, id: u64, signal: Signal) { 87 | if let Some(proc) = self.processes.get(&id) { 88 | proc.send(signal); 89 | } 90 | } 91 | 92 | fn get_next_process_id(&self) -> u64 { 93 | self.next_process_id.fetch_add(1, Ordering::Relaxed) 94 | } 95 | 96 | fn id(&self) -> u64 { 97 | self.environment_id 98 | } 99 | 100 | async fn can_spawn_next_process(&self) -> Result> { 101 | // Don't impose any limits to process spawning 102 | Ok(Some(())) 103 | } 104 | } 105 | 106 | #[derive(Clone, Default)] 107 | pub struct LunaticEnvironments { 108 | envs: Arc>>, 109 | } 110 | 111 | #[async_trait] 112 | impl Environments for LunaticEnvironments { 113 | type Env = LunaticEnvironment; 114 | async fn create(&self, id: u64) -> Result> { 115 | let env = Arc::new(LunaticEnvironment::new(id)); 116 | self.envs.insert(id, env.clone()); 117 | #[cfg(feature = "metrics")] 118 | metrics::gauge!("lunatic.process.environment.count", self.envs.len() as f64); 119 | Ok(env) 120 | } 121 | 122 | async fn get(&self, id: u64) -> Option> { 123 | self.envs.get(&id).map(|e| e.clone()) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/lunatic-process/src/message.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The [`Message`] is a special variant of a [`Signal`](crate::Signal) that can be sent to 3 | processes. The most common kind of Message is a [`DataMessage`], but there are also some special 4 | kinds of messages, like the [`Message::LinkDied`], that is received if a linked process dies. 5 | */ 6 | 7 | use std::{ 8 | any::Any, 9 | fmt::Debug, 10 | io::{Read, Write}, 11 | sync::Arc, 12 | }; 13 | 14 | use lunatic_networking_api::{TcpConnection, TlsConnection}; 15 | use tokio::net::UdpSocket; 16 | 17 | use crate::runtimes::wasmtime::WasmtimeCompiledModule; 18 | 19 | pub type Resource = dyn Any + Send + Sync; 20 | 21 | /// Can be sent between processes by being embedded into a [`Signal::Message`][0] 22 | /// 23 | /// A [`Message`] has 2 variants: 24 | /// * Data - Regular message containing a tag, buffer and resources. 25 | /// * LinkDied - A `LinkDied` signal that was turned into a message. 26 | /// 27 | /// [0]: crate::Signal 28 | #[derive(Debug)] 29 | pub enum Message { 30 | Data(DataMessage), 31 | LinkDied(Option), 32 | ProcessDied(u64), 33 | } 34 | 35 | impl Message { 36 | pub fn tag(&self) -> Option { 37 | match self { 38 | Message::Data(message) => message.tag, 39 | Message::LinkDied(tag) => *tag, 40 | Message::ProcessDied(_) => None, 41 | } 42 | } 43 | 44 | pub fn process_id(&self) -> Option { 45 | match self { 46 | Message::Data(_) => None, 47 | Message::LinkDied(_) => None, 48 | Message::ProcessDied(process_id) => Some(*process_id), 49 | } 50 | } 51 | 52 | #[cfg(feature = "metrics")] 53 | pub fn write_metrics(&self) { 54 | match self { 55 | Message::Data(message) => message.write_metrics(), 56 | Message::LinkDied(_) => { 57 | metrics::increment_counter!("lunatic.process.messages.link_died.count"); 58 | } 59 | Message::ProcessDied(_) => {} 60 | } 61 | } 62 | } 63 | 64 | /// A variant of a [`Message`] that has a buffer of data and resources attached to it. 65 | /// 66 | /// It implements the [`Read`](std::io::Read) and [`Write`](std::io::Write) traits. 67 | #[derive(Debug, Default)] 68 | pub struct DataMessage { 69 | // TODO: Only the Node implementation depends on these fields being public. 70 | pub tag: Option, 71 | pub read_ptr: usize, 72 | pub buffer: Vec, 73 | pub resources: Vec>>, 74 | } 75 | 76 | impl DataMessage { 77 | /// Create a new message. 78 | pub fn new(tag: Option, buffer_capacity: usize) -> Self { 79 | Self { 80 | tag, 81 | read_ptr: 0, 82 | buffer: Vec::with_capacity(buffer_capacity), 83 | resources: Vec::new(), 84 | } 85 | } 86 | 87 | /// Create a new message from a vec. 88 | pub fn new_from_vec(tag: Option, buffer: Vec) -> Self { 89 | Self { 90 | tag, 91 | read_ptr: 0, 92 | buffer, 93 | resources: Vec::new(), 94 | } 95 | } 96 | 97 | /// Adds a resource to the message and returns the index of it inside of the message. 98 | /// 99 | /// The resource is `Any` and is downcasted when accessing later. 100 | pub fn add_resource(&mut self, resource: Arc) -> usize { 101 | self.resources.push(Some(resource)); 102 | self.resources.len() - 1 103 | } 104 | 105 | /// Takes a module from the message, but preserves the indexes of all others. 106 | /// 107 | /// If the index is out of bound or the resource is not a module the function will return 108 | /// None. 109 | pub fn take_module( 110 | &mut self, 111 | index: usize, 112 | ) -> Option>> { 113 | self.take_downcast(index) 114 | } 115 | 116 | /// Takes a TCP stream from the message, but preserves the indexes of all others. 117 | /// 118 | /// If the index is out of bound or the resource is not a tcp stream the function will return 119 | /// None. 120 | pub fn take_tcp_stream(&mut self, index: usize) -> Option> { 121 | self.take_downcast(index) 122 | } 123 | 124 | /// Takes a UDP Socket from the message, but preserves the indexes of all others. 125 | /// 126 | /// If the index is out of bound or the resource is not a tcp stream the function will return 127 | /// None. 128 | pub fn take_udp_socket(&mut self, index: usize) -> Option> { 129 | self.take_downcast(index) 130 | } 131 | 132 | /// Takes a TLS stream from the message, but preserves the indexes of all others. 133 | /// 134 | /// If the index is out of bound or the resource is not a tcp stream the function will return 135 | /// None. 136 | pub fn take_tls_stream(&mut self, index: usize) -> Option> { 137 | self.take_downcast(index) 138 | } 139 | 140 | /// Moves read pointer to index. 141 | pub fn seek(&mut self, index: usize) { 142 | self.read_ptr = index; 143 | } 144 | 145 | pub fn size(&self) -> usize { 146 | self.buffer.len() 147 | } 148 | 149 | #[cfg(feature = "metrics")] 150 | pub fn write_metrics(&self) { 151 | metrics::increment_counter!("lunatic.process.messages.data.count"); 152 | metrics::histogram!( 153 | "lunatic.process.messages.data.resources.count", 154 | self.resources.len() as f64 155 | ); 156 | metrics::histogram!("lunatic.process.messages.data.size", self.size() as f64); 157 | } 158 | 159 | fn take_downcast(&mut self, index: usize) -> Option> { 160 | let resource = self.resources.get_mut(index); 161 | match resource { 162 | Some(resource_ref) => { 163 | let resource_any = std::mem::take(resource_ref).map(|resource| resource.downcast()); 164 | match resource_any { 165 | Some(Ok(resource)) => Some(resource), 166 | Some(Err(resource)) => { 167 | *resource_ref = Some(resource); 168 | None 169 | } 170 | None => None, 171 | } 172 | } 173 | None => None, 174 | } 175 | } 176 | } 177 | 178 | impl Write for DataMessage { 179 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 180 | self.buffer.extend(buf); 181 | Ok(buf.len()) 182 | } 183 | 184 | fn flush(&mut self) -> std::io::Result<()> { 185 | Ok(()) 186 | } 187 | } 188 | 189 | impl Read for DataMessage { 190 | fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { 191 | let slice = if let Some(slice) = self.buffer.get(self.read_ptr..) { 192 | slice 193 | } else { 194 | return Err(std::io::Error::new( 195 | std::io::ErrorKind::OutOfMemory, 196 | "Reading outside message buffer", 197 | )); 198 | }; 199 | let bytes = buf.write(slice)?; 200 | self.read_ptr += bytes; 201 | Ok(bytes) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /crates/lunatic-process/src/runtimes/mod.rs: -------------------------------------------------------------------------------- 1 | //! WebAssembly runtimes powering lunatic. 2 | //! 3 | //! Currently only Wasmtime is supported, but it should be "easy" to add any runtime that has a 4 | //! `Linker` abstraction and supports `async` host functions. 5 | //! 6 | //! NOTE: This traits are not used at all. Until rust supports async-traits all functions working 7 | //! with a runtime will directly take `wasmtime::WasmtimeRuntime` instead of a generic. 8 | 9 | use std::sync::Arc; 10 | 11 | use anyhow::Result; 12 | use dashmap::DashMap; 13 | use tokio::task::JoinHandle; 14 | 15 | use crate::state::ProcessState; 16 | 17 | use self::wasmtime::{WasmtimeCompiledModule, WasmtimeRuntime}; 18 | 19 | pub mod wasmtime; 20 | 21 | pub struct RawWasm { 22 | // Id returned by control and used when spawning modules on other nodes 23 | pub id: Option, 24 | pub bytes: Vec, 25 | } 26 | 27 | impl RawWasm { 28 | pub fn new(id: Option, bytes: Vec) -> Self { 29 | Self { id, bytes } 30 | } 31 | 32 | pub fn as_slice(&self) -> &[u8] { 33 | self.bytes.as_slice() 34 | } 35 | } 36 | 37 | impl From> for RawWasm { 38 | fn from(bytes: Vec) -> Self { 39 | Self::new(None, bytes) 40 | } 41 | } 42 | 43 | /// A `WasmRuntime` is a compiler that can generate runnable code from raw .wasm files. 44 | /// 45 | /// It also provides a mechanism to register host functions that are accessible to the wasm guest 46 | /// code through the generic type `T`. The type `T` must implement the [`ProcessState`] trait and 47 | /// expose a `register` function for host functions. 48 | pub trait WasmRuntime: Clone 49 | where 50 | T: crate::state::ProcessState + Default + Send, 51 | { 52 | type WasmInstance: WasmInstance; 53 | 54 | /// Takes a raw binary WebAssembly module and returns the index of a compiled module. 55 | fn compile_module(&mut self, data: RawWasm) -> anyhow::Result; 56 | 57 | /// Returns a reference to the raw binary WebAssembly module if the index exists. 58 | fn wasm_module(&self, index: usize) -> Option<&RawWasm>; 59 | 60 | // Creates a wasm instance from compiled module if the index exists. 61 | /* async fn instantiate( 62 | &self, 63 | index: usize, 64 | state: T, 65 | config: ProcessConfig, 66 | ) -> Result>; */ 67 | } 68 | 69 | pub trait WasmInstance { 70 | type Param; 71 | 72 | // Calls a wasm function by name with the specified arguments. Ignores the returned values. 73 | /* async fn call(&mut self, function: &str, params: Vec) -> Result<()>; */ 74 | } 75 | 76 | pub struct Modules { 77 | modules: Arc>>>, 78 | } 79 | 80 | impl Clone for Modules { 81 | fn clone(&self) -> Self { 82 | Self { 83 | modules: self.modules.clone(), 84 | } 85 | } 86 | } 87 | 88 | impl Default for Modules { 89 | fn default() -> Self { 90 | Self { 91 | modules: Arc::new(DashMap::new()), 92 | } 93 | } 94 | } 95 | 96 | impl Modules { 97 | pub fn get(&self, module_id: u64) -> Option>> { 98 | self.modules.get(&module_id).map(|m| m.clone()) 99 | } 100 | 101 | pub fn compile( 102 | &self, 103 | runtime: WasmtimeRuntime, 104 | wasm: RawWasm, 105 | ) -> JoinHandle>>> { 106 | let modules = self.modules.clone(); 107 | tokio::task::spawn_blocking(move || { 108 | let id = wasm.id; 109 | match runtime.compile_module(wasm) { 110 | Ok(m) => { 111 | let module = Arc::new(m); 112 | if let Some(id) = id { 113 | modules.insert(id, Arc::clone(&module)); 114 | } 115 | Ok(module) 116 | } 117 | Err(e) => Err(e), 118 | } 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crates/lunatic-process/src/runtimes/wasmtime.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use wasmtime::ResourceLimiter; 5 | 6 | use crate::{ 7 | config::{ProcessConfig, UNIT_OF_COMPUTE_IN_INSTRUCTIONS}, 8 | state::ProcessState, 9 | ExecutionResult, ResultValue, 10 | }; 11 | 12 | use super::RawWasm; 13 | 14 | #[derive(Clone)] 15 | pub struct WasmtimeRuntime { 16 | engine: wasmtime::Engine, 17 | } 18 | 19 | impl WasmtimeRuntime { 20 | pub fn new(config: &wasmtime::Config) -> Result { 21 | let engine = wasmtime::Engine::new(config)?; 22 | Ok(Self { engine }) 23 | } 24 | 25 | /// Compiles a wasm module to machine code and performs type-checking on host functions. 26 | pub fn compile_module(&self, data: RawWasm) -> Result> 27 | where 28 | T: ProcessState, 29 | { 30 | let module = wasmtime::Module::new(&self.engine, data.as_slice())?; 31 | let mut linker = wasmtime::Linker::new(&self.engine); 32 | // Register host functions to linker. 33 | ::register(&mut linker)?; 34 | let instance_pre = linker.instantiate_pre(&module)?; 35 | let compiled_module = WasmtimeCompiledModule::new(data, module, instance_pre); 36 | Ok(compiled_module) 37 | } 38 | 39 | pub async fn instantiate( 40 | &self, 41 | compiled_module: &WasmtimeCompiledModule, 42 | state: T, 43 | ) -> Result> 44 | where 45 | T: ProcessState + Send + ResourceLimiter, 46 | { 47 | let max_fuel = state.config().get_max_fuel(); 48 | let mut store = wasmtime::Store::new(&self.engine, state); 49 | // Set limits of the store 50 | store.limiter(|state| state); 51 | // Trap if out of fuel 52 | store.out_of_fuel_trap(); 53 | // Define maximum fuel 54 | match max_fuel { 55 | Some(max_fuel) => { 56 | store.out_of_fuel_async_yield(max_fuel, UNIT_OF_COMPUTE_IN_INSTRUCTIONS) 57 | } 58 | // If no limit is specified use maximum 59 | None => store.out_of_fuel_async_yield(u64::MAX, UNIT_OF_COMPUTE_IN_INSTRUCTIONS), 60 | }; 61 | // Create instance 62 | let instance = compiled_module 63 | .instantiator() 64 | .instantiate_async(&mut store) 65 | .await?; 66 | // Mark state as initialized 67 | store.data_mut().initialize(); 68 | Ok(WasmtimeInstance { store, instance }) 69 | } 70 | } 71 | 72 | pub struct WasmtimeCompiledModule { 73 | inner: Arc>, 74 | } 75 | 76 | pub struct WasmtimeCompiledModuleInner { 77 | source: RawWasm, 78 | module: wasmtime::Module, 79 | instance_pre: wasmtime::InstancePre, 80 | } 81 | 82 | impl WasmtimeCompiledModule { 83 | pub fn new( 84 | source: RawWasm, 85 | module: wasmtime::Module, 86 | instance_pre: wasmtime::InstancePre, 87 | ) -> WasmtimeCompiledModule { 88 | let inner = Arc::new(WasmtimeCompiledModuleInner { 89 | source, 90 | module, 91 | instance_pre, 92 | }); 93 | Self { inner } 94 | } 95 | 96 | pub fn exports(&self) -> impl ExactSizeIterator> { 97 | self.inner.module.exports() 98 | } 99 | 100 | pub fn source(&self) -> &RawWasm { 101 | &self.inner.source 102 | } 103 | 104 | pub fn instantiator(&self) -> &wasmtime::InstancePre { 105 | &self.inner.instance_pre 106 | } 107 | } 108 | 109 | impl Clone for WasmtimeCompiledModule { 110 | fn clone(&self) -> Self { 111 | Self { 112 | inner: self.inner.clone(), 113 | } 114 | } 115 | } 116 | 117 | pub struct WasmtimeInstance 118 | where 119 | T: Send, 120 | { 121 | store: wasmtime::Store, 122 | instance: wasmtime::Instance, 123 | } 124 | 125 | impl WasmtimeInstance 126 | where 127 | T: Send, 128 | { 129 | pub async fn call(mut self, function: &str, params: Vec) -> ExecutionResult { 130 | let entry = self.instance.get_func(&mut self.store, function); 131 | 132 | if entry.is_none() { 133 | return ExecutionResult { 134 | state: self.store.into_data(), 135 | result: ResultValue::SpawnError(format!("Function '{function}' not found")), 136 | }; 137 | } 138 | 139 | let result = entry 140 | .unwrap() 141 | .call_async(&mut self.store, ¶ms, &mut []) 142 | .await; 143 | 144 | ExecutionResult { 145 | state: self.store.into_data(), 146 | result: match result { 147 | Ok(()) => ResultValue::Ok, 148 | Err(err) => { 149 | // If the trap is a result of calling `proc_exit(0)`, treat it as an no-error finish. 150 | match err.downcast_ref::() { 151 | Some(wasmtime_wasi::I32Exit(0)) => ResultValue::Ok, 152 | _ => ResultValue::Failed(err.to_string()), 153 | } 154 | } 155 | }, 156 | } 157 | } 158 | } 159 | 160 | pub fn default_config() -> wasmtime::Config { 161 | let mut config = wasmtime::Config::new(); 162 | config 163 | .async_support(true) 164 | .debug_info(false) 165 | // The behavior of fuel running out is defined on the Store 166 | .consume_fuel(true) 167 | .wasm_reference_types(true) 168 | .wasm_bulk_memory(true) 169 | .wasm_multi_value(true) 170 | .wasm_multi_memory(true) 171 | .cranelift_opt_level(wasmtime::OptLevel::SpeedAndSize) 172 | // Allocate resources on demand because we can't predict how many process will exist 173 | .allocation_strategy(wasmtime::InstanceAllocationStrategy::OnDemand) 174 | // Always use static memories 175 | .static_memory_forced(true); 176 | config 177 | } 178 | -------------------------------------------------------------------------------- /crates/lunatic-process/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use anyhow::Result; 4 | use hash_map_id::HashMapId; 5 | use tokio::sync::{ 6 | mpsc::{UnboundedReceiver, UnboundedSender}, 7 | Mutex, RwLock, 8 | }; 9 | use wasmtime::Linker; 10 | 11 | use crate::{ 12 | config::ProcessConfig, 13 | mailbox::MessageMailbox, 14 | runtimes::wasmtime::{WasmtimeCompiledModule, WasmtimeRuntime}, 15 | Signal, 16 | }; 17 | 18 | pub type ConfigResources = HashMapId; 19 | pub type SignalSender = UnboundedSender; 20 | pub type SignalReceiver = Arc>>; 21 | 22 | /// The internal state of a process. 23 | /// 24 | /// The `ProcessState` has two main roles: 25 | /// - It holds onto all vm resources (file descriptors, tcp streams, channels, ...) 26 | /// - Registers all host functions working on those resources to the `Linker` 27 | pub trait ProcessState: Sized { 28 | type Config: ProcessConfig + Default + Send + Sync; 29 | 30 | // Create a new `ProcessState` using the parent's state (self) to inherit environment and 31 | // other parts of the state. 32 | // This is used in the guest function `spawn` which uses this trait and not the concrete state. 33 | fn new_state( 34 | &self, 35 | module: Arc>, 36 | config: Arc, 37 | ) -> Result; 38 | 39 | /// Register all host functions to the linker. 40 | fn register(linker: &mut Linker) -> Result<()>; 41 | /// Marks a wasm instance as initialized 42 | fn initialize(&mut self); 43 | /// Returns true if the instance was initialized 44 | fn is_initialized(&self) -> bool; 45 | 46 | /// Returns the WebAssembly runtime 47 | fn runtime(&self) -> &WasmtimeRuntime; 48 | // Returns the WebAssembly module 49 | fn module(&self) -> &Arc>; 50 | /// Returns the process configuration 51 | fn config(&self) -> &Arc; 52 | 53 | // Returns process ID 54 | fn id(&self) -> u64; 55 | // Returns signal mailbox 56 | fn signal_mailbox(&self) -> &(SignalSender, SignalReceiver); 57 | // Returns message mailbox 58 | fn message_mailbox(&self) -> &MessageMailbox; 59 | 60 | // Config resources 61 | fn config_resources(&self) -> &ConfigResources; 62 | fn config_resources_mut(&mut self) -> &mut ConfigResources; 63 | 64 | // Registry 65 | fn registry(&self) -> &Arc>>; 66 | } 67 | -------------------------------------------------------------------------------- /crates/lunatic-process/src/wasm.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use log::trace; 5 | use tokio::task::JoinHandle; 6 | use wasmtime::{ResourceLimiter, Val}; 7 | 8 | use crate::env::Environment; 9 | use crate::runtimes::wasmtime::{WasmtimeCompiledModule, WasmtimeRuntime}; 10 | use crate::state::ProcessState; 11 | use crate::{Process, Signal, WasmProcess}; 12 | 13 | /// Spawns a new wasm process from a compiled module. 14 | /// 15 | /// A `Process` is created from a `module`, entry `function`, array of arguments and config. The 16 | /// configuration will define some characteristics of the process, such as maximum memory, fuel 17 | /// and host function properties (filesystem access, networking, ..). 18 | /// 19 | /// After it's spawned the process will keep running in the background. A process can be killed 20 | /// with `Signal::Kill` signal. If you would like to block until the process is finished you can 21 | /// `.await` on the returned `JoinHandle<()>`. 22 | pub async fn spawn_wasm( 23 | env: Arc, 24 | runtime: WasmtimeRuntime, 25 | module: &WasmtimeCompiledModule, 26 | state: S, 27 | function: &str, 28 | params: Vec, 29 | link: Option<(Option, Arc)>, 30 | ) -> Result<(JoinHandle>, Arc)> 31 | where 32 | S: ProcessState + Send + Sync + ResourceLimiter + 'static, 33 | { 34 | let id = state.id(); 35 | trace!("Spawning process: {}", id); 36 | let signal_mailbox = state.signal_mailbox().clone(); 37 | let message_mailbox = state.message_mailbox().clone(); 38 | 39 | let instance = runtime.instantiate(module, state).await?; 40 | let function = function.to_string(); 41 | let fut = async move { instance.call(&function, params).await }; 42 | let child_process = crate::new(fut, id, env.clone(), signal_mailbox.1, message_mailbox); 43 | let child_process_handle = Arc::new(WasmProcess::new(id, signal_mailbox.0.clone())); 44 | 45 | env.add_process(id, child_process_handle.clone()); 46 | 47 | // **Child link guarantees**: 48 | // The link signal is going to be put inside of the child's mailbox and is going to be 49 | // processed before any child code can run. This means that any failure inside the child 50 | // Wasm code will be correctly reported to the parent. 51 | // 52 | // We assume here that the code inside of `process::new()` will not fail during signal 53 | // handling. 54 | // 55 | // **Parent link guarantees**: 56 | // A `tokio::task::yield_now()` call is executed to allow the parent to link the child 57 | // before continuing any further execution. This should force the parent to process all 58 | // signals right away. 59 | // 60 | // The parent could have received a `kill` signal in its mailbox before this function was 61 | // called and this signal is going to be processed before the link is established (FIFO). 62 | // Only after the yield function we can guarantee that the child is going to be notified 63 | // if the parent fails. This is ok, as the actual spawning of the child happens after the 64 | // call, so the child wouldn't even exist if the parent failed before. 65 | // 66 | // TODO: The guarantees provided here don't hold anymore in a distributed environment and 67 | // will require some rethinking. This function will be executed on a completely 68 | // different computer and needs to be synced in a more robust way with the parent 69 | // running somewhere else. 70 | if let Some((tag, process)) = link { 71 | // Send signal to itself to perform the linking 72 | process.send(Signal::Link(None, child_process_handle.clone())); 73 | // Suspend itself to process all new signals 74 | tokio::task::yield_now().await; 75 | // Send signal to child to link it 76 | signal_mailbox 77 | .0 78 | .send(Signal::Link(tag, process)) 79 | .expect("receiver must exist at this point"); 80 | } 81 | 82 | // Spawn a background process 83 | trace!("Process size: {}", std::mem::size_of_val(&child_process)); 84 | let join = tokio::task::spawn(child_process); 85 | Ok((join, child_process_handle)) 86 | } 87 | -------------------------------------------------------------------------------- /crates/lunatic-registry-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-registry-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions for registering named processes." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [features] 11 | metrics = ["dep:metrics"] 12 | 13 | [dependencies] 14 | lunatic-common-api = { workspace = true } 15 | lunatic-process = { workspace = true } 16 | lunatic-process-api = { workspace = true } 17 | 18 | tokio = { workspace = true, features = ["sync"] } 19 | anyhow = { workspace = true } 20 | metrics = { workspace = true, optional = true } 21 | wasmtime = { workspace = true } 22 | -------------------------------------------------------------------------------- /crates/lunatic-registry-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | use anyhow::Result; 4 | use lunatic_common_api::{get_memory, IntoTrap}; 5 | use lunatic_process::state::ProcessState; 6 | use lunatic_process_api::ProcessCtx; 7 | use wasmtime::{Caller, Linker}; 8 | 9 | // Register the registry APIs to the linker 10 | pub fn register + Send + Sync + 'static>( 11 | linker: &mut Linker, 12 | ) -> Result<()> { 13 | linker.func_wrap4_async("lunatic::registry", "put", put)?; 14 | linker.func_wrap4_async("lunatic::registry", "get", get)?; 15 | linker.func_wrap2_async("lunatic::registry", "remove", remove)?; 16 | 17 | #[cfg(feature = "metrics")] 18 | metrics::describe_counter!( 19 | "lunatic.registry.write", 20 | metrics::Unit::Count, 21 | "number of new entries written to the registry" 22 | ); 23 | #[cfg(feature = "metrics")] 24 | metrics::describe_counter!( 25 | "lunatic.timers.read", 26 | metrics::Unit::Count, 27 | "number of entries read from the registry" 28 | ); 29 | #[cfg(feature = "metrics")] 30 | metrics::describe_counter!( 31 | "lunatic.timers.deletion", 32 | metrics::Unit::Count, 33 | "number of entries deleted from the registry" 34 | ); 35 | #[cfg(feature = "metrics")] 36 | metrics::describe_gauge!( 37 | "lunatic.timers.registered", 38 | metrics::Unit::Count, 39 | "number of processes currently registered" 40 | ); 41 | 42 | Ok(()) 43 | } 44 | 45 | // Registers process with ID under `name`. 46 | // 47 | // Traps: 48 | // * If the process ID doesn't exist. 49 | // * If any memory outside the guest heap space is referenced. 50 | fn put + Send + Sync>( 51 | mut caller: Caller, 52 | name_str_ptr: u32, 53 | name_str_len: u32, 54 | node_id: u64, 55 | process_id: u64, 56 | ) -> Box> + Send + '_> { 57 | Box::new(async move { 58 | let memory = get_memory(&mut caller)?; 59 | let (memory_slice, state) = memory.data_and_store_mut(&mut caller); 60 | let name = memory_slice 61 | .get(name_str_ptr as usize..(name_str_ptr + name_str_len) as usize) 62 | .or_trap("lunatic::registry::put")?; 63 | let name = std::str::from_utf8(name).or_trap("lunatic::registry::put")?; 64 | 65 | state 66 | .registry() 67 | .write() 68 | .await 69 | .insert(name.to_owned(), (node_id, process_id)); 70 | 71 | #[cfg(feature = "metrics")] 72 | metrics::increment_counter!("lunatic.registry.write"); 73 | 74 | #[cfg(feature = "metrics")] 75 | metrics::increment_gauge!("lunatic.registry.registered", 1.0); 76 | 77 | Ok(()) 78 | }) 79 | } 80 | 81 | // Looks up process under `name` and returns 0 if it was found or 1 if not found. 82 | // 83 | // Traps: 84 | // * If any memory outside the guest heap space is referenced. 85 | fn get + Send + Sync>( 86 | mut caller: Caller, 87 | name_str_ptr: u32, 88 | name_str_len: u32, 89 | node_id_ptr: u32, 90 | process_id_ptr: u32, 91 | ) -> Box> + Send + '_> { 92 | Box::new(async move { 93 | let memory = get_memory(&mut caller)?; 94 | let (memory_slice, state) = memory.data_and_store_mut(&mut caller); 95 | let name = memory_slice 96 | .get(name_str_ptr as usize..(name_str_ptr + name_str_len) as usize) 97 | .or_trap("lunatic::registry::get")?; 98 | let name = std::str::from_utf8(name).or_trap("lunatic::registry::get")?; 99 | 100 | #[cfg(feature = "metrics")] 101 | metrics::increment_counter!("lunatic.registry.read"); 102 | 103 | let (node_id, process_id) = if let Some(process) = state.registry().read().await.get(name) { 104 | *process 105 | } else { 106 | return Ok(1); 107 | }; 108 | 109 | memory 110 | .write(&mut caller, node_id_ptr as usize, &node_id.to_le_bytes()) 111 | .or_trap("lunatic::registry::get")?; 112 | 113 | memory 114 | .write( 115 | &mut caller, 116 | process_id_ptr as usize, 117 | &process_id.to_le_bytes(), 118 | ) 119 | .or_trap("lunatic::registry::get")?; 120 | Ok(0) 121 | }) 122 | } 123 | 124 | // Removes process under `name` if it exists. 125 | // 126 | // Traps: 127 | // * If any memory outside the guest heap space is referenced. 128 | fn remove + Send + Sync>( 129 | mut caller: Caller, 130 | name_str_ptr: u32, 131 | name_str_len: u32, 132 | ) -> Box> + Send + '_> { 133 | Box::new(async move { 134 | let memory = get_memory(&mut caller)?; 135 | let (memory_slice, state) = memory.data_and_store_mut(&mut caller); 136 | let name = memory_slice 137 | .get(name_str_ptr as usize..(name_str_ptr + name_str_len) as usize) 138 | .or_trap("lunatic::registry::get")?; 139 | let name = std::str::from_utf8(name).or_trap("lunatic::registry::get")?; 140 | 141 | state.registry().write().await.remove(name); 142 | 143 | #[cfg(feature = "metrics")] 144 | metrics::increment_counter!("lunatic.registry.deletion"); 145 | 146 | #[cfg(feature = "metrics")] 147 | metrics::decrement_gauge!("lunatic.registry.registered", 1.0); 148 | 149 | Ok(()) 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Lunatic host functions for sqlite." 3 | edition = "2021" 4 | homepage = "https://lunatic.solutions" 5 | license = "Apache-2.0 OR MIT" 6 | name = "lunatic-sqlite-api" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-sqlite-api" 8 | version = "0.13.3" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | bincode = { workspace = true } 14 | serde = { version = "1.0", features = ["derive"] } 15 | 16 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 17 | anyhow = { workspace = true } 18 | hash-map-id = { workspace = true } 19 | lunatic-common-api = { workspace = true } 20 | lunatic-error-api = { workspace = true } 21 | lunatic-process = { workspace = true } 22 | lunatic-process-api = { workspace = true } 23 | sqlite = { version = "0.30.4", package = "sqlite-bindings-lunatic" } 24 | wasmtime = { workspace = true } 25 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/src/guest_api/guest_bindings.rs: -------------------------------------------------------------------------------- 1 | pub use crate::wire_format::{ 2 | BindKey, BindList, BindPair, BindValue, SqliteError, SqliteRow, SqliteValue, 3 | }; 4 | 5 | pub mod sqlite_guest_bindings { 6 | #[link(wasm_import_module = "lunatic::sqlite")] 7 | extern "C" { 8 | /// opens a new connection and stores a reference to the resource 9 | /// 10 | /// returns the connection_id which can be used for later calls and 11 | /// can be safely transported between guest and host 12 | pub fn open(path: *const u8, path_len: usize, connection_id: *mut u32) -> u64; 13 | 14 | /// 15 | /// Creates a new prepared statement and returns the id of the prepared statement 16 | /// to the guest so that values can be bound to the statement at a later point 17 | pub fn query_prepare(connection_id: u64, query_str: *const u8, query_str_len: u32) -> u64; 18 | 19 | /// Executes the passed query and returns the SQLite response code 20 | pub fn execute(connection_id: u64, exec_str: *const u8, exec_str_len: u32) -> u32; 21 | 22 | /// Binds one or more values to the statement identified by `statement_id`. 23 | /// The function expects to receive a `BindList` encoded via `bincode` as demonstrated by this example: 24 | /// 25 | /// ``` 26 | /// 27 | /// let query = "INSERT INTO cars (manufacturer, model) VALUES(:manufacturer, :model_name);" 28 | /// let statement_id = unsafe {sqlite_guest_bindings::query_prepare(connection_id, query.as_ptr(), query.len() as u32)}; 29 | /// 30 | /// let key = BindKey::String("model_name".into()); 31 | /// let value = BindValue::Int(996); 32 | /// let bind_list = BindList(vec![ 33 | /// BindPair(key, value) 34 | /// ]); 35 | /// let encoded = bincode::serialize(&bind_list).unwrap(); 36 | /// let result = unsafe { 37 | /// sqlite_guest_bindings::bind_value( 38 | /// statement_id, 39 | /// encoded.as_ptr() as u32, 40 | /// encoded.len() as u32, 41 | /// ) 42 | /// }; 43 | /// ``` 44 | /// 45 | /// Anything other than a `BindList` will be rejected and a Trap will be returned 46 | pub fn bind_value(statement_id: u64, bind_data_ptr: u32, bind_data_len: u32); 47 | 48 | /// returns count of changes/rows that the last call to SQLite triggered 49 | pub fn sqlite3_changes(connection_id: u64) -> u32; 50 | 51 | /// resets the bound statement so that it can be used/bound again 52 | pub fn statement_reset(statement_id: u64); 53 | 54 | /// furthers the internal SQLite cursor and returns either 55 | /// SQLITE_DONE or SQLITE_ROW to indicate whether there's more 56 | /// data to be pulled from the previous query 57 | pub fn sqlite3_step(connection_id: u64) -> u32; 58 | 59 | /// Drops the connection identified by `connection_id` in the host and 60 | /// closes the connection to SQLite 61 | pub fn sqlite3_finalize(connection_id: u64); 62 | 63 | /// returns the count of columns for the executed statement 64 | pub fn column_count(statement_id: u64) -> u32; 65 | 66 | /// NOTE: the following functions will require a registered `alloc` function 67 | /// because it relies on calling into the guest and allocating a chunk of memory 68 | /// in the guest so that results of queries can be written directly into the 69 | /// guest memory and not temporarily stored in the host as is the case with 70 | /// `query_prepare_and_consume` and `query_result_get`. 71 | /// 72 | /// The functions have a return value of `u64` which contains the pointer 73 | /// to the allocated guest memory (most likely a `Vec`) to which the 74 | /// results of the call have been written. 75 | /// The value of `u64` is split into two `u32` parts respectively: 76 | /// - the length of data written 77 | /// - the pointer to the data 78 | /// 79 | /// and can be retrieved via a function such as this: 80 | /// 81 | /// ``` 82 | /// fn unroll_vec(ptr: u64) -> Vec { 83 | /// unsafe { 84 | /// // the leftmost half contains the length 85 | /// let length = (ptr >> 32) as usize; 86 | /// // the rightmost half contains the pointer 87 | /// let ptr = 0x00000000FFFFFFFF & ptr; 88 | /// Vec::from_raw_parts(ptr as *mut u8, length, length) 89 | /// } 90 | /// } 91 | /// ``` 92 | /// 93 | /// 94 | 95 | /// looks up the value of the last error, encodes an `SqliteError` via bincode 96 | /// and writes it into a guest allocated Vec 97 | /// Returns a composite length + pointer to the data (see explanation above) 98 | pub fn last_error(connection_id: u64, opaque_ptr: *mut u32) -> u32; 99 | 100 | /// reads the column under index `col_idx` encodes a `SqliteValue` via bincode 101 | /// and writes it into a guest allocated Vec 102 | /// Returns a composite length + pointer to the data (see explanation above) 103 | pub fn read_column(statement_id: u64, col_idx: u32, opaque_ptr: *mut u32) -> u32; 104 | 105 | /// reads the next row, encodes a `SqliteRow` via bincode 106 | /// and writes it into a guest allocated Vec 107 | pub fn read_row(statement_id: u64, opaque_ptr: *mut u32) -> u32; 108 | 109 | /// looks up the name of the column under index `col_idx`, encodes a `String` via bincode 110 | /// and writes it into a guest allocated Vec 111 | pub fn column_name(statement_id: u64, col_idx: u32, opaque_ptr: *mut u32) -> u32; 112 | 113 | /// looks up the value of the last error, encodes a `Vec` via bincode 114 | /// and writes it into a guest allocated Vec 115 | pub fn column_names(statement_id: u64, opaque_ptr: *mut u32) -> u32; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/src/guest_api/mod.rs: -------------------------------------------------------------------------------- 1 | mod guest_bindings; 2 | 3 | pub use guest_bindings::*; 4 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod wire_format; 2 | 3 | #[cfg(not(target_arch = "wasm32"))] 4 | mod sqlite_bindings; 5 | 6 | #[cfg(not(target_arch = "wasm32"))] 7 | pub use sqlite_bindings::*; 8 | 9 | #[cfg(target_arch = "wasm32")] 10 | pub mod guest_api; 11 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/src/wire_format/bind_values/host_api.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use lunatic_common_api::IntoTrap; 3 | use sqlite::Statement; 4 | 5 | use super::{BindKey, BindPair, BindValue, SqliteError}; 6 | 7 | impl BindPair { 8 | pub fn bind(&self, statement: &mut Statement) -> Result<()> { 9 | if let BindKey::Numeric(idx) = self.0 { 10 | return match self.1.clone() { 11 | BindValue::Null => statement.bind((idx, ())), 12 | BindValue::Blob(b) => statement.bind((idx, &b[..])), 13 | BindValue::Text(t) => statement.bind((idx, t.as_str())), 14 | BindValue::Double(d) => statement.bind((idx, d)), 15 | BindValue::Int(i) => statement.bind((idx, i as i64)), 16 | BindValue::Int64(i) => statement.bind((idx, i)), 17 | } 18 | .or_trap("sqlite::bind::pair"); 19 | } 20 | match self.1.clone() { 21 | BindValue::Blob(b) => statement.bind(&[&b[..]][..]), 22 | BindValue::Null => statement.bind(&[()][..]), 23 | BindValue::Text(t) => statement.bind(&[t.as_str()][..]), 24 | BindValue::Double(d) => statement.bind(&[d][..]), 25 | BindValue::Int(i) => statement.bind(&[i as i64][..]), 26 | BindValue::Int64(i) => statement.bind(&[i][..]), 27 | } 28 | .or_trap("sqlite::bind::single") 29 | } 30 | } 31 | 32 | // mapping of error from sqlite error 33 | impl From for SqliteError { 34 | fn from(err: sqlite::Error) -> Self { 35 | Self { 36 | code: err.code.map(|code| code as u32), 37 | message: err.message, 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/src/wire_format/bind_values/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[cfg(not(target_arch = "wasm32"))] 6 | mod host_api; 7 | 8 | /// Struct used for binding a certain `BindValue` to either 9 | /// a numeric key or a named key in a prepared statement 10 | #[derive(Debug, Serialize, Deserialize)] 11 | pub enum BindKey { 12 | /// Is encoded as 0x00 13 | None, 14 | /// Is encoded as 0x01 15 | /// and uses a u32 for length of stream 16 | /// which is stored as usize because the sqlite library needs usize 17 | /// and this will save repeated conversions 18 | Numeric(usize), 19 | /// Is encoded as 0x02 20 | /// indicates that a string is used as index for the bind value 21 | String(String), 22 | } 23 | 24 | /// Represents a pair of BindKey and BindValue 25 | /// that are used to bind certain data to a prepared statement 26 | /// where BindKey is usually either a numeric index 27 | /// starting with 1 or a string. 28 | #[derive(Debug, Serialize, Deserialize)] 29 | pub struct BindPair(pub BindKey, pub BindValue); 30 | 31 | /// Enum that represents possible different 32 | /// types of values that can be bound to a prepared statements 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | pub enum BindValue { 35 | Null, 36 | Blob(Vec), 37 | Text(String), 38 | Double(f64), 39 | Int(i32), 40 | Int64(i64), 41 | } 42 | 43 | #[derive(Debug, Serialize, Deserialize)] 44 | pub struct BindList(pub Vec); 45 | 46 | impl Deref for BindList { 47 | type Target = Vec; 48 | 49 | fn deref(&self) -> &Self::Target { 50 | &self.0 51 | } 52 | } 53 | 54 | // ============================ 55 | // Error structure 56 | // ============================ 57 | /// Error structure that carries data from sqlite_errmsg and sqlite_errcode 58 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 59 | pub struct SqliteError { 60 | pub code: Option, 61 | pub message: Option, 62 | } 63 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/src/wire_format/mod.rs: -------------------------------------------------------------------------------- 1 | mod bind_values; 2 | mod sqlite_value; 3 | 4 | pub use bind_values::*; 5 | pub use sqlite_value::*; 6 | 7 | #[derive(Eq, PartialEq, Clone, Debug)] 8 | pub enum DbError<'a> { 9 | /// contains path to which access was attempted 10 | PermissionDenied(&'a str), 11 | } 12 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/src/wire_format/sqlite_value/guest_api.rs: -------------------------------------------------------------------------------- 1 | use super::{SqliteRow, SqliteValue}; 2 | 3 | #[cfg(target_arch = "wasm32")] 4 | impl SqliteRow { 5 | pub fn get_column(&self, idx: i32) -> Option<&SqliteValue> { 6 | self.0.get(idx as usize) 7 | } 8 | } 9 | 10 | #[cfg(target_arch = "wasm32")] 11 | impl SqliteValue { 12 | pub fn read_text(&self) -> &str { 13 | if let SqliteValue::Text(text) = self { 14 | return text.as_str(); 15 | } 16 | panic!("Trying to read non-text value as text"); 17 | } 18 | 19 | pub fn read_text_string(&self) -> String { 20 | if let SqliteValue::Text(text) = self { 21 | return text.clone(); 22 | } 23 | panic!("Trying to read non-text value as text"); 24 | } 25 | 26 | pub fn read_blob(&self) -> &[u8] { 27 | if let SqliteValue::Blob(blob) = self { 28 | return blob.as_slice(); 29 | } 30 | panic!("Trying to read non-blob value as blob"); 31 | } 32 | 33 | pub fn read_integer(&self) -> i32 { 34 | if let SqliteValue::Integer(int) = self { 35 | return *int as i32; 36 | } 37 | panic!("Trying to read non-integer value as integer"); 38 | } 39 | 40 | pub fn read_long(&self) -> i64 { 41 | if let SqliteValue::I64(int) = self { 42 | return *int; 43 | } 44 | panic!("Trying to read non-long value as long"); 45 | } 46 | 47 | pub fn read_double(&self) -> f64 { 48 | if let SqliteValue::Double(double) = self { 49 | return *double; 50 | } 51 | panic!("Trying to read non-double value as double"); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/src/wire_format/sqlite_value/host_api.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | use anyhow::Result; 3 | #[cfg(not(target_arch = "wasm32"))] 4 | use lunatic_common_api::IntoTrap; 5 | 6 | #[cfg(not(target_arch = "wasm32"))] 7 | use sqlite::Statement; 8 | 9 | use super::{SqliteRow, SqliteValue}; 10 | 11 | #[cfg(not(target_arch = "wasm32"))] 12 | impl SqliteRow { 13 | pub fn read_row(statement: &mut Statement) -> Result { 14 | let mut row = SqliteRow::default(); 15 | for column_idx in 0..statement.column_count() { 16 | row.0.push(SqliteValue::read_column(statement, column_idx)?); 17 | } 18 | Ok(row) 19 | } 20 | } 21 | 22 | #[cfg(not(target_arch = "wasm32"))] 23 | impl<'stmt> SqliteValue { 24 | pub fn read_column(statement: &'stmt Statement, col_idx: usize) -> Result { 25 | match statement.column_type(col_idx).or_trap("read_column")? { 26 | sqlite::Type::Binary => { 27 | let bytes = statement 28 | .read::, usize>(col_idx) 29 | .or_trap("lunatic::sqlite::query_prepare::read_binary")?; 30 | 31 | Ok(SqliteValue::Blob(bytes)) 32 | } 33 | sqlite::Type::Float => Ok(SqliteValue::Double( 34 | statement 35 | .read::(col_idx) 36 | .or_trap("lunatic::sqlite::query_prepare::read_float")?, 37 | )), 38 | sqlite::Type::Integer => Ok(SqliteValue::Integer( 39 | statement 40 | .read::(col_idx) 41 | .or_trap("lunatic::sqlite::query_prepare::read_integer")?, 42 | )), 43 | sqlite::Type::String => { 44 | let bytes = statement 45 | .read::(col_idx) 46 | .or_trap("lunatic::sqlite::query_prepare::read_string")?; 47 | 48 | Ok(SqliteValue::Text(bytes)) 49 | } 50 | sqlite::Type::Null => Ok(SqliteValue::Null), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/lunatic-sqlite-api/src/wire_format/sqlite_value/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[cfg(not(target_arch = "wasm32"))] 4 | mod host_api; 5 | #[cfg(not(target_arch = "wasm32"))] 6 | pub use host_api::*; 7 | 8 | #[cfg(target_arch = "wasm32")] 9 | mod guest_api; 10 | 11 | #[cfg(target_arch = "wasm32")] 12 | pub use guest_api::*; 13 | 14 | #[derive(Debug, Serialize, Deserialize, Clone)] 15 | pub enum SqliteValue { 16 | Null, 17 | Blob(Vec), 18 | Text(String), 19 | Double(f64), 20 | Integer(i64), 21 | I64(i64), 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize, Default, Clone)] 25 | pub struct SqliteRow(pub Vec); 26 | -------------------------------------------------------------------------------- /crates/lunatic-stdout-capture/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-stdout-capture" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Helper library for holding stdout streams of lunatic processes." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-registry-api" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | wasi-common = { workspace = true } 12 | wiggle = { workspace = true } 13 | -------------------------------------------------------------------------------- /crates/lunatic-timer-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-timer-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions for working with timers." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-timer-api" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [features] 11 | metrics = ["dep:metrics"] 12 | 13 | [dependencies] 14 | hash-map-id = { workspace = true } 15 | lunatic-common-api = { workspace = true } 16 | lunatic-process = { workspace = true } 17 | lunatic-process-api = { workspace = true } 18 | 19 | anyhow = { workspace = true } 20 | metrics = { workspace = true, optional = true } 21 | tokio = { workspace = true, features = ["time", "rt"] } 22 | wasmtime = { workspace = true } 23 | -------------------------------------------------------------------------------- /crates/lunatic-timer-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::Ordering, 3 | collections::BinaryHeap, 4 | future::Future, 5 | time::{Duration, Instant}, 6 | }; 7 | 8 | use anyhow::Result; 9 | use hash_map_id::HashMapId; 10 | use lunatic_common_api::IntoTrap; 11 | use lunatic_process::{state::ProcessState, Signal}; 12 | use lunatic_process_api::ProcessCtx; 13 | use tokio::task::JoinHandle; 14 | use wasmtime::{Caller, Linker}; 15 | 16 | #[derive(Debug)] 17 | struct HeapValue { 18 | instant: Instant, 19 | key: u64, 20 | } 21 | 22 | impl PartialOrd for HeapValue { 23 | fn partial_cmp(&self, other: &Self) -> Option { 24 | Some(self.cmp(other)) 25 | } 26 | } 27 | 28 | impl Ord for HeapValue { 29 | fn cmp(&self, other: &Self) -> Ordering { 30 | self.instant.cmp(&other.instant).reverse() 31 | } 32 | } 33 | 34 | impl PartialEq for HeapValue { 35 | fn eq(&self, other: &Self) -> bool { 36 | self.instant.eq(&other.instant) 37 | } 38 | } 39 | 40 | impl Eq for HeapValue {} 41 | 42 | #[derive(Debug, Default)] 43 | pub struct TimerResources { 44 | hash_map: HashMapId>, 45 | heap: BinaryHeap, 46 | } 47 | 48 | impl TimerResources { 49 | pub fn add(&mut self, handle: JoinHandle<()>, target_time: Instant) -> u64 { 50 | self.cleanup_expired_timers(); 51 | 52 | let id = self.hash_map.add(handle); 53 | self.heap.push(HeapValue { 54 | instant: target_time, 55 | key: id, 56 | }); 57 | id 58 | } 59 | 60 | fn cleanup_expired_timers(&mut self) { 61 | let deadline = Instant::now(); 62 | while let Some(HeapValue { instant, .. }) = self.heap.peek() { 63 | if *instant > deadline { 64 | // instant is after the deadline so stop 65 | return; 66 | } 67 | 68 | let key = self 69 | .heap 70 | .pop() 71 | .expect("not empty because we matched on peek") 72 | .key; 73 | self.hash_map.remove(key); 74 | } 75 | } 76 | 77 | pub fn remove(&mut self, id: u64) -> Option> { 78 | self.hash_map.remove(id) 79 | } 80 | } 81 | 82 | pub trait TimerCtx { 83 | fn timer_resources(&self) -> &TimerResources; 84 | fn timer_resources_mut(&mut self) -> &mut TimerResources; 85 | } 86 | 87 | pub fn register + TimerCtx + Send + 'static>( 88 | linker: &mut Linker, 89 | ) -> Result<()> { 90 | linker.func_wrap("lunatic::timer", "send_after", send_after)?; 91 | linker.func_wrap1_async("lunatic::timer", "cancel_timer", cancel_timer)?; 92 | 93 | #[cfg(feature = "metrics")] 94 | metrics::describe_counter!( 95 | "lunatic.timers.started", 96 | metrics::Unit::Count, 97 | "number of timers set since startup, will usually be completed + canceled + active" 98 | ); 99 | #[cfg(feature = "metrics")] 100 | metrics::describe_counter!( 101 | "lunatic.timers.completed", 102 | metrics::Unit::Count, 103 | "number of timers completed since startup" 104 | ); 105 | #[cfg(feature = "metrics")] 106 | metrics::describe_counter!( 107 | "lunatic.timers.canceled", 108 | metrics::Unit::Count, 109 | "number of timers canceled since startup" 110 | ); 111 | #[cfg(feature = "metrics")] 112 | metrics::describe_gauge!( 113 | "lunatic.timers.active", 114 | metrics::Unit::Count, 115 | "number of timers currently active" 116 | ); 117 | 118 | Ok(()) 119 | } 120 | 121 | // Sends the message to a process after a delay. 122 | // 123 | // There are no guarantees that the message will be received. 124 | // 125 | // Traps: 126 | // * If the process ID doesn't exist. 127 | // * If it's called before creating the next message. 128 | fn send_after + TimerCtx>( 129 | mut caller: Caller, 130 | process_id: u64, 131 | delay: u64, 132 | ) -> Result { 133 | let message = caller 134 | .data_mut() 135 | .message_scratch_area() 136 | .take() 137 | .or_trap("lunatic::message::send_after")?; 138 | 139 | let process = caller.data_mut().environment().get_process(process_id); 140 | 141 | let target_time = Instant::now() + Duration::from_millis(delay); 142 | let timer_handle = tokio::task::spawn(async move { 143 | #[cfg(feature = "metrics")] 144 | metrics::increment_counter!("lunatic.timers.started"); 145 | #[cfg(feature = "metrics")] 146 | metrics::increment_gauge!("lunatic.timers.active", 1.0); 147 | let duration_remaining = target_time - Instant::now(); 148 | if duration_remaining != Duration::ZERO { 149 | tokio::time::sleep(duration_remaining).await; 150 | } 151 | if let Some(process) = process { 152 | #[cfg(feature = "metrics")] 153 | metrics::increment_counter!("lunatic.timers.completed"); 154 | #[cfg(feature = "metrics")] 155 | metrics::decrement_gauge!("lunatic.timers.active", 1.0); 156 | process.send(Signal::Message(message)); 157 | } 158 | }); 159 | 160 | let id = caller 161 | .data_mut() 162 | .timer_resources_mut() 163 | .add(timer_handle, target_time); 164 | Ok(id) 165 | } 166 | 167 | // Cancels the specified timer. 168 | // 169 | // Returns: 170 | // * 1 if a timer with the timer_id was found 171 | // * 0 if no timer was found, this can be either because: 172 | // - timer had expired 173 | // - timer already had been canceled 174 | // - timer_id never corresponded to a timer 175 | fn cancel_timer( 176 | mut caller: Caller, 177 | timer_id: u64, 178 | ) -> Box> + Send + '_> { 179 | Box::new(async move { 180 | let timer_handle = caller.data_mut().timer_resources_mut().remove(timer_id); 181 | match timer_handle { 182 | Some(timer_handle) => { 183 | timer_handle.abort(); 184 | #[cfg(feature = "metrics")] 185 | metrics::increment_counter!("lunatic.timers.canceled"); 186 | #[cfg(feature = "metrics")] 187 | metrics::decrement_gauge!("lunatic.timers.active", 1.0); 188 | Ok(1) 189 | } 190 | None => Ok(0), 191 | } 192 | }) 193 | } 194 | -------------------------------------------------------------------------------- /crates/lunatic-trap-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-trap-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions for catching traps." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | lunatic-common-api = { workspace = true } 12 | 13 | anyhow = { workspace = true } 14 | wasmtime = { workspace = true } -------------------------------------------------------------------------------- /crates/lunatic-trap-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | use anyhow::Result; 4 | use lunatic_common_api::IntoTrap; 5 | use wasmtime::{Caller, Linker, Val}; 6 | 7 | // Register the trap APIs to the linker 8 | pub fn register(linker: &mut Linker) -> Result<()> { 9 | linker.func_wrap2_async("lunatic::trap", "catch", catch_trap::)?; 10 | Ok(()) 11 | } 12 | 13 | // Can be used as a trampoline to catch traps inside of guest by jumping 14 | // through the host. 15 | // 16 | // WebAssembly doesn't have unwinding support, this means that traps can't 17 | // be caught by just guest code. To work around that, this function can be 18 | // used to jump back into the guest. 19 | // 20 | // If the guest code invoked by this function fails, it will return `0`, 21 | // otherwise it will return whatever the guest export `_lunatic_catch_trap` 22 | // returns. 23 | // 24 | // This function will expect a `_lunatic_catch_trap` function export. This 25 | // export will get the parameters `function` and `pointer` forwarded to it. 26 | // 27 | // Traps: 28 | // * If export `_lunatic_catch_trap` doesn't exist or is not a function. 29 | fn catch_trap( 30 | mut caller: Caller, 31 | function: i32, 32 | pointer: i32, 33 | ) -> Box> + Send + '_> { 34 | Box::new(async move { 35 | let lunatic_catch_trap = caller 36 | .get_export("_lunatic_catch_trap") 37 | .or_trap("lunatic::trap::catch: No export `_lunatic_catch_trap` defined in module")? 38 | .into_func() 39 | .or_trap("lunatic::trap::catch: Export `_lunatic_catch_trap` is not a function")?; 40 | 41 | let params = [Val::I32(function), Val::I32(pointer)]; 42 | let mut result = [Val::I32(0)]; 43 | let execution_result = lunatic_catch_trap 44 | .call_async(caller, ¶ms, &mut result) 45 | .await; 46 | match execution_result { 47 | Ok(()) => Ok(result.get(0).unwrap().i32().unwrap()), 48 | Err(_) => Ok(0), 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /crates/lunatic-version-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-version-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions for getting Lunatic host version" 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-version-api" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | anyhow = { workspace = true } 12 | wasmtime = { workspace = true } 13 | -------------------------------------------------------------------------------- /crates/lunatic-version-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasmtime::Linker; 2 | 3 | /// Links the `version` APIs. 4 | pub fn register(linker: &mut Linker) -> anyhow::Result<()> { 5 | linker.func_wrap("lunatic::version", "major", major)?; 6 | linker.func_wrap("lunatic::version", "minor", minor)?; 7 | linker.func_wrap("lunatic::version", "patch", patch)?; 8 | Ok(()) 9 | } 10 | 11 | fn major() -> u32 { 12 | env!("CARGO_PKG_VERSION_MAJOR").parse::().unwrap() 13 | } 14 | 15 | fn minor() -> u32 { 16 | env!("CARGO_PKG_VERSION_MINOR").parse::().unwrap() 17 | } 18 | 19 | fn patch() -> u32 { 20 | env!("CARGO_PKG_VERSION_PATCH").parse::().unwrap() 21 | } 22 | -------------------------------------------------------------------------------- /crates/lunatic-wasi-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lunatic-wasi-api" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "Lunatic host functions for WASI." 6 | homepage = "https://lunatic.solutions" 7 | repository = "https://github.com/lunatic-solutions/lunatic/tree/main/crates/lunatic-wasi-api" 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [dependencies] 11 | lunatic-common-api = { workspace = true } 12 | lunatic-process = { workspace = true } 13 | lunatic-stdout-capture = { workspace = true } 14 | 15 | anyhow = { workspace = true } 16 | wasmtime = { workspace = true } 17 | wasmtime-wasi = { workspace = true } 18 | -------------------------------------------------------------------------------- /crates/lunatic-wasi-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use lunatic_common_api::{get_memory, IntoTrap}; 3 | use lunatic_process::state::ProcessState; 4 | use lunatic_stdout_capture::StdoutCapture; 5 | use wasmtime::{Caller, Linker}; 6 | use wasmtime_wasi::{ambient_authority, Dir, WasiCtx, WasiCtxBuilder}; 7 | 8 | /// Create a `WasiCtx` from configuration settings. 9 | pub fn build_wasi( 10 | args: Option<&Vec>, 11 | envs: Option<&Vec<(String, String)>>, 12 | dirs: &[(String, String)], 13 | ) -> Result { 14 | let mut wasi = WasiCtxBuilder::new().inherit_stdio(); 15 | if let Some(envs) = envs { 16 | wasi = wasi.envs(envs)?; 17 | } 18 | if let Some(args) = args { 19 | wasi = wasi.args(args)?; 20 | } 21 | for (preopen_dir_path, resolved_path) in dirs { 22 | let preopen_dir = Dir::open_ambient_dir(resolved_path, ambient_authority())?; 23 | wasi = wasi.preopened_dir(preopen_dir, preopen_dir_path)?; 24 | } 25 | Ok(wasi.build()) 26 | } 27 | 28 | pub trait LunaticWasiConfigCtx { 29 | fn add_environment_variable(&mut self, key: String, value: String); 30 | fn add_command_line_argument(&mut self, argument: String); 31 | fn preopen_dir(&mut self, dir: String); 32 | } 33 | 34 | pub trait LunaticWasiCtx { 35 | fn wasi(&self) -> &WasiCtx; 36 | fn wasi_mut(&mut self) -> &mut WasiCtx; 37 | fn set_stdout(&mut self, stdout: StdoutCapture); 38 | fn get_stdout(&self) -> Option<&StdoutCapture>; 39 | fn set_stderr(&mut self, stderr: StdoutCapture); 40 | fn get_stderr(&self) -> Option<&StdoutCapture>; 41 | } 42 | 43 | // Register WASI APIs to the linker 44 | pub fn register(linker: &mut Linker) -> Result<()> 45 | where 46 | T: ProcessState + LunaticWasiCtx + Send + 'static, 47 | T::Config: LunaticWasiConfigCtx, 48 | { 49 | // Register all wasi host functions 50 | wasmtime_wasi::sync::snapshots::preview_1::add_wasi_snapshot_preview1_to_linker( 51 | linker, 52 | |ctx| ctx.wasi_mut(), 53 | )?; 54 | 55 | // Register host functions to configure wasi 56 | linker.func_wrap( 57 | "lunatic::wasi", 58 | "config_add_environment_variable", 59 | add_environment_variable, 60 | )?; 61 | linker.func_wrap( 62 | "lunatic::wasi", 63 | "config_add_command_line_argument", 64 | add_command_line_argument, 65 | )?; 66 | linker.func_wrap("lunatic::wasi", "config_preopen_dir", preopen_dir)?; 67 | 68 | Ok(()) 69 | } 70 | 71 | // Adds environment variable to a configuration. 72 | // 73 | // Traps: 74 | // * If the config ID doesn't exist. 75 | // * If the key or value string is not a valid utf8 string. 76 | // * If any of the memory slices falls outside the memory. 77 | fn add_environment_variable( 78 | mut caller: Caller, 79 | config_id: u64, 80 | key_ptr: u32, 81 | key_len: u32, 82 | value_ptr: u32, 83 | value_len: u32, 84 | ) -> Result<()> 85 | where 86 | T: ProcessState, 87 | T::Config: LunaticWasiConfigCtx, 88 | { 89 | let memory = get_memory(&mut caller)?; 90 | let key_str = memory 91 | .data(&caller) 92 | .get(key_ptr as usize..(key_ptr + key_len) as usize) 93 | .or_trap("lunatic::wasi::config_add_environment_variable")?; 94 | let key = std::str::from_utf8(key_str) 95 | .or_trap("lunatic::wasi::config_add_environment_variable")? 96 | .to_string(); 97 | let value_str = memory 98 | .data(&caller) 99 | .get(value_ptr as usize..(value_ptr + value_len) as usize) 100 | .or_trap("lunatic::wasi::config_add_environment_variable")?; 101 | let value = std::str::from_utf8(value_str) 102 | .or_trap("lunatic::wasi::config_add_environment_variable")? 103 | .to_string(); 104 | 105 | caller 106 | .data_mut() 107 | .config_resources_mut() 108 | .get_mut(config_id) 109 | .or_trap("lunatic::wasi::config_set_max_memory: Config ID doesn't exist")? 110 | .add_environment_variable(key, value); 111 | Ok(()) 112 | } 113 | 114 | // Adds command line argument to a configuration. 115 | // 116 | // Traps: 117 | // * If the config ID doesn't exist. 118 | // * If the argument string is not a valid utf8 string. 119 | // * If any of the memory slices falls outside the memory. 120 | fn add_command_line_argument( 121 | mut caller: Caller, 122 | config_id: u64, 123 | argument_ptr: u32, 124 | argument_len: u32, 125 | ) -> Result<()> 126 | where 127 | T: ProcessState, 128 | T::Config: LunaticWasiConfigCtx, 129 | { 130 | let memory = get_memory(&mut caller)?; 131 | let argument_str = memory 132 | .data(&caller) 133 | .get(argument_ptr as usize..(argument_ptr + argument_len) as usize) 134 | .or_trap("lunatic::wasi::add_command_line_argument")?; 135 | let argument = std::str::from_utf8(argument_str) 136 | .or_trap("lunatic::wasi::add_command_line_argument")? 137 | .to_string(); 138 | 139 | caller 140 | .data_mut() 141 | .config_resources_mut() 142 | .get_mut(config_id) 143 | .or_trap("lunatic::wasi::add_command_line_argument: Config ID doesn't exist")? 144 | .add_command_line_argument(argument); 145 | Ok(()) 146 | } 147 | 148 | // Mark a directory as preopened in the configuration. 149 | // 150 | // Traps: 151 | // * If the config ID doesn't exist. 152 | // * If the directory string is not a valid utf8 string. 153 | // * If any of the memory slices falls outside the memory. 154 | fn preopen_dir(mut caller: Caller, config_id: u64, dir_ptr: u32, dir_len: u32) -> Result<()> 155 | where 156 | T: ProcessState, 157 | T::Config: LunaticWasiConfigCtx, 158 | { 159 | let memory = get_memory(&mut caller)?; 160 | let dir_str = memory 161 | .data(&caller) 162 | .get(dir_ptr as usize..(dir_ptr + dir_len) as usize) 163 | .or_trap("lunatic::wasi::preopen_dir")?; 164 | let dir = std::str::from_utf8(dir_str) 165 | .or_trap("lunatic::wasi::preopen_dir")? 166 | .to_string(); 167 | 168 | caller 169 | .data_mut() 170 | .config_resources_mut() 171 | .get_mut(config_id) 172 | .or_trap("lunatic::wasi::preopen_dir: Config ID doesn't exist")? 173 | .preopen_dir(dir); 174 | Ok(()) 175 | } 176 | -------------------------------------------------------------------------------- /examples/native_process.rs: -------------------------------------------------------------------------------- 1 | // TODO: Show an example of spawning native processes and using them from Wasm. 2 | pub fn main() {} 3 | -------------------------------------------------------------------------------- /src/cargo_lunatic.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::{args_os, set_var}, 3 | process::{exit, Command}, 4 | }; 5 | 6 | fn main() { 7 | set_var("CARGO_BUILD_TARGET", "wasm32-wasi"); 8 | set_var("CARGO_TARGET_WASM32_WASI_RUNNER", "lunatic"); 9 | exit( 10 | Command::new("cargo") 11 | .args(args_os().skip(2)) 12 | .status() 13 | .unwrap() 14 | .code() 15 | .unwrap(), 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The [lunatic vm](https://lunatic.solutions/) is a system for creating actors from WebAssembly 3 | modules. This `lunatic-runtime` library allows you to embed the `lunatic vm` inside your Rust 4 | code. 5 | 6 | > _The actor model in computer science is a mathematical model of concurrent computation that 7 | treats actor as the universal primitive of concurrent computation. In response to a message it 8 | receives, an actor can: make local decisions, create more actors, send more messages, and 9 | determine how to respond to the next message received. Actors may modify their own private 10 | state, but can only affect each other indirectly through messaging (removing the need for 11 | lock-based synchronization)._ 12 | > 13 | > Source: 14 | 15 | _**Note:** If you are looking to build actors in Rust and compile them to `lunatic` compatible 16 | Wasm modules, checkout out the [lunatic crate](https://crates.io/crates/lunatic)_. 17 | 18 | ## Core Concepts 19 | 20 | * [`Environment`] - defines the characteristics of Processes that are spawned into it. An 21 | [`Environment`] is created with an [`EnvConfig`] to tweak various settings, like maximum 22 | memory and compute usage. 23 | 24 | * [`WasmProcess`](process::WasmProcess) - a handle to send signals and messages to spawned 25 | Wasm processes. It implements the [`Process`](process::Process) trait. 26 | 27 | 28 | ## WebAssembly module requirements 29 | 30 | TODO 31 | */ 32 | 33 | mod config; 34 | pub mod state; 35 | 36 | pub use config::DefaultProcessConfig; 37 | pub use lunatic_process::{Finished, Process, Signal, WasmProcess}; 38 | pub use state::DefaultProcessState; 39 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod mode; 2 | 3 | use mode::{cargo_test, execution}; 4 | 5 | use anyhow::Result; 6 | use regex::Regex; 7 | use std::collections::VecDeque; 8 | use std::{env, path::PathBuf}; 9 | 10 | // Lunatic versions under 0.13 implied run 11 | // This checks whether the 0.12 behaviour is wanted with a regex 12 | fn is_run_implied() -> bool { 13 | if std::env::args().count() < 2 { 14 | return false; 15 | } 16 | 17 | // lunatic -> Implied run 18 | // lunatic run -> Explicit run 19 | // lunatic fdskl -> Not implied run 20 | let test_re = Regex::new(r"^(--bench|--dir|.+\.wasm)") 21 | .expect("BUG: Regex error with lunatic::mode::execution::is_run_implied()"); 22 | 23 | test_re.is_match(&std::env::args().nth(1).unwrap()) 24 | } 25 | 26 | #[tokio::main] 27 | async fn main() -> Result<()> { 28 | // Run is implied from lunatic 0.12 29 | let augmented_args = if is_run_implied() { 30 | let mut augmented_args: VecDeque = std::env::args().collect(); 31 | augmented_args.insert(1, "run".to_owned()); 32 | Some(augmented_args.into()) 33 | } else { 34 | None 35 | }; 36 | 37 | // Detect if `cargo test` is running 38 | // https://internals.rust-lang.org/t/cargo-config-tom-different-runner-for-tests/16342/ 39 | let cargo_test = match env::var("CARGO_MANIFEST_DIR") { 40 | Ok(_manifest_dir) => { 41 | // _manifest_dir is not used as a prefix because it breaks testing in workspaces where 42 | // the `target` dir lives outside the manifest dir. 43 | let test_path_matcher: PathBuf = [ 44 | "target", 45 | "wasm32-(wasi|unknown-unknown)", 46 | "(debug|release)", 47 | "deps", 48 | ] 49 | .iter() 50 | .collect(); 51 | // Escape \ if it is used as path separator 52 | let separator = format!("{}", std::path::MAIN_SEPARATOR).replace('\\', r"\\"); 53 | let test_path_matcher = test_path_matcher.to_string_lossy().replace('\\', r"\\"); 54 | // Regex that will match test builds 55 | let test_regex = format!("{separator}{test_path_matcher}{separator}.*\\.wasm$"); 56 | let test_regex = regex::Regex::new(&test_regex).unwrap(); 57 | 58 | let skip_positions = match is_run_implied() { 59 | true => 1, 60 | false => 2, 61 | }; 62 | 63 | // Check if the 3rd argument is a rust wasm build in the `deps` directory 64 | // && none of the other arguments indicate a benchmark 65 | let mut arguments = env::args().skip(skip_positions); 66 | match arguments.next() { 67 | Some(wasm_file) => { 68 | test_regex.is_match(&wasm_file) && !arguments.any(|arg| arg == "--bench") 69 | } 70 | 71 | None => false, 72 | } 73 | } 74 | Err(_) => false, 75 | }; 76 | 77 | if cargo_test { 78 | cargo_test::test(augmented_args).await 79 | } else { 80 | execution::execute(augmented_args).await 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/mode/app.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use clap::Parser; 3 | use reqwest::{Method, StatusCode}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::mode::config::ProjectLunaticConfig; 7 | 8 | use super::config::ConfigManager; 9 | 10 | #[derive(Parser, Debug, Clone)] 11 | #[clap(rename_all = "kebab_case")] 12 | pub enum AppArgs { 13 | Create { name: String }, 14 | } 15 | 16 | #[derive(Parser, Debug)] 17 | pub struct Args { 18 | #[command(subcommand)] 19 | app: AppArgs, 20 | } 21 | 22 | #[derive(Serialize)] 23 | pub struct CreateProject { 24 | name: String, 25 | } 26 | 27 | #[derive(Serialize, Deserialize, Debug)] 28 | pub struct Project { 29 | pub project_id: i64, 30 | pub name: String, 31 | pub domains: Vec, 32 | } 33 | 34 | #[derive(Serialize, Deserialize, Debug)] 35 | pub struct App { 36 | app_id: i64, 37 | } 38 | 39 | #[derive(Serialize, Deserialize, Debug)] 40 | pub struct Env { 41 | env_id: i64, 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Debug)] 45 | pub struct ProjectDetails { 46 | pub project_id: i64, 47 | pub apps: Vec, 48 | pub envs: Vec, 49 | } 50 | 51 | pub(crate) async fn start(args: Args) -> Result<()> { 52 | match args.app { 53 | AppArgs::Create { name } => { 54 | let mut config_manager = ConfigManager::new().unwrap(); 55 | if config_manager.project_config.is_some() { 56 | return Err(anyhow!( 57 | "Project is already initialized, `lunatic.toml` exists in current directory." 58 | )); 59 | } 60 | let (_, project): (StatusCode, Project) = config_manager 61 | .request_platform( 62 | Method::POST, 63 | "api/projects", 64 | "create app", 65 | Some(CreateProject { name }), 66 | None, 67 | ) 68 | .await?; 69 | let (_, project_details): (StatusCode, ProjectDetails) = config_manager 70 | .request_platform::( 71 | Method::GET, 72 | &format!("api/projects/{}", project.project_id), 73 | "get project", 74 | None, 75 | None, 76 | ) 77 | .await?; 78 | // TODO for now every project has single app and env 79 | config_manager.init_project(ProjectLunaticConfig { 80 | project_id: project.project_id, 81 | project_name: project.name, 82 | domains: project.domains, 83 | app_id: project_details 84 | .apps 85 | .get(0) 86 | .map(|app| app.app_id) 87 | .ok_or_else(|| anyhow!("Unexpected config missing app_id"))?, 88 | env_id: project_details 89 | .envs 90 | .get(0) 91 | .map(|env| env.env_id) 92 | .ok_or_else(|| anyhow!("Unexpected config missing env_id"))?, 93 | env_vars: None, 94 | assets_dir: None, 95 | }); 96 | config_manager.flush()?; 97 | } 98 | } 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /src/mode/common.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::Arc}; 2 | 3 | use anyhow::{anyhow, Context, Result}; 4 | use clap::Args; 5 | 6 | use lunatic_distributed::DistributedProcessState; 7 | use lunatic_process::{ 8 | env::{Environment, LunaticEnvironment, LunaticEnvironments}, 9 | runtimes::{wasmtime::WasmtimeRuntime, RawWasm}, 10 | wasm::spawn_wasm, 11 | }; 12 | use lunatic_process_api::ProcessConfigCtx; 13 | use lunatic_runtime::{DefaultProcessConfig, DefaultProcessState}; 14 | 15 | #[derive(Args, Debug)] 16 | pub struct WasmArgs {} 17 | 18 | pub struct RunWasm { 19 | pub path: PathBuf, 20 | pub wasm_args: Vec, 21 | pub dir: Vec, 22 | 23 | pub runtime: WasmtimeRuntime, 24 | pub envs: Arc, 25 | pub env: Arc, 26 | pub distributed: Option, 27 | } 28 | 29 | pub async fn run_wasm(args: RunWasm) -> Result<()> { 30 | let mut config = DefaultProcessConfig::default(); 31 | // Allow initial process to compile modules, create configurations and spawn sub-processes 32 | config.set_can_compile_modules(true); 33 | config.set_can_create_configs(true); 34 | config.set_can_spawn_processes(true); 35 | 36 | // Path to wasm file 37 | let path = args.path; 38 | 39 | // Set correct command line arguments for the guest 40 | let filename = path.file_name().unwrap().to_string_lossy().to_string(); 41 | let mut wasi_args = vec![filename]; 42 | wasi_args.extend(args.wasm_args); 43 | config.set_command_line_arguments(wasi_args); 44 | 45 | // Inherit environment variables 46 | config.set_environment_variables(std::env::vars().collect()); 47 | 48 | // Always preopen the current dir 49 | config.preopen_dir("."); 50 | for dir in args.dir { 51 | if let Some(s) = dir.as_os_str().to_str() { 52 | config.preopen_dir(s); 53 | } 54 | } 55 | 56 | // Spawn main process 57 | let module = std::fs::read(&path).map_err(|err| match err.kind() { 58 | std::io::ErrorKind::NotFound => anyhow!("Module '{}' not found", path.display()), 59 | _ => err.into(), 60 | })?; 61 | let module: RawWasm = if let Some(dist) = args.distributed.as_ref() { 62 | dist.control.add_module(module).await? 63 | } else { 64 | module.into() 65 | }; 66 | 67 | let module = Arc::new(args.runtime.compile_module::(module)?); 68 | let state = DefaultProcessState::new( 69 | args.env.clone(), 70 | args.distributed, 71 | args.runtime.clone(), 72 | module.clone(), 73 | Arc::new(config), 74 | Default::default(), 75 | ) 76 | .unwrap(); 77 | 78 | args.env.can_spawn_next_process().await?; 79 | let (task, _) = spawn_wasm( 80 | args.env, 81 | args.runtime, 82 | &module, 83 | state, 84 | "_start", 85 | Vec::new(), 86 | None, 87 | ) 88 | .await 89 | .context(format!( 90 | "Failed to spawn process from {}::_start()", 91 | path.to_string_lossy() 92 | ))?; 93 | 94 | // Wait on the main process to finish 95 | task.await.map(|_| ()).map_err(|e| anyhow!(e.to_string())) 96 | } 97 | 98 | #[cfg(feature = "prometheus")] 99 | #[derive(Args, Debug)] 100 | pub struct PrometheusArgs { 101 | /// Enables the prometheus metrics exporter 102 | #[arg(long)] 103 | pub prometheus: bool, 104 | 105 | /// Address to bind the prometheus http listener to 106 | #[arg(long, value_name = "PROMETHEUS_HTTP_ADDRESS", requires = "prometheus")] 107 | pub prometheus_http: Option, 108 | } 109 | 110 | #[cfg(feature = "prometheus")] 111 | pub fn prometheus(http_socket: Option, node_id: Option) -> Result<()> { 112 | metrics_exporter_prometheus::PrometheusBuilder::new() 113 | .with_http_listener(http_socket.unwrap_or_else(|| "0.0.0.0:9927".parse().unwrap())) 114 | .add_global_label("node_id", node_id.unwrap_or(0).to_string()) 115 | .install()?; 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /src/mode/control.rs: -------------------------------------------------------------------------------- 1 | use std::net::{SocketAddr, TcpListener}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use clap::Parser; 5 | 6 | #[derive(Parser, Debug)] 7 | pub(crate) struct Args { 8 | #[arg(long, value_name = "CONTROL_SERVER_SOCKET")] 9 | bind_socket: Option, 10 | } 11 | 12 | pub(crate) async fn start(args: Args) -> Result<()> { 13 | if let Some(socket) = args.bind_socket { 14 | log::info!("Register URL: http://{}/", socket); 15 | lunatic_control_axum::server::control_server(socket).await?; 16 | } else if let Some(listener) = get_available_localhost() { 17 | log::info!("Register URL: http://{}/", listener.local_addr().unwrap()); 18 | lunatic_control_axum::server::control_server_from_tcp(listener).await?; 19 | } 20 | 21 | Err(anyhow!("No available port on 127.0.0.1. Aborting")) 22 | } 23 | 24 | fn get_available_localhost() -> Option { 25 | for port in 3030..3999u16 { 26 | if let Ok(s) = TcpListener::bind(("127.0.0.1", port)) { 27 | return Some(s); 28 | } 29 | } 30 | 31 | for port in 1025..65535u16 { 32 | if let Ok(s) = TcpListener::bind(("127.0.0.1", port)) { 33 | return Some(s); 34 | } 35 | } 36 | 37 | None 38 | } 39 | -------------------------------------------------------------------------------- /src/mode/deploy/README.md: -------------------------------------------------------------------------------- 1 | # Lunatic platform CLI 2 | 3 | This platform-related subset of lunatic CLI app is a command-line interface tool that allows you authenticate, manage, build and deploy programs to the lunatic platform. 4 | 5 | ## Getting Started 6 | 7 | 1. Before starting install [Lunatic](https://github.com/lunatic-solutions/lunatic). 8 | 9 | ``` 10 | cargo install lunatic-runtime 11 | ``` 12 | 13 | 2. Create a new account in [Lunatic Cloud](https://lunatic.cloud/). 14 | 15 | Then, login your Lunatic CLI and connect it with Your account. 16 | 17 | ``` 18 | lunatic login 19 | ``` 20 | 21 | Follow instructions displayed in Your terminal and authorize the CLI. 22 | 23 | 24 | 3. Create a new Lunatic Rust project (skip if you have an existing one). 25 | 26 | ``` 27 | # Add the WebAssemby target 28 | rustup target add wasm32-wasi 29 | 30 | # Create a new Rust project 31 | cargo new hello-lunatic 32 | cd hello-lunatic 33 | 34 | # Initialize project for Lunatic 35 | lunatic init 36 | ``` 37 | 38 | 4. Setup Your project on the [Lunatic Cloud](https://lunatic.cloud). 39 | 40 | ``` 41 | lunatic app create hello-lunatic 42 | ``` 43 | 44 | This will create a `lunatic.toml` configuration file with the following content. 45 | ```toml 46 | project_id = 17 47 | project_name = "hello-lunatic" 48 | domains = ["73685543-25ce-462d-b397-21bf921873d6.lunatic.run"] 49 | app_id = 18 50 | env_id = 17 51 | ``` 52 | 53 | 5. Deploy Your application. 54 | 55 | ``` 56 | lunatic deploy 57 | ``` 58 | -------------------------------------------------------------------------------- /src/mode/deploy/artefact.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::mode::config::{FileBased, ProjectLunaticConfig}; 4 | 5 | pub(crate) static TARGET_DIR: &str = "target"; 6 | 7 | pub fn get_target_dir() -> PathBuf { 8 | let mut current_dir = 9 | ProjectLunaticConfig::get_file_path().expect("should have found config path"); 10 | current_dir.pop(); 11 | current_dir.join(TARGET_DIR) 12 | } 13 | -------------------------------------------------------------------------------- /src/mode/deploy/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Stdio; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use log::{debug, info}; 5 | 6 | use crate::mode::deploy::artefact::get_target_dir; 7 | 8 | pub(crate) async fn start_build() -> Result<()> { 9 | let target_dir = get_target_dir(); 10 | info!("Starting build"); 11 | debug!("Executing the command `cargo build --release`"); 12 | std::process::Command::new("cargo") 13 | .env("CARGO_TARGET_DIR", target_dir) 14 | .args(["build", "--release"]) 15 | .stdout(Stdio::inherit()) 16 | .stderr(Stdio::inherit()) 17 | .output() 18 | .map_err(|e| anyhow!("failed to execute build {e:?}"))?; 19 | info!("Successfully built artefacts"); 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /src/mode/deploy/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | ffi::OsStr, 4 | fs::File, 5 | io::{Cursor, Read, Write}, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | use anyhow::{anyhow, Context, Result}; 10 | use log::debug; 11 | use reqwest::Method; 12 | use serde::{Deserialize, Serialize}; 13 | use serde_json::Value; 14 | use zip::{write::FileOptions, CompressionMethod, ZipWriter}; 15 | mod artefact; 16 | mod build; 17 | 18 | use super::config::ConfigManager; 19 | 20 | #[derive(Debug, Deserialize)] 21 | struct Package { 22 | name: String, 23 | } 24 | 25 | #[derive(Debug, Deserialize)] 26 | struct CargoToml { 27 | package: Package, 28 | } 29 | 30 | #[derive(Debug, Serialize)] 31 | struct StartApp { 32 | app_id: i64, 33 | } 34 | 35 | #[derive(Debug, Serialize)] 36 | struct NewAppInstance { 37 | app_version_id: i64, 38 | env_id: i64, 39 | } 40 | 41 | pub(crate) async fn start() -> Result<()> { 42 | let cwd = std::env::current_dir()?; 43 | let mut config = ConfigManager::new().map_err(|e| anyhow!("Failed to load config {e:?}"))?; 44 | let project_config = config 45 | .project_config 46 | .as_ref() 47 | .ok_or_else(|| anyhow!("Cannot find project config, missing `lunatic.toml`"))?; 48 | let project_name = project_config.project_name.clone(); 49 | let app_id = project_config.app_id; 50 | let env_id = project_config.env_id; 51 | let env_vars = project_config.env_vars.clone(); 52 | let assets_dir = project_config.assets_dir.clone(); 53 | 54 | let mut file = File::open(cwd.join("Cargo.toml")).map_err(|e| { 55 | anyhow!( 56 | "Cannot find project Cargo.toml in path {}. {e}", 57 | cwd.to_string_lossy() 58 | ) 59 | })?; 60 | let mut content = String::new(); 61 | 62 | file.read_to_string(&mut content)?; 63 | 64 | let cargo: CargoToml = toml::from_str(&content)?; 65 | debug!("{:#?}", cargo); 66 | 67 | build::start_build().await?; 68 | 69 | let binary_name = format!("{}.wasm", cargo.package.name); 70 | let artefact = cwd.join("target/wasm32-wasi/release").join(&binary_name); 71 | 72 | if artefact.exists() && artefact.is_file() { 73 | println!( 74 | "Deploying project: {project_name} new version of app {}", 75 | cargo.package.name 76 | ); 77 | let new_version_id = 78 | upload_wasm_binary(env_id, app_id, binary_name, artefact, &mut config).await?; 79 | upload_env_vars_if_exist(&cwd, env_id, env_vars, &config).await?; 80 | upload_static_files_if_exist(&cwd, env_id, assets_dir, &config).await?; 81 | start_app(app_id, env_id, &config).await?; 82 | println!( 83 | "Deployed project: {project_name} new version app \"{}\", version={new_version_id}", 84 | cargo.package.name 85 | ); 86 | Ok(()) 87 | } else { 88 | Err(anyhow!("Cannot find {binary_name} build directory")) 89 | } 90 | } 91 | 92 | async fn upload_env_vars_if_exist( 93 | cwd: &Path, 94 | env_id: i64, 95 | env_vars: Option, 96 | config_manager: &ConfigManager, 97 | ) -> Result<()> { 98 | let mut envs = HashMap::new(); 99 | let envs_path = cwd.join(env_vars.unwrap_or_else(|| ".env".to_string())); 100 | if envs_path.exists() && envs_path.is_file() { 101 | if let Ok(iter) = dotenvy::from_path_iter(envs_path) { 102 | for item in iter { 103 | let (key, val) = item.with_context(|| "Error reading .env variables.")?; 104 | envs.insert(key, val); 105 | } 106 | config_manager 107 | .request_platform::>( 108 | Method::POST, 109 | &format!("api/env/{}/vars", env_id), 110 | "env vars", 111 | Some(envs), 112 | None, 113 | ) 114 | .await?; 115 | } 116 | } 117 | Ok(()) 118 | } 119 | 120 | async fn upload_wasm_binary( 121 | env_id: i64, 122 | app_id: i64, 123 | binary_name: String, 124 | artefact: PathBuf, 125 | config_manager: &mut ConfigManager, 126 | ) -> Result { 127 | let mut artefact = File::open(artefact)?; 128 | let mut artefact_bytes = Vec::new(); 129 | artefact.read_to_end(&mut artefact_bytes)?; 130 | let new_version_id = config_manager 131 | .upload_artefact_for_app(&app_id, artefact_bytes, binary_name) 132 | .await?; 133 | config_manager 134 | .request_platform::( 135 | Method::POST, 136 | &format!("api/apps/{app_id}/instances"), 137 | "create app instance", 138 | Some(NewAppInstance { 139 | app_version_id: new_version_id, 140 | env_id, 141 | }), 142 | None, 143 | ) 144 | .await?; 145 | Ok(new_version_id) 146 | } 147 | 148 | async fn upload_static_files_if_exist( 149 | cwd: &Path, 150 | env_id: i64, 151 | assets_dir: Option, 152 | config_manager: &ConfigManager, 153 | ) -> Result<()> { 154 | let static_path = cwd.join(assets_dir.unwrap_or_else(|| "static".to_string())); 155 | if static_path.exists() && static_path.is_dir() { 156 | let writer = Cursor::new(Vec::new()); 157 | let options = FileOptions::default() 158 | .compression_method(CompressionMethod::Stored) 159 | .unix_permissions(0o755); 160 | let mut zip = ZipWriter::new(writer); 161 | let walkdir = walkdir::WalkDir::new(static_path.clone()); 162 | let it = walkdir.into_iter(); 163 | 164 | let zip_root = static_path.file_name().unwrap_or(OsStr::new("static")); 165 | let parent_path = static_path.parent().unwrap_or(&static_path); 166 | zip.add_directory(zip_root.to_string_lossy(), options)?; 167 | 168 | for entry in it.skip(1) { 169 | let entry = entry?; 170 | let path = entry.path(); 171 | let name = path.strip_prefix(parent_path)?; 172 | 173 | if path.is_file() { 174 | zip.start_file(name.to_string_lossy(), options)?; 175 | let mut f = File::open(path)?; 176 | let mut buffer = Vec::new(); 177 | f.read_to_end(&mut buffer)?; 178 | zip.write_all(&buffer)?; 179 | } else if path.is_dir() { 180 | zip.add_directory(name.to_string_lossy(), options)?; 181 | } 182 | } 183 | 184 | let buffer = zip.finish()?.into_inner(); 185 | let part = reqwest::multipart::Part::bytes(buffer) 186 | .file_name("assets.zip") 187 | .mime_str("application/zip")?; 188 | let form = reqwest::multipart::Form::new().part("file", part); 189 | 190 | config_manager 191 | .request_platform::( 192 | Method::POST, 193 | &format!("api/env/{}/assets", env_id), 194 | "assets zip", 195 | None, 196 | Some(form), 197 | ) 198 | .await?; 199 | } 200 | Ok(()) 201 | } 202 | 203 | async fn start_app(app_id: i64, env_id: i64, config_manager: &ConfigManager) -> Result<()> { 204 | config_manager 205 | .request_platform::( 206 | Method::POST, 207 | &format!("api/env/{}/start", env_id), 208 | "app start", 209 | Some(StartApp { app_id }), 210 | None, 211 | ) 212 | .await?; 213 | Ok(()) 214 | } 215 | -------------------------------------------------------------------------------- /src/mode/execution.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::{Parser, Subcommand}; 3 | 4 | #[derive(Parser, Debug)] 5 | #[command(version)] 6 | pub struct Args { 7 | #[command(subcommand)] 8 | command: Commands, 9 | 10 | #[cfg(feature = "prometheus")] 11 | #[command(flatten)] 12 | prometheus: super::common::PrometheusArgs, 13 | } 14 | 15 | #[derive(Debug, Subcommand)] 16 | enum Commands { 17 | /// Initialize a Rust cargo project as a lunatic project 18 | /// 19 | /// This command should be run inside the root folder of a cargo project, 20 | /// containing the Cargo.toml file. It will add configuration options to it 21 | /// in the `cargo/config.toml` file, setting the compilation target to 22 | /// `wasm32-wasi` and the default runner for this target to `lunatic run`. 23 | Init, 24 | /// Executes a .wasm file 25 | Run(super::run::Args), 26 | /// Starts a control node 27 | Control(super::control::Args), 28 | /// Starts a node 29 | Node(super::node::Args), 30 | /// Login to Lunatic cloud 31 | Login(super::login::Args), 32 | /// Manage lunatic applications 33 | App(super::app::Args), 34 | /// Deploy Lunatic app to cloud 35 | Deploy, 36 | } 37 | 38 | pub(crate) async fn execute(augmented_args: Option>) -> Result<()> { 39 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); 40 | 41 | let args = match augmented_args { 42 | Some(a) => Args::parse_from(a), 43 | None => Args::parse(), 44 | }; 45 | 46 | match args.command { 47 | Commands::Init => super::init::start(), 48 | Commands::Run(a) => super::run::start(a).await, 49 | Commands::Control(a) => super::control::start(a).await, 50 | Commands::Node(a) => super::node::start(a).await, 51 | Commands::Login(a) => super::login::start(a).await, 52 | Commands::App(a) => super::app::start(a).await, 53 | Commands::Deploy => super::deploy::start().await, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/mode/init.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{create_dir_all, OpenOptions}, 3 | io::{Read, Seek, Write}, 4 | path::Path, 5 | }; 6 | 7 | use anyhow::{anyhow, Result}; 8 | use toml::{value::Table, Value}; 9 | 10 | pub(crate) fn start() -> Result<()> { 11 | // Check if the current directory is a Rust cargo project. 12 | if !Path::new("Cargo.toml").exists() { 13 | return Err(anyhow!("Must be called inside a cargo project")); 14 | } 15 | 16 | // Open or create cargo config file. 17 | create_dir_all(".cargo").unwrap(); 18 | let mut config_toml = OpenOptions::new() 19 | .read(true) 20 | .write(true) 21 | .create(true) 22 | .open(".cargo/config.toml") 23 | .unwrap(); 24 | 25 | let mut content = String::new(); 26 | config_toml.read_to_string(&mut content).unwrap(); 27 | 28 | let mut content = content.parse::().unwrap(); 29 | let table = content 30 | .as_table_mut() 31 | .expect("wrong .cargo/config.toml` format"); 32 | 33 | // Set correct target 34 | match table.get_mut("build") { 35 | Some(value) => { 36 | let build = value 37 | .as_table_mut() 38 | .expect("wrong `.cargo/config.toml` format"); 39 | match build.get_mut("target") { 40 | Some(target) 41 | if target.as_str().expect("wrong `.cargo/config.toml` format") 42 | != "wasm32-wasi" => 43 | { 44 | return Err( 45 | anyhow!("value `build.target` inside `.cargo/config.toml` already set to something else than `wasm32-wasi`") 46 | ); 47 | } 48 | None => { 49 | // If value is missing, add it. 50 | build.insert("target".to_owned(), Value::String("wasm32-wasi".to_owned())); 51 | } 52 | _ => { 53 | // If correct value is set don't do anything. 54 | } 55 | } 56 | } 57 | None => { 58 | let mut new_build = Table::new(); 59 | new_build.insert("target".to_owned(), Value::String("wasm32-wasi".to_owned())); 60 | table.insert("build".to_owned(), Value::Table(new_build)); 61 | } 62 | }; 63 | 64 | // Set correct runner 65 | match table.get_mut("target") { 66 | Some(value) => { 67 | let target = value 68 | .as_table_mut() 69 | .expect("wrong `.cargo/config.toml` format"); 70 | match target.get_mut("wasm32-wasi") { 71 | Some(value) => { 72 | let target = value 73 | .as_table_mut() 74 | .expect("wrong `.cargo/config.toml` format"); 75 | match target.get_mut("runner") { 76 | Some(runner) 77 | if runner.as_str().expect("wrong `.cargo/config.toml` format") 78 | == "lunatic" => 79 | { 80 | // Update old runner to new one 81 | target.insert( 82 | "runner".to_owned(), 83 | Value::String("lunatic run".to_owned()), 84 | ); 85 | } 86 | Some(runner) 87 | if runner.as_str().expect("wrong `.cargo/config.toml` format") 88 | != "lunatic run" => 89 | { 90 | return Err( 91 | anyhow!("value `target.wasm32-wasi.runner` inside `.cargo/config.toml` already set to something else than `lunatic run`") 92 | ); 93 | } 94 | None => { 95 | // If value is missing, add it. 96 | target.insert( 97 | "runner".to_owned(), 98 | Value::String("lunatic run".to_owned()), 99 | ); 100 | } 101 | _ => { 102 | // If correct value is set don't do anything. 103 | } 104 | } 105 | } 106 | None => { 107 | // Create sub-table `wasm32-wasi` with runner set. 108 | let mut new_wasm32_wasi = Table::new(); 109 | new_wasm32_wasi 110 | .insert("runner".to_owned(), Value::String("lunatic run".to_owned())); 111 | target.insert("wasm32-wasi".to_owned(), Value::Table(new_wasm32_wasi)); 112 | } 113 | } 114 | } 115 | None => { 116 | // Create sub-table `wasm32-wasi` with runner set. 117 | let mut new_wasm32_wasi = Table::new(); 118 | new_wasm32_wasi.insert("runner".to_owned(), Value::String("lunatic run".to_owned())); 119 | // Create table `target` with value `wasm32-wasi`. 120 | let mut new_target = Table::new(); 121 | new_target.insert("wasm32-wasi".to_owned(), Value::Table(new_wasm32_wasi)); 122 | table.insert("target".to_owned(), Value::Table(new_target)); 123 | } 124 | }; 125 | 126 | let new_config = toml::to_string(table).unwrap(); 127 | // Truncate existing config 128 | config_toml.set_len(0).unwrap(); 129 | config_toml.rewind().unwrap(); 130 | config_toml 131 | .write_all(new_config.as_bytes()) 132 | .expect("unable to write new config to `.cargo/config.toml`"); 133 | 134 | println!("Cargo project initialized!"); 135 | 136 | Ok(()) 137 | } 138 | -------------------------------------------------------------------------------- /src/mode/login.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{anyhow, Context, Result}; 4 | use clap::Parser; 5 | use log::debug; 6 | use reqwest::StatusCode; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::mode::config::{ConfigManager, Provider}; 10 | 11 | #[derive(Parser, Debug)] 12 | pub struct Args { 13 | #[clap(short, long)] 14 | provider: Option, 15 | } 16 | 17 | #[derive(Debug, Serialize, Deserialize)] 18 | pub struct CliLoginResponse { 19 | pub login_id: String, 20 | } 21 | 22 | #[derive(Clone, Debug, Serialize, Deserialize)] 23 | pub struct CliLogin { 24 | pub app_id: String, 25 | } 26 | 27 | static CTRL_URL: &str = "https://lunatic.cloud"; 28 | 29 | pub(crate) async fn start(args: Args) -> Result<()> { 30 | let mut config_manager = ConfigManager::new().unwrap(); 31 | let provider = args.provider.unwrap_or_else(|| CTRL_URL.to_string()); 32 | 33 | match config_manager.global_config.provider { 34 | Some(_) => { 35 | if is_authenticated(&mut config_manager).await? { 36 | println!("\n\nYou are already authenticated.\n\n"); 37 | Ok(()) 38 | } else { 39 | refresh_existing_login(&mut config_manager).await 40 | } 41 | } 42 | None => new_login(provider, &mut config_manager).await, 43 | } 44 | } 45 | 46 | async fn check_auth_status(status_url: &str, client: &reqwest::Client) -> Vec { 47 | loop { 48 | match client.get(status_url).send().await { 49 | Ok(res) => { 50 | if res.status() == StatusCode::OK { 51 | return res 52 | .headers() 53 | .get_all("set-cookie") 54 | .into_iter() 55 | .map(|header| { 56 | header 57 | .to_str() 58 | .expect("Failed to get Cookie value") 59 | .to_string() 60 | }) 61 | .collect(); 62 | } 63 | if [StatusCode::UNAUTHORIZED, StatusCode::FORBIDDEN].contains(&res.status()) { 64 | debug!("Retrying in 5 seconds"); 65 | tokio::time::sleep(Duration::from_secs(5)).await; 66 | continue; 67 | } 68 | panic!("Something went wrong {:?}", res); 69 | } 70 | Err(e) => { 71 | // code 401 means the app is still unauthorized and needs to try later 72 | if let Some(StatusCode::FORBIDDEN) = e.status() { 73 | debug!("Retrying in 5 seconds"); 74 | tokio::time::sleep(Duration::from_secs(5)).await; 75 | continue; 76 | } 77 | // something must have gone with either the request or the users connection 78 | panic!("Connection error {:?}", e); 79 | } 80 | } 81 | } 82 | } 83 | 84 | async fn new_login(provider: String, config_manager: &mut ConfigManager) -> Result<()> { 85 | let client = reqwest::Client::new(); 86 | let res = client 87 | .post(format!("{provider}/api/cli/login")) 88 | .json(&CliLogin { 89 | app_id: config_manager.get_app_id(), 90 | }) 91 | .send() 92 | .await 93 | .with_context(|| "Error sending HTTP login request.")?; 94 | let status = res.status(); 95 | if !status.is_success() { 96 | let body = res.text().await.with_context(|| { 97 | format!("Error parsing body as text. Response not successful: {status}") 98 | })?; 99 | Err(anyhow!( 100 | "HTTP login request returned an error reponse: {body}" 101 | )) 102 | } else { 103 | let login = res 104 | .json::() 105 | .await 106 | .with_context(|| "Error parsing the login request JSON.")?; 107 | 108 | let login_id = 109 | url::form_urlencoded::byte_serialize(login.login_id.as_bytes()).collect::(); 110 | let app_id = url::form_urlencoded::byte_serialize(config_manager.get_app_id().as_bytes()) 111 | .collect::(); 112 | println!("\n\nPlease visit the following URL to authenticate this cli app {provider}/cli/authenticate/{app_id}?login_id={login_id}\n\n"); 113 | 114 | let status_url = format!("{provider}/api/cli/login/{}", login.login_id); 115 | let auth_status = check_auth_status(&status_url, &client).await; 116 | 117 | if auth_status.is_empty() { 118 | Err(anyhow!("Cli Login failed")) 119 | } else { 120 | config_manager.login(Provider { 121 | name: provider, 122 | cookies: auth_status, 123 | login_id, 124 | }); 125 | config_manager.flush()?; 126 | Ok(()) 127 | } 128 | } 129 | } 130 | 131 | async fn is_authenticated(config_manager: &mut ConfigManager) -> Result { 132 | let provider = config_manager 133 | .global_config 134 | .provider 135 | .as_ref() 136 | .ok_or_else(|| anyhow::anyhow!("Unexpected missing provider in `lunatic.toml`"))?; 137 | let client = reqwest::Client::new(); 138 | let response = client 139 | .get( 140 | provider 141 | .get_url()? 142 | .join(&format!("api/cli/login/{}", provider.login_id))?, 143 | ) 144 | .send() 145 | .await?; 146 | if response.status() == StatusCode::OK { 147 | return Ok(true); 148 | } 149 | if response.status() == StatusCode::UNAUTHORIZED { 150 | return Ok(false); 151 | } 152 | let response = response.error_for_status()?; 153 | let body = response.text().await?; 154 | Err(anyhow!("Unexpected login API response: {body}")) 155 | } 156 | 157 | async fn refresh_existing_login(config_manager: &mut ConfigManager) -> Result<()> { 158 | let root = config_manager 159 | .global_config 160 | .provider 161 | .as_ref() 162 | .ok_or_else(|| anyhow::anyhow!("Unexpected missing provider in `lunatic.toml`"))? 163 | .name 164 | .clone(); 165 | let login_id = config_manager 166 | .global_config 167 | .provider 168 | .as_ref() 169 | .ok_or_else(|| anyhow::anyhow!("Unexpected missing provider in `lunatic.toml`"))? 170 | .login_id 171 | .clone(); 172 | let app_id = &config_manager.global_config.cli_app_id; 173 | println!("\n\nPlease visit the following URL to authenticate this cli app {root}/cli/refresh/{app_id}?login_id={login_id}\n\n"); 174 | 175 | let client = reqwest::Client::new(); 176 | let status_url = format!("{root}/api/cli/login/{}", login_id); 177 | let auth_status = check_auth_status(&status_url, &client).await; 178 | 179 | if auth_status.is_empty() { 180 | return Err(anyhow!("Cli Login failed")); 181 | } 182 | 183 | config_manager.login(Provider { 184 | name: root, 185 | cookies: auth_status, 186 | login_id, 187 | }); 188 | 189 | config_manager.flush()?; 190 | Ok(()) 191 | } 192 | -------------------------------------------------------------------------------- /src/mode/mod.rs: -------------------------------------------------------------------------------- 1 | //! Depending on the environment that the `lunatic` binary is invoked from, it may behave 2 | //! differently. All the different modes of working are defined in this module. 3 | 4 | // If invoked as part of a `cargo test` command. 5 | pub(crate) mod cargo_test; 6 | // Default mode, if no other mode could be detected. 7 | pub(crate) mod execution; 8 | 9 | mod app; 10 | mod common; 11 | mod config; 12 | mod control; 13 | mod deploy; 14 | mod init; 15 | mod login; 16 | mod node; 17 | mod run; 18 | -------------------------------------------------------------------------------- /src/mode/node.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | net::{SocketAddr, UdpSocket}, 4 | path::PathBuf, 5 | }; 6 | 7 | use clap::Parser; 8 | 9 | use std::{collections::HashMap, sync::Arc}; 10 | 11 | use anyhow::{anyhow, Context, Result}; 12 | use lunatic_distributed::{ 13 | control::{self}, 14 | distributed::{self, server::ServerCtx}, 15 | quic, 16 | }; 17 | use lunatic_process::{ 18 | env::{Environments, LunaticEnvironments}, 19 | runtimes::{self, Modules}, 20 | }; 21 | use lunatic_runtime::DefaultProcessState; 22 | use uuid::Uuid; 23 | 24 | use crate::mode::common::{run_wasm, RunWasm}; 25 | 26 | #[derive(Parser, Debug)] 27 | pub(crate) struct Args { 28 | /// Control server register URL 29 | #[arg( 30 | index = 1, 31 | value_name = "CONTROL_URL", 32 | default_value = "http://127.0.0.1:3030/" 33 | )] 34 | control: String, 35 | 36 | #[arg(long, value_name = "NODE_SOCKET")] 37 | bind_socket: Option, 38 | 39 | #[arg(long, value_name = "WASM_MODULE")] 40 | wasm: Option, 41 | 42 | /// Define key=value variable to store as node information 43 | #[arg(long, value_parser = parse_key_val, action = clap::ArgAction::Append)] 44 | tag: Vec<(String, String)>, 45 | 46 | #[cfg(feature = "prometheus")] 47 | #[command(flatten)] 48 | prometheus: super::common::PrometheusArgs, 49 | } 50 | 51 | pub(crate) async fn start(args: Args) -> Result<()> { 52 | #[cfg(feature = "prometheus")] 53 | if args.prometheus.prometheus { 54 | super::common::prometheus(args.prometheus.prometheus_http, None)?; 55 | } 56 | 57 | let socket = args 58 | .bind_socket 59 | .or_else(get_available_localhost) 60 | .ok_or_else(|| anyhow!("No available localhost UDP port"))?; 61 | let http_client = reqwest::Client::new(); 62 | 63 | // TODO unwrap, better message 64 | let node_name = Uuid::new_v4(); 65 | let node_name_str = node_name.as_hyphenated().to_string(); 66 | let node_attributes: HashMap = args.tag.clone().into_iter().collect(); 67 | let node_cert = lunatic_distributed::distributed::server::gen_node_cert(&node_name_str) 68 | .with_context(|| "Failed to generate node CSR and PK")?; 69 | log::info!("Generate CSR for node name {node_name_str}"); 70 | 71 | let reg = control::Client::register( 72 | &http_client, 73 | args.control 74 | .parse() 75 | .with_context(|| "Parsing control URL")?, 76 | node_name, 77 | node_cert.serialize_request_pem()?, 78 | ) 79 | .await?; 80 | 81 | let allowed_envs = if reg.is_privileged { 82 | None 83 | } else { 84 | Some( 85 | reg.envs 86 | .iter() 87 | .map(|env_id| *env_id as u64) 88 | .collect::>(), 89 | ) 90 | }; 91 | 92 | let control_client = 93 | control::Client::new(http_client.clone(), reg.clone(), socket, node_attributes).await?; 94 | 95 | let node_id = control_client.node_id(); 96 | 97 | log::info!("Registration successful, node id {}", node_id); 98 | 99 | let quic_client = quic::new_quic_client( 100 | ®.root_cert, 101 | reg.cert_pem_chain 102 | .get(0) 103 | .ok_or_else(|| anyhow!("No certificate available for QUIC client"))?, 104 | &node_cert.serialize_private_key_pem(), 105 | ) 106 | .with_context(|| "Failed to create mTLS QUIC client")?; 107 | 108 | let distributed_client = 109 | distributed::Client::new(node_id, control_client.clone(), quic_client.clone()); 110 | 111 | let dist = lunatic_distributed::DistributedProcessState::new( 112 | node_id, 113 | control_client.clone(), 114 | distributed_client.clone(), 115 | ) 116 | .await?; 117 | 118 | let wasmtime_config = runtimes::wasmtime::default_config(); 119 | let runtime = runtimes::wasmtime::WasmtimeRuntime::new(&wasmtime_config)?; 120 | let envs = Arc::new(LunaticEnvironments::default()); 121 | 122 | let node = tokio::task::spawn(lunatic_distributed::distributed::server::node_server( 123 | ServerCtx { 124 | envs: envs.clone(), 125 | modules: Modules::::default(), 126 | distributed: dist.clone(), 127 | runtime: runtime.clone(), 128 | node_client: distributed_client.clone(), 129 | allowed_envs, 130 | }, 131 | socket, 132 | reg.root_cert, 133 | reg.cert_pem_chain, 134 | node_cert.serialize_private_key_pem(), 135 | )); 136 | 137 | if args.wasm.is_some() { 138 | let env = envs.create(1).await?; 139 | tokio::task::spawn(async { 140 | if let Err(e) = run_wasm(RunWasm { 141 | path: args.wasm.unwrap(), 142 | wasm_args: vec![], 143 | dir: vec![], 144 | runtime, 145 | envs, 146 | env, 147 | distributed: Some(dist), 148 | }) 149 | .await 150 | { 151 | log::error!("Error running wasm: {e:?}"); 152 | } 153 | }); 154 | } 155 | 156 | let ctrl = control_client.clone(); 157 | tokio::task::spawn(async move { 158 | async_ctrlc::CtrlC::new().unwrap().await; 159 | log::info!("Shutting down node"); 160 | ctrl.notify_node_stopped().await.ok(); 161 | std::process::exit(0); 162 | }); 163 | 164 | node.await.ok(); 165 | 166 | control_client.notify_node_stopped().await.ok(); 167 | 168 | Ok(()) 169 | } 170 | 171 | fn get_available_localhost() -> Option { 172 | for port in 1025..65535u16 { 173 | let addr = SocketAddr::new("127.0.0.1".parse().unwrap(), port); 174 | if UdpSocket::bind(addr).is_ok() { 175 | return Some(addr); 176 | } 177 | } 178 | 179 | None 180 | } 181 | 182 | fn parse_key_val(s: &str) -> Result<(String, String)> { 183 | if let Some((key, value)) = s.split_once('=') { 184 | Ok((key.to_string(), value.to_string())) 185 | } else { 186 | Err(anyhow!(format!("Tag '{s}' is not formatted as key=value"))) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/mode/run.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::Arc}; 2 | 3 | use anyhow::Result; 4 | use clap::Parser; 5 | use lunatic_process::{ 6 | env::{Environments, LunaticEnvironments}, 7 | runtimes::{self}, 8 | }; 9 | 10 | use super::common::{run_wasm, RunWasm}; 11 | 12 | #[derive(Parser, Debug)] 13 | #[command(version)] 14 | pub struct Args { 15 | /// Grant access to the given host directories 16 | #[arg(long, value_name = "DIRECTORY")] 17 | pub dir: Vec, 18 | 19 | /// Indicate that a benchmark is running 20 | #[arg(long)] 21 | pub bench: bool, 22 | 23 | /// Entry .wasm file 24 | #[arg(index = 1)] 25 | pub path: PathBuf, 26 | 27 | /// Arguments passed to the guest 28 | #[arg(index = 2)] 29 | pub wasm_args: Vec, 30 | 31 | #[cfg(feature = "prometheus")] 32 | #[command(flatten)] 33 | prometheus: super::common::PrometheusArgs, 34 | } 35 | 36 | pub(crate) async fn start(mut args: Args) -> Result<()> { 37 | #[cfg(feature = "prometheus")] 38 | if args.prometheus.prometheus { 39 | super::common::prometheus(args.prometheus.prometheus_http, None)?; 40 | } 41 | 42 | // Create wasmtime runtime 43 | let wasmtime_config = runtimes::wasmtime::default_config(); 44 | let runtime = runtimes::wasmtime::WasmtimeRuntime::new(&wasmtime_config)?; 45 | let envs = Arc::new(LunaticEnvironments::default()); 46 | 47 | let env = envs.create(1).await?; 48 | if args.bench { 49 | args.wasm_args.push("--bench".to_owned()); 50 | } 51 | run_wasm(RunWasm { 52 | path: args.path, 53 | wasm_args: args.wasm_args, 54 | dir: args.dir, 55 | runtime, 56 | envs, 57 | env, 58 | distributed: None, 59 | }) 60 | .await 61 | } 62 | -------------------------------------------------------------------------------- /wat/hello.wat: -------------------------------------------------------------------------------- 1 | ;; This file is used in instantiation benchmarks, as the simplest possible module. 2 | (module (func (export "hello") nop)) --------------------------------------------------------------------------------