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
18 changes: 18 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ path = "src/helloworld/server.rs"
name = "helloworld-client"
path = "src/helloworld/client.rs"

[[bin]]
name = "socket-activation-tcp-server"
path = "src/socket_activation/tcp_server.rs"

[[bin]]
name = "socket-activation-tcp-client"
path = "src/socket_activation/tcp_client.rs"

[[bin]]
name = "socket-activation-uds-server"
path = "src/socket_activation/uds_server.rs"
required-features = ["uds"]

[[bin]]
name = "socket-activation-uds-client"
path = "src/socket_activation/uds_client.rs"
required-features = ["uds"]

[[bin]]
name = "blocking-server"
path = "src/blocking/server.rs"
Expand Down
17 changes: 17 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,23 @@ The autoload example requires the following crates installed globally:
* [systemfd](https://crates.io/crates/systemfd)
* [cargo-watch](https://crates.io/crates/cargo-watch)

## Socket Activation (systemd)

The servers adopt a listening socket passed by a socket-activation manager (via
`LISTEN_FDS`/`LISTEN_PID`), or bind directly otherwise.

### TCP

```bash
$ ./src/socket_activation/test_tcp.sh
```

### Unix domain socket

```bash
$ ./src/socket_activation/test_uds.sh
```

## Richer Error

Both clients and both servers do the same thing, but using the two different
Expand Down
11 changes: 11 additions & 0 deletions examples/src/socket_activation/systemd/socketact-tcp.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Service unit for the tonic TCP socket-activation example.
#
# Replace ExecStart with the absolute path to your built binary, e.g.
# /path/to/target/debug/socket-activation-tcp-server
[Unit]
Description=tonic socket-activation TCP example service
Requires=socketact-tcp.socket
After=socketact-tcp.socket

[Service]
ExecStart=/path/to/target/debug/socket-activation-tcp-server
9 changes: 9 additions & 0 deletions examples/src/socket_activation/systemd/socketact-tcp.socket
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Socket unit for the tonic TCP socket-activation example.
[Unit]
Description=tonic socket-activation TCP example socket

[Socket]
ListenStream=[::1]:50051

[Install]
WantedBy=sockets.target
13 changes: 13 additions & 0 deletions examples/src/socket_activation/systemd/socketact-uds.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Service unit for the tonic socket-activation UDS example.
#
# Replace ExecStart with the absolute path to your built binary, e.g.
# /path/to/target/debug/socket-activation-uds-server
[Unit]
Description=tonic socket-activation UDS example service
Requires=socketact-uds.socket
After=socketact-uds.socket

[Service]
ExecStart=/path/to/target/debug/socket-activation-uds-server
# Must match ListenStream in socketact-uds.socket.
Environment=TONIC_UDS_PATH=%t/tonic-sockact.sock
9 changes: 9 additions & 0 deletions examples/src/socket_activation/systemd/socketact-uds.socket
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Socket unit for the tonic socket-activation UDS example.
[Unit]
Description=tonic socket-activation UDS example socket

[Socket]
ListenStream=%t/tonic-sockact.sock

[Install]
WantedBy=sockets.target
23 changes: 23 additions & 0 deletions examples/src/socket_activation/tcp_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! Client for the socket-activation example, connecting over a TCP socket.

use hello_world::HelloRequest;
use hello_world::greeter_client::GreeterClient;

pub mod hello_world {
tonic::include_proto!("helloworld");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = GreeterClient::connect("http://[::1]:50051").await?;

let request = tonic::Request::new(HelloRequest {
name: "Tonic".into(),
});

let response = client.say_hello(request).await?;

println!("RESPONSE={response:?}");

Ok(())
}
49 changes: 49 additions & 0 deletions examples/src/socket_activation/tcp_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! Greeter server demonstrating systemd socket activation over a TCP socket.

use tonic::{Request, Response, Status, transport::Server};

use hello_world::greeter_server::{Greeter, GreeterServer};
use hello_world::{HelloReply, HelloRequest};

pub mod hello_world {
tonic::include_proto!("helloworld");
}

#[derive(Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
async fn say_hello(
&self,
request: Request<HelloRequest>,
) -> Result<Response<HelloReply>, Status> {
println!("Got a request from {:?}", request.remote_addr());

let reply = hello_world::HelloReply {
message: format!("Hello {}!", request.into_inner().name),
};
Ok(Response::new(reply))
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse().unwrap();
let greeter = MyGreeter::default();

if std::env::var_os("LISTEN_FDS").is_some() {
println!(
"socket activation detected: adopting socket-activated descriptor for {addr} if available"
);
} else {
println!("no socket activation: binding {addr} directly");
}

Server::builder()
.add_service(GreeterServer::new(greeter))
.serve(addr)
.await?;

Ok(())
}
90 changes: 90 additions & 0 deletions examples/src/socket_activation/test_tcp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Smoke test for the tonic systemd socket-activation example over TCP.

set -u

ADDR="[::1]:50051"
UNIT_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
SOCKET_UNIT="socketact-tcp.socket"
SERVICE_UNIT="socketact-tcp.service"

clean() {
echo "Cleaning..."
systemctl --user stop "${SOCKET_UNIT}" 2>/dev/null
systemctl --user stop "${SERVICE_UNIT}" 2>/dev/null
rm -f "${UNIT_DIR}/${SOCKET_UNIT}" "${UNIT_DIR}/${SERVICE_UNIT}"
systemctl --user daemon-reload 2>/dev/null
}

fail() {
clean
echo "FAIL: $*" >&2
exit 1
}

pass() {
echo "SUCCESS: $1"
}

command -v systemctl >/dev/null 2>&1 \
|| fail "systemctl not found (install systemd)"
systemctl --user show-environment >/dev/null 2>&1 \
|| fail "no systemd user instance available (needs a user session bus)"

echo "Building example binaries..."
cargo build --bin socket-activation-tcp-server --bin socket-activation-tcp-client \
|| fail "Failed to build example binaries"

SERVER_BIN="$(cargo metadata --format-version 1 --no-deps \
| grep -o '"target_directory":"[^"]*"' | head -n1 | cut -d'"' -f4)/debug/socket-activation-tcp-server"
[[ -x "${SERVER_BIN}" ]] || fail "Server binary not found at ${SERVER_BIN}"

# Install user units. systemd owns the listening socket; the service inherits it
# as fd 3 when it is activated on the first client connection.
echo "Installing user units into ${UNIT_DIR}..."
mkdir -p "${UNIT_DIR}"

cat > "${UNIT_DIR}/${SOCKET_UNIT}" <<EOF
[Unit]
Description=tonic socket-activation TCP example socket

[Socket]
ListenStream=${ADDR}
ReusePort=true

[Install]
WantedBy=sockets.target
EOF

cat > "${UNIT_DIR}/${SERVICE_UNIT}" <<EOF
[Unit]
Description=tonic socket-activation TCP example service

[Service]
ExecStart=${SERVER_BIN}
EOF

systemctl --user daemon-reload || fail "daemon-reload failed"
systemctl --user start "${SOCKET_UNIT}" || fail "Failed to start ${SOCKET_UNIT}"

# The socket is now listening but the server has NOT started yet: on-demand
# activation means the service stays inactive until the first connection.
[[ "$(systemctl --user is-active "${SOCKET_UNIT}")" == "active" ]] \
|| fail "${SOCKET_UNIT} did not become active"
[[ "$(systemctl --user is-active "${SERVICE_UNIT}")" == "inactive" ]] \
|| fail "${SERVICE_UNIT} should be inactive before the first connection"
echo "Socket active, service still inactive."

echo "Running client (this triggers activation)..."
OUTPUT="$(cargo run --bin socket-activation-tcp-client 2>/dev/null)" \
|| fail "Client failed to run"

echo "${OUTPUT}" | grep -q "Hello" || fail "Response not received (got: ${OUTPUT})"

# The connection should have activated the service.
[[ "$(systemctl --user is-active "${SERVICE_UNIT}")" == "active" ]] \
|| fail "${SERVICE_UNIT} should be active after the client connected"
echo "Service activated on demand by the client connection."

pass "Response received: ${OUTPUT}"
clean
97 changes: 97 additions & 0 deletions examples/src/socket_activation/test_uds.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Smoke test for the tonic systemd socket-activation example over a Unix domain
# socket.

set -u

RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp}"
SOCK="${RUNTIME_DIR}/tonic-sockact.sock"
export TONIC_UDS_PATH="${SOCK}"
UNIT_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
SOCKET_UNIT="socketact-uds.socket"
SERVICE_UNIT="socketact-uds.service"

clean() {
echo "Cleaning..."
systemctl --user stop "${SOCKET_UNIT}" 2>/dev/null
systemctl --user stop "${SERVICE_UNIT}" 2>/dev/null
rm -f "${UNIT_DIR}/${SOCKET_UNIT}" "${UNIT_DIR}/${SERVICE_UNIT}"
systemctl --user daemon-reload 2>/dev/null
rm -f "${SOCK}"
}

fail() {
clean
echo "FAIL: $*" >&2
exit 1
}

pass() {
echo "SUCCESS: $1"
}

command -v systemctl >/dev/null 2>&1 \
|| fail "systemctl not found (install systemd)"
systemctl --user show-environment >/dev/null 2>&1 \
|| fail "no systemd user instance available (needs a user session bus)"

echo "Building example binaries..."
cargo build --bin socket-activation-uds-server --bin socket-activation-uds-client \
|| fail "Failed to build example binaries"

SERVER_BIN="$(cargo metadata --format-version 1 --no-deps \
| grep -o '"target_directory":"[^"]*"' | head -n1 | cut -d'"' -f4)/debug/socket-activation-uds-server"
[[ -x "${SERVER_BIN}" ]] || fail "Server binary not found at ${SERVER_BIN}"

# Install user units. systemd owns the listening socket; the service inherits it
# as fd 3 when it is activated on the first client connection. TONIC_UDS_PATH
# tells the server which path the descriptor corresponds to.
echo "Installing user units into ${UNIT_DIR}..."
mkdir -p "${UNIT_DIR}"
rm -f "${SOCK}"

cat > "${UNIT_DIR}/${SOCKET_UNIT}" <<EOF
[Unit]
Description=tonic socket-activation UDS example socket

[Socket]
ListenStream=${SOCK}

[Install]
WantedBy=sockets.target
EOF

cat > "${UNIT_DIR}/${SERVICE_UNIT}" <<EOF
[Unit]
Description=tonic socket-activation UDS example service

[Service]
ExecStart=${SERVER_BIN}
Environment=TONIC_UDS_PATH=${SOCK}
EOF

systemctl --user daemon-reload || fail "daemon-reload failed"
systemctl --user start "${SOCKET_UNIT}" || fail "Failed to start ${SOCKET_UNIT}"

# The socket is now listening but the server has NOT started yet: on-demand
# activation means the service stays inactive until the first connection.
[[ -S "${SOCK}" ]] || fail "Socket ${SOCK} was not created"
[[ "$(systemctl --user is-active "${SOCKET_UNIT}")" == "active" ]] \
|| fail "${SOCKET_UNIT} did not become active"
[[ "$(systemctl --user is-active "${SERVICE_UNIT}")" == "inactive" ]] \
|| fail "${SERVICE_UNIT} should be inactive before the first connection"
echo "Socket active, service still inactive."

echo "Running client (this triggers activation)..."
OUTPUT="$(cargo run --bin socket-activation-uds-client 2>/dev/null)" \
|| fail "Client failed to run"

echo "${OUTPUT}" | grep -q "Hello" || fail "Response not received (got: ${OUTPUT})"

# The connection should have activated the service.
[[ "$(systemctl --user is-active "${SERVICE_UNIT}")" == "active" ]] \
|| fail "${SERVICE_UNIT} should be active after the client connected"
echo "Service activated on demand by the client connection."

pass "Response received: ${OUTPUT}"
clean
Loading
Loading