Sprint 0: Project Setup
Goal: Establish project structure, build system, and minimal daemon that can start and stop cleanly.
Objectives
- Initialize Rust workspace with two crates (gardmd, gardm-greeter)
- Set up shared library for IPC protocol
- Create systemd service file
- Establish logging infrastructure
- Basic daemon lifecycle (start, signal handling, shutdown)
Tasks
0.1 Initialize Cargo Workspace
gardm/
├── Cargo.toml # Workspace root
├── gardmd/
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # Daemon entry point
│ └── lib.rs
├── gardm-greeter/
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # Greeter entry point
│ └── lib.rs
└── gardm-ipc/
├── Cargo.toml
└── src/
└── lib.rs # Shared IPC types
Root Cargo.toml:
[workspace]
members = ["gardmd", "gardm-greeter", "gardm-ipc"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1.0"
thiserror = "1.0"
0.2 Define IPC Protocol Types
In gardm-ipc/src/lib.rs:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Request {
CreateSession { username: String },
Authenticate { response: String },
StartSession { cmd: Vec<String>, env: Vec<String> },
CancelSession,
Shutdown,
Reboot,
Suspend,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Response {
Success,
AuthPrompt { prompt: String, echo: bool },
AuthInfo { message: String },
AuthError { message: String },
Error { message: String },
}
0.3 Basic Daemon Skeleton
In gardmd/src/main.rs:
use tokio::signal::unix::{signal, SignalKind};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize logging to journald
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_default_env())
.init();
tracing::info!("gardmd starting");
// Signal handling
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
tokio::select! {
_ = sigterm.recv() => {
tracing::info!("Received SIGTERM, shutting down");
}
_ = sigint.recv() => {
tracing::info!("Received SIGINT, shutting down");
}
}
tracing::info!("gardmd stopped");
Ok(())
}
0.4 systemd Service File
Create gardm.service:
[Unit]
Description=gar Display Manager
Documentation=man:gardmd(8)
After=systemd-user-sessions.service getty@tty1.service plymouth-quit.service
Conflicts=getty@tty1.service
[Service]
Type=notify
ExecStart=/usr/bin/gardmd
Restart=always
RestartSec=1
# Security hardening
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
NoNewPrivileges=false # Required for PAM
ReadWritePaths=/run
[Install]
Alias=display-manager.service
0.5 Configuration Scaffolding
Create gardmd/src/config.rs:
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Deserialize)]
pub struct Config {
#[serde(default)]
pub general: GeneralConfig,
#[serde(default)]
pub greeter: GreeterConfig,
}
#[derive(Debug, Deserialize, Default)]
pub struct GeneralConfig {
#[serde(default = "default_session")]
pub default_session: String,
#[serde(default = "default_greeter")]
pub greeter: PathBuf,
#[serde(default)]
pub vt: u32,
}
#[derive(Debug, Deserialize, Default)]
pub struct GreeterConfig {
#[serde(default = "default_blur_radius")]
pub blur_radius: u32,
}
fn default_session() -> String { "gar".to_string() }
fn default_greeter() -> PathBuf { "/usr/bin/gardm-greeter".into() }
fn default_blur_radius() -> u32 { 20 }
impl Config {
pub fn load() -> anyhow::Result<Self> {
let path = PathBuf::from("/etc/gardm/config.toml");
if path.exists() {
let content = std::fs::read_to_string(&path)?;
Ok(toml::from_str(&content)?)
} else {
Ok(Config::default())
}
}
}
Acceptance Criteria
cargo build --releasesucceeds for all workspace membersgardmdstarts, logs to stdout, and exits cleanly on SIGTERM- IPC types serialize/deserialize correctly (unit test)
- Config loads from file or uses defaults
- Service file passes
systemd-analyze verify
Pitfalls to Avoid
- Don't start X11 yet - that's Sprint 2. Keep this sprint minimal.
- Don't implement PAM yet - that's Sprint 1. Focus on structure.
- Avoid premature optimization - get it working first.
- Don't forget
sd_notify- systemd needs to know when daemon is ready. - Test without root first - use
RUST_LOG=debug cargo runfor development.
Testing
# Build
cargo build --release
# Run daemon in foreground (Ctrl+C to stop)
RUST_LOG=debug ./target/release/gardmd
# Verify signal handling
kill -TERM $(pidof gardmd)
# Test config loading
mkdir -p /tmp/gardm-test
echo '[general]' > /tmp/gardm-test/config.toml
echo 'default_session = "test"' >> /tmp/gardm-test/config.toml
Dependencies for This Sprint
# gardmd/Cargo.toml
[dependencies]
gardm-ipc = { path = "../gardm-ipc" }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
anyhow = { workspace = true }
serde = { workspace = true }
toml = "0.8"
sd-notify = "0.4"
Next Sprint
Sprint 1 will add PAM authentication to the daemon.
View source
| 1 | # Sprint 0: Project Setup |
| 2 | |
| 3 | **Goal:** Establish project structure, build system, and minimal daemon that can start and stop cleanly. |
| 4 | |
| 5 | ## Objectives |
| 6 | |
| 7 | - Initialize Rust workspace with two crates (gardmd, gardm-greeter) |
| 8 | - Set up shared library for IPC protocol |
| 9 | - Create systemd service file |
| 10 | - Establish logging infrastructure |
| 11 | - Basic daemon lifecycle (start, signal handling, shutdown) |
| 12 | |
| 13 | ## Tasks |
| 14 | |
| 15 | ### 0.1 Initialize Cargo Workspace |
| 16 | |
| 17 | ``` |
| 18 | gardm/ |
| 19 | ├── Cargo.toml # Workspace root |
| 20 | ├── gardmd/ |
| 21 | │ ├── Cargo.toml |
| 22 | │ └── src/ |
| 23 | │ ├── main.rs # Daemon entry point |
| 24 | │ └── lib.rs |
| 25 | ├── gardm-greeter/ |
| 26 | │ ├── Cargo.toml |
| 27 | │ └── src/ |
| 28 | │ ├── main.rs # Greeter entry point |
| 29 | │ └── lib.rs |
| 30 | └── gardm-ipc/ |
| 31 | ├── Cargo.toml |
| 32 | └── src/ |
| 33 | └── lib.rs # Shared IPC types |
| 34 | ``` |
| 35 | |
| 36 | **Root Cargo.toml:** |
| 37 | ```toml |
| 38 | [workspace] |
| 39 | members = ["gardmd", "gardm-greeter", "gardm-ipc"] |
| 40 | resolver = "2" |
| 41 | |
| 42 | [workspace.dependencies] |
| 43 | serde = { version = "1.0", features = ["derive"] } |
| 44 | serde_json = "1.0" |
| 45 | tokio = { version = "1", features = ["full"] } |
| 46 | tracing = "0.1" |
| 47 | tracing-subscriber = "0.3" |
| 48 | anyhow = "1.0" |
| 49 | thiserror = "1.0" |
| 50 | ``` |
| 51 | |
| 52 | ### 0.2 Define IPC Protocol Types |
| 53 | |
| 54 | In `gardm-ipc/src/lib.rs`: |
| 55 | |
| 56 | ```rust |
| 57 | use serde::{Deserialize, Serialize}; |
| 58 | |
| 59 | #[derive(Debug, Serialize, Deserialize)] |
| 60 | #[serde(tag = "type", rename_all = "snake_case")] |
| 61 | pub enum Request { |
| 62 | CreateSession { username: String }, |
| 63 | Authenticate { response: String }, |
| 64 | StartSession { cmd: Vec<String>, env: Vec<String> }, |
| 65 | CancelSession, |
| 66 | Shutdown, |
| 67 | Reboot, |
| 68 | Suspend, |
| 69 | } |
| 70 | |
| 71 | #[derive(Debug, Serialize, Deserialize)] |
| 72 | #[serde(tag = "type", rename_all = "snake_case")] |
| 73 | pub enum Response { |
| 74 | Success, |
| 75 | AuthPrompt { prompt: String, echo: bool }, |
| 76 | AuthInfo { message: String }, |
| 77 | AuthError { message: String }, |
| 78 | Error { message: String }, |
| 79 | } |
| 80 | ``` |
| 81 | |
| 82 | ### 0.3 Basic Daemon Skeleton |
| 83 | |
| 84 | In `gardmd/src/main.rs`: |
| 85 | |
| 86 | ```rust |
| 87 | use tokio::signal::unix::{signal, SignalKind}; |
| 88 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; |
| 89 | |
| 90 | #[tokio::main] |
| 91 | async fn main() -> anyhow::Result<()> { |
| 92 | // Initialize logging to journald |
| 93 | tracing_subscriber::registry() |
| 94 | .with(fmt::layer()) |
| 95 | .with(EnvFilter::from_default_env()) |
| 96 | .init(); |
| 97 | |
| 98 | tracing::info!("gardmd starting"); |
| 99 | |
| 100 | // Signal handling |
| 101 | let mut sigterm = signal(SignalKind::terminate())?; |
| 102 | let mut sigint = signal(SignalKind::interrupt())?; |
| 103 | |
| 104 | tokio::select! { |
| 105 | _ = sigterm.recv() => { |
| 106 | tracing::info!("Received SIGTERM, shutting down"); |
| 107 | } |
| 108 | _ = sigint.recv() => { |
| 109 | tracing::info!("Received SIGINT, shutting down"); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | tracing::info!("gardmd stopped"); |
| 114 | Ok(()) |
| 115 | } |
| 116 | ``` |
| 117 | |
| 118 | ### 0.4 systemd Service File |
| 119 | |
| 120 | Create `gardm.service`: |
| 121 | |
| 122 | ```ini |
| 123 | [Unit] |
| 124 | Description=gar Display Manager |
| 125 | Documentation=man:gardmd(8) |
| 126 | After=systemd-user-sessions.service getty@tty1.service plymouth-quit.service |
| 127 | Conflicts=getty@tty1.service |
| 128 | |
| 129 | [Service] |
| 130 | Type=notify |
| 131 | ExecStart=/usr/bin/gardmd |
| 132 | Restart=always |
| 133 | RestartSec=1 |
| 134 | |
| 135 | # Security hardening |
| 136 | ProtectSystem=strict |
| 137 | ProtectHome=read-only |
| 138 | PrivateTmp=true |
| 139 | NoNewPrivileges=false # Required for PAM |
| 140 | ReadWritePaths=/run |
| 141 | |
| 142 | [Install] |
| 143 | Alias=display-manager.service |
| 144 | ``` |
| 145 | |
| 146 | ### 0.5 Configuration Scaffolding |
| 147 | |
| 148 | Create `gardmd/src/config.rs`: |
| 149 | |
| 150 | ```rust |
| 151 | use serde::Deserialize; |
| 152 | use std::path::PathBuf; |
| 153 | |
| 154 | #[derive(Debug, Deserialize)] |
| 155 | pub struct Config { |
| 156 | #[serde(default)] |
| 157 | pub general: GeneralConfig, |
| 158 | #[serde(default)] |
| 159 | pub greeter: GreeterConfig, |
| 160 | } |
| 161 | |
| 162 | #[derive(Debug, Deserialize, Default)] |
| 163 | pub struct GeneralConfig { |
| 164 | #[serde(default = "default_session")] |
| 165 | pub default_session: String, |
| 166 | #[serde(default = "default_greeter")] |
| 167 | pub greeter: PathBuf, |
| 168 | #[serde(default)] |
| 169 | pub vt: u32, |
| 170 | } |
| 171 | |
| 172 | #[derive(Debug, Deserialize, Default)] |
| 173 | pub struct GreeterConfig { |
| 174 | #[serde(default = "default_blur_radius")] |
| 175 | pub blur_radius: u32, |
| 176 | } |
| 177 | |
| 178 | fn default_session() -> String { "gar".to_string() } |
| 179 | fn default_greeter() -> PathBuf { "/usr/bin/gardm-greeter".into() } |
| 180 | fn default_blur_radius() -> u32 { 20 } |
| 181 | |
| 182 | impl Config { |
| 183 | pub fn load() -> anyhow::Result<Self> { |
| 184 | let path = PathBuf::from("/etc/gardm/config.toml"); |
| 185 | if path.exists() { |
| 186 | let content = std::fs::read_to_string(&path)?; |
| 187 | Ok(toml::from_str(&content)?) |
| 188 | } else { |
| 189 | Ok(Config::default()) |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | ``` |
| 194 | |
| 195 | ## Acceptance Criteria |
| 196 | |
| 197 | 1. `cargo build --release` succeeds for all workspace members |
| 198 | 2. `gardmd` starts, logs to stdout, and exits cleanly on SIGTERM |
| 199 | 3. IPC types serialize/deserialize correctly (unit test) |
| 200 | 4. Config loads from file or uses defaults |
| 201 | 5. Service file passes `systemd-analyze verify` |
| 202 | |
| 203 | ## Pitfalls to Avoid |
| 204 | |
| 205 | 1. **Don't start X11 yet** - that's Sprint 2. Keep this sprint minimal. |
| 206 | 2. **Don't implement PAM yet** - that's Sprint 1. Focus on structure. |
| 207 | 3. **Avoid premature optimization** - get it working first. |
| 208 | 4. **Don't forget `sd_notify`** - systemd needs to know when daemon is ready. |
| 209 | 5. **Test without root first** - use `RUST_LOG=debug cargo run` for development. |
| 210 | |
| 211 | ## Testing |
| 212 | |
| 213 | ```bash |
| 214 | # Build |
| 215 | cargo build --release |
| 216 | |
| 217 | # Run daemon in foreground (Ctrl+C to stop) |
| 218 | RUST_LOG=debug ./target/release/gardmd |
| 219 | |
| 220 | # Verify signal handling |
| 221 | kill -TERM $(pidof gardmd) |
| 222 | |
| 223 | # Test config loading |
| 224 | mkdir -p /tmp/gardm-test |
| 225 | echo '[general]' > /tmp/gardm-test/config.toml |
| 226 | echo 'default_session = "test"' >> /tmp/gardm-test/config.toml |
| 227 | ``` |
| 228 | |
| 229 | ## Dependencies for This Sprint |
| 230 | |
| 231 | ```toml |
| 232 | # gardmd/Cargo.toml |
| 233 | [dependencies] |
| 234 | gardm-ipc = { path = "../gardm-ipc" } |
| 235 | tokio = { workspace = true } |
| 236 | tracing = { workspace = true } |
| 237 | tracing-subscriber = { workspace = true } |
| 238 | anyhow = { workspace = true } |
| 239 | serde = { workspace = true } |
| 240 | toml = "0.8" |
| 241 | sd-notify = "0.4" |
| 242 | ``` |
| 243 | |
| 244 | ## Next Sprint |
| 245 | |
| 246 | Sprint 1 will add PAM authentication to the daemon. |