markdown · 5872 bytes Raw Blame History

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

  1. cargo build --release succeeds for all workspace members
  2. gardmd starts, logs to stdout, and exits cleanly on SIGTERM
  3. IPC types serialize/deserialize correctly (unit test)
  4. Config loads from file or uses defaults
  5. Service file passes systemd-analyze verify

Pitfalls to Avoid

  1. Don't start X11 yet - that's Sprint 2. Keep this sprint minimal.
  2. Don't implement PAM yet - that's Sprint 1. Focus on structure.
  3. Avoid premature optimization - get it working first.
  4. Don't forget sd_notify - systemd needs to know when daemon is ready.
  5. Test without root first - use RUST_LOG=debug cargo run for 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.