Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,11 @@ rustflags = [
"-C", "link-arg=dynamic_lookup",
]

[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = [
"--cfg", "tokio_unstable",
"-C", "link-arg=/STACK:8388608",
]

[env]
LIBSQLITE3_FLAGS = "SQLITE_ENABLE_BATCH_ATOMIC_WRITE"
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ members = [
slog = "2.7"
slog-async = "2.8"
slog-term = "2.9"
socket2 = "0.6"
scopeguard = "1.2"
statrs = "0.18"
subtle = "2"
Expand Down
48 changes: 48 additions & 0 deletions docs-internal/WINDOWS_DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Windows Development

Build and run Rivet natively on Windows, without WSL. Run all commands in PowerShell from the repository root.

## Prerequisites

```powershell
winget install Microsoft.VisualStudio.2022.BuildTools --override "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
winget install Rustlang.Rustup
winget install LLVM.LLVM
winget install Kitware.CMake
winget install NASM.NASM
winget install OpenJS.NodeJS.LTS
```

NASM does not add itself to `PATH`, and `bindgen` needs to find `libclang`:

```powershell
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$env:LOCALAPPDATA\bin\NASM", "User")
[Environment]::SetEnvironmentVariable("LIBCLANG_PATH", "$env:ProgramFiles\LLVM\bin", "User")
```

Open a new terminal so the changes apply.

## Build

```powershell
npm install --global pnpm
pnpm install
pnpm -F @rivetkit/engine-frontend build:engine
cargo build -p rivet-engine
$env:SKIP_WASM_BUILD = "1"
pnpm build -F rivetkit -F @rivetkit/react
```

`build:engine` builds the dashboard that the engine embeds, so build it before the engine. `SKIP_WASM_BUILD` skips the wasm runtime, which this guide does not cover.

## Run

The published Windows engine package does not run yet, so point RivetKit at the locally built engine:

```powershell
$env:RIVET_ENGINE_BINARY_PATH = "$PWD\target\debug\rivet-engine.exe"
cd examples\hello-world
pnpm dev
```

Open http://localhost:5173 for the example and http://127.0.0.1:6420/ui/ for the dashboard.
2 changes: 1 addition & 1 deletion engine/packages/api-peer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub async fn start(config: rivet_config::Config, pools: rivet_pools::Pools) -> R

let router = router::router(config, pools).await?;

let listener = tokio::net::TcpListener::bind(addr).await?;
let listener = rivet_util::net::bind_tcp_listener(addr)?;
tracing::info!(?host, ?port, "api-peer server listening");

axum::serve(listener, router).await?;
Expand Down
4 changes: 2 additions & 2 deletions engine/packages/guard-core/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub async fn run_server(
routing_fn.clone(),
cache_key_fn.clone(),
));
let http_listener = tokio::net::TcpListener::bind(http_addr).await?;
let http_listener = rivet_util::net::bind_tcp_listener(http_addr)?;

// Set up HTTPS server (if configured)
let (https_addr, https_factory, https_listener, https_acceptor) = if let Some(https) =
Expand All @@ -49,7 +49,7 @@ pub async fn run_server(
routing_fn.clone(),
cache_key_fn.clone(),
));
let listener = tokio::net::TcpListener::bind(https_addr).await?;
let listener = rivet_util::net::bind_tcp_listener(https_addr)?;

// Configure TLS if resolver function is provided
let acceptor = if let Some(resolver_fn) = cert_resolver_fn {
Expand Down
1 change: 1 addition & 0 deletions engine/packages/metrics-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ opentelemetry_sdk.workspace = true
rivet-config.workspace = true
rivet-env.workspace = true
rivet-metrics.workspace = true
rivet-util.workspace = true
tokio.workspace = true
tracing.workspace = true
8 changes: 7 additions & 1 deletion engine/packages/metrics-server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::*;
use hyper::{
Body, Request, Response, Server,
header::CONTENT_TYPE,
server::{Builder, conn::AddrIncoming},
service::{make_service_fn, service_fn},
};
use rivet_metrics::prometheus::{Encoder, TextEncoder};
Expand All @@ -14,7 +15,7 @@ pub async fn run_standalone(config: rivet_config::Config) -> Result<()> {
let port = config.metrics.port();
let addr = SocketAddr::from((host, port));

let server = match Server::try_bind(&addr) {
let server = match bind_server(addr) {
Ok(x) => x,
Err(err) => {
tracing::error!(?host, ?port, ?err, "failed to bind metrics server");
Expand All @@ -34,6 +35,11 @@ pub async fn run_standalone(config: rivet_config::Config) -> Result<()> {
Ok(())
}

fn bind_server(addr: SocketAddr) -> Result<Builder<AddrIncoming>> {
let listener = rivet_util::net::bind_std_tcp_listener(addr)?;
Ok(Server::from_tcp(listener)?)
}

#[tracing::instrument(level = "debug", skip_all)]
async fn serve_req(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
let encoder = TextEncoder::new();
Expand Down
3 changes: 2 additions & 1 deletion engine/packages/runner-protocol/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ mod typescript {

let output_path = src_dir.join("index.ts");

let output = Command::new(repo_root.join("node_modules/@bare-ts/tools/dist/bin/cli.js"))
let output = Command::new("node")
.arg(repo_root.join("node_modules/@bare-ts/tools/dist/bin/cli.js"))
.arg("compile")
.arg("--generator")
.arg("ts")
Expand Down
1 change: 1 addition & 0 deletions engine/packages/util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ rivet-tracing-utils.workspace = true
rivet-util-serde.workspace = true
rivet-util-id = { workspace = true, features = ["universaldb"] }
serde.workspace = true
socket2.workspace = true
tokio.workspace = true
tracing.workspace = true
url.workspace = true
Expand Down
1 change: 1 addition & 0 deletions engine/packages/util/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod geo;
pub mod math;
pub mod metric;
pub mod metrics;
pub mod net;
pub mod req;
pub mod serde;
pub mod size;
Expand Down
34 changes: 34 additions & 0 deletions engine/packages/util/src/net.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use std::{io, net::SocketAddr};

use socket2::{Domain, Protocol, Socket, Type};

const LISTEN_BACKLOG: i32 = 1024;

pub fn bind_tcp_listener(addr: SocketAddr) -> io::Result<tokio::net::TcpListener> {
tokio::net::TcpListener::from_std(bind_std_tcp_listener(addr)?)
}

pub fn bind_std_tcp_listener(addr: SocketAddr) -> io::Result<std::net::TcpListener> {
let socket = Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))?;

if addr.is_ipv6() {
socket.set_only_v6(false)?;
}

allow_fast_rebind(&socket)?;
socket.bind(&addr.into())?;
socket.listen(LISTEN_BACKLOG)?;
socket.set_nonblocking(true)?;

Ok(socket.into())
}

#[cfg(unix)]
fn allow_fast_rebind(socket: &Socket) -> io::Result<()> {
socket.set_reuse_address(true)
}

#[cfg(not(unix))]
fn allow_fast_rebind(_socket: &Socket) -> io::Result<()> {
Ok(())
}
24 changes: 24 additions & 0 deletions engine/packages/util/tests/net.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};

use rivet_util::net::bind_tcp_listener;
use tokio::net::TcpStream;

#[tokio::test]
async fn ipv6_unspecified_listener_accepts_ipv4_loopback_connections() {
assert_accepts_ipv4_loopback_connection(Ipv6Addr::UNSPECIFIED.into()).await;
}

#[tokio::test]
async fn ipv4_listener_accepts_ipv4_loopback_connections() {
assert_accepts_ipv4_loopback_connection(Ipv4Addr::LOCALHOST.into()).await;
}

async fn assert_accepts_ipv4_loopback_connection(listen_ip: IpAddr) {
let listener = bind_tcp_listener(SocketAddr::new(listen_ip, 0)).expect("bind listener");
let port = listener.local_addr().expect("listener address").port();

TcpStream::connect((Ipv4Addr::LOCALHOST, port))
.await
.expect("connect over ipv4 loopback");
listener.accept().await.expect("accept ipv4 connection");
}
3 changes: 2 additions & 1 deletion engine/sdks/rust/actor-runtime-socket-protocol/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ mod typescript {
.and_then(|stem| stem.to_str())
.expect("schema has valid UTF-8 file stem");
let output_path = output_dir.join(format!("{version}.ts"));
let output = Command::new(&cli_js_path)
let output = Command::new("node")
.arg(&cli_js_path)
.arg("compile")
.arg("--generator")
.arg("ts")
Expand Down
29 changes: 15 additions & 14 deletions engine/sdks/rust/envoy-protocol/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,21 @@ mod typescript {

let output_path = src_dir.join("index.ts");

let output = Command::new(
workspace_root
.parent()
.unwrap()
.join("node_modules/@bare-ts/tools/dist/bin/cli.js"),
)
.arg("compile")
.arg("--generator")
.arg("ts")
.arg(highest_version_path)
.arg("-o")
.arg(&output_path)
.output()
.expect("Failed to execute bare compiler for TypeScript");
let output = Command::new("node")
.arg(
workspace_root
.parent()
.unwrap()
.join("node_modules/@bare-ts/tools/dist/bin/cli.js"),
)
.arg("compile")
.arg("--generator")
.arg("ts")
.arg(highest_version_path)
.arg("-o")
.arg(&output_path)
.output()
.expect("Failed to execute bare compiler for TypeScript");

if !output.status.success() {
panic!(
Expand Down
8 changes: 4 additions & 4 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
"dev": "vite",
"dev:inspector": "vite build --config vite.inspector-ui.config.ts --watch",
"check-types": "tsc --noEmit",
"build": "NODE_OPTIONS='--max-old-space-size=8192' vite build --mode=production",
"build:engine": "BASE_URL=/ui/ NODE_OPTIONS='--max-old-space-size=8192' vite build --mode=production",
"build": "node scripts/vite-build.mjs",
"build:engine": "node scripts/vite-build.mjs --engine",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"dev:ladle": "ladle dev",
"build:inspector": "NODE_OPTIONS='--max-old-space-size=8192' vite build --config vite.inspector.config.ts --mode=production",
"build:inspector-ui": "NODE_OPTIONS='--max-old-space-size=8192' vite build --config vite.inspector-ui.config.ts --mode=production && node scripts/generate-inspector-tab-css.mjs",
"build:inspector": "node scripts/vite-build.mjs --config vite.inspector.config.ts",
"build:inspector-ui": "node scripts/vite-build.mjs --config vite.inspector-ui.config.ts && node scripts/generate-inspector-tab-css.mjs",
"build:inspector-tab-css": "node scripts/generate-inspector-tab-css.mjs",
"build:ladle": "ladle build"
},
Expand Down
49 changes: 49 additions & 0 deletions frontend/scripts/vite-build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";

const require = createRequire(import.meta.url);

const BUILD_HEAP_LIMIT_MB = 8192;
const ENGINE_BUILD_DEFAULTS = {
BASE_URL: "/ui/",
VITE_APP_API_URL: "__SAME__",
VITE_FEATURE_FLAGS: "",
};

const args = process.argv.slice(2);
const isEngineBuild = args.includes("--engine");
const viteArgs = args.filter((arg) => arg !== "--engine");

function resolveViteBin() {
const packageJsonPath = require.resolve("vite/package.json");
const { bin } = require(packageJsonPath);
return join(dirname(packageJsonPath), bin.vite);
}

function engineBuildEnv() {
return {
...process.env,
BASE_URL: ENGINE_BUILD_DEFAULTS.BASE_URL,
VITE_APP_API_URL:
process.env.VITE_APP_API_URL ?? ENGINE_BUILD_DEFAULTS.VITE_APP_API_URL,
VITE_FEATURE_FLAGS:
process.env.VITE_FEATURE_FLAGS ?? ENGINE_BUILD_DEFAULTS.VITE_FEATURE_FLAGS,
};
}

execFileSync(
process.execPath,
[
`--max-old-space-size=${BUILD_HEAP_LIMIT_MB}`,
resolveViteBin(),
"build",
"--mode=production",
...viteArgs,
],
{
stdio: "inherit",
env: isEngineBuild ? engineBuildEnv() : process.env,
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"code": "unsupported",
"group": "actor_runtime_socket",
"message": "Actor Runtime Socket is only available on Unix native runtimes."
}
}
3 changes: 2 additions & 1 deletion rivetkit-rust/packages/client-protocol/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ mod typescript {
.expect("schema has valid UTF-8 file stem");
let output_path = output_dir.join(format!("{version}.ts"));

let output = Command::new(&cli_js_path)
let output = Command::new("node")
.arg(&cli_js_path)
.arg("compile")
.arg("--generator")
.arg("ts")
Expand Down
11 changes: 11 additions & 0 deletions rivetkit-rust/packages/engine-process/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const ENGINE_VERSION_ENV: &str = "RIVETKIT_ENGINE_VERSION";
const RELEASES_ENDPOINT_ENV: &str = "RIVETKIT_ENGINE_RELEASES_ENDPOINT";
const RELEASES_ENDPOINT: &str = "https://releases.rivet.dev";
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(60);
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;

#[derive(Debug, Deserialize)]
struct EngineHealthResponse {
Expand Down Expand Up @@ -116,6 +118,8 @@ pub enum EngineStartup {
/// `Child` does not send SIGKILL on drop.
/// - Stdout and stderr are routed to log files at spawn time so the engine's
/// write fds remain valid after the host's pipes close.
/// - The engine runs in its own process group on Unix and with its own hidden
/// console on Windows, so terminal signals aimed at the host skip it.
/// - On startup we probe the configured endpoint and reuse a healthy engine
/// instead of spawning a duplicate.
///
Expand Down Expand Up @@ -226,6 +230,13 @@ impl EngineProcessManager {
#[cfg(unix)]
command.process_group(0);

// Windows gives a console program started by a windowed host, such as
// Electron, a visible console window of its own. A hidden console avoids
// that window, and like the Unix process group above it keeps Ctrl+C in
// the host's terminal from reaching the engine.
#[cfg(windows)]
command.creation_flags(CREATE_NO_WINDOW);

let mut child = command
.spawn()
.with_context(|| format!("spawn engine binary `{}`", binary_path.display()))?;
Expand Down
Loading