small hot-reloading

This commit is contained in:
2026-02-06 12:30:13 +01:00
parent 51811d4f57
commit 39c16cbac5
3 changed files with 681 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
# Config Struct Analysis: cargo run vs Standalone EXE
## QUICK ANSWER
**The config.toml modifications work in `cargo run` but not standalone EXE because:**
1. **Both behave identically** - neither reloads config during runtime
2. **The difference is file location** - where config.toml is searched for
3. **cargo run appears to work** because users restart the process frequently
4. **Standalone EXE appears broken** because users expect live reloading
The real issue: **Config is frozen at startup and never reloaded.**
---
## THE CORE PROBLEM
### Configuration Loading (One-Time Only)
**File: `src/main.rs`, Line 54**
```rust
let app_config = config::load_config(); // Loaded ONCE, never again
```
### Configuration Freezing Points
**Freeze Point 1: BackgroundRenderer (`src/app.rs:28-34`)**
```rust
let background_renderer = Some(Arc::new(BackgroundRenderer::new(
&cc.egui_ctx,
&config.appearance.background_image,
config.appearance.background_opacity, // ← COPIED
config.appearance.fallback_color, // ← COPIED
config.appearance.fallback_color_opacity, // ← COPIED
)));
```
- Appearance values **copied** into struct once
- Stored in `Arc<BackgroundRenderer>` (immutable)
- **Never updated** during application lifetime
**Freeze Point 2: Config Field (`src/app.rs:49`)**
```rust
Self {
config, // ← Stored as owned field, never mutated
// ...
}
```
- Config struct **stored once** at initialization
- Colors, text sizing, window settings **frozen**
- **Never reloaded** from disk
- **Never updated** at runtime
---
## WHY CHANGES DON'T APPEAR
### Background Changes (opacity, color)
1. User modifies `config.toml` (e.g., changes `background_opacity`)
2. Application still has original value in `BackgroundRenderer` memory
3. Every frame, `render_fullscreen()` uses **frozen opacity value**
4. No code to reload config or recreate `BackgroundRenderer`
5. **Change never takes effect**
### Color Changes
1. User modifies `config.toml` (e.g., changes text color)
2. Application still has original `Config` struct in memory
3. Every frame, `update()` reads from `self.config`
4. But `self.config` was never updated
5. `ctx.set_visuals()` applies **same frozen colors**
6. **Change never takes effect**
---
## cargo run vs Standalone: The REAL Story
### cargo run Process
```
User runs: cargo run
├─ New process created
├─ load_config() reads config.toml from repo root (fallback path)
├─ Application starts with current config
├─ User modifies config.toml while app is running
│ └─ Change DOESN'T take effect (no reload mechanism)
├─ User exits app
├─ User runs: cargo run again
│ ├─ New process created
│ ├─ load_config() reads UPDATED config.toml
│ └─ Application shows new values ✓
└─ User thinks "config changes work in cargo run!"
```
### Standalone EXE Process
```
User runs: Create-App-Secret.exe
├─ Process created
├─ load_config() looks in EXE directory
│ └─ If config.toml exists there, loads it
│ └─ Otherwise falls back to ./config.toml
├─ Application starts with current config
├─ User modifies config.toml while app is running
│ └─ Change DOESN'T take effect (no reload mechanism)
├─ User expects changes to appear
│ └─ Nothing happens ✗
├─ User closes app
├─ User runs: Create-App-Secret.exe again
│ ├─ New process created
│ ├─ load_config() reads UPDATED config.toml
│ └─ Application shows new values ✓
└─ User thinks "config changes don't work in standalone!"
```
### The Actual Difference
| Aspect | cargo run | Standalone |
|--------|-----------|-----------|
| Config loading code | Identical | Identical |
| Config freezing | Yes, identical | Yes, identical |
| Hot-reload | No | No |
| Updates on restart | Yes ✓ | Yes ✓ |
| File location | Repo root (fallback) | EXE directory |
---
## CODE PATH: From Config Loading to Display
```
main.rs:54
config::load_config() ← Reads config.toml from disk [ONCE]
Returns Config struct
main.rs:76
Moved into AzureAppManager::new()
app.rs:28-34: BackgroundRenderer created
└─ opacity, fallback_color COPIED from config [FROZEN]
└─ Stored in Arc<BackgroundRenderer>
app.rs:49: Config stored
└─ self.config = config [FROZEN]
eframe event loop (every frame ~60x/sec):
├─ app.rs:375-457: update() called
│ ├─ app.rs:380: Read colors from self.config [frozen values]
│ ├─ app.rs:467: Read text sizing from self.config [frozen values]
│ ├─ app.rs:489: Read position offset from self.config [frozen values]
│ └─ app.rs:505-507: Render background with frozen renderer
└─ Every frame: Same frozen values used again
(Because no code reloads or updates them)
```
---
## What Would Be Needed to Fix
### 1. File Watching
```rust
// Add to Cargo.toml
notify = "6.0" // File system event watcher
```
### 2. Config Reload Mechanism
```rust
pub fn reload_config_from_disk() -> Config {
config::load_config() // Re-read from disk
}
```
### 3. Mutable Config Storage
```rust
// Change from:
config: Config
// To:
config: Arc<Mutex<Config>>
```
### 4. Updatable Background Renderer
```rust
// Either:
// - Store as Arc<Mutex<BackgroundRenderer>>
// - Or recreate it every frame with current config values
```
### 5. Hot Reload Trigger
```rust
if config_file_changed {
new_config = reload_config_from_disk();
update_background_renderer(&new_config);
ctx.request_repaint();
}
```
---
## Summary Table
| Component | Location | Issue | Status |
|-----------|----------|-------|--------|
| Config loading | `main.rs:54` | Loaded once | FROZEN |
| Config movement | `main.rs:76` | Moved via ownership | FROZEN |
| Background values | `app.rs:28-34` | Copied into struct | FROZEN |
| Config storage | `app.rs:49` | Stored as owned field | FROZEN |
| Color application | `app.rs:380-444` | Reads frozen config | EVERY FRAME |
| Background rendering | `background.rs:106` | Uses frozen values | EVERY FRAME |
| Reload mechanism | *Not found* | No file watching | ✗ MISSING |
---
## Key Insight
**The application architecture assumes configuration is set once at startup and never changes.** This is a design choice, not a bug. The same frozen-config behavior exists in both `cargo run` and standalone EXE.
The perceived difference comes from:
- **cargo run**: Users frequently restart processes → see updated config
- **Standalone EXE**: Users expect live updates → see frozen config
Both are executing identical code with identical behavior.
+243
View File
@@ -0,0 +1,243 @@
================================================================================
COMPLETE REFERENCE: Config Struct Analysis
================================================================================
Two comprehensive analysis documents have been created:
1. analysis.txt (212 lines)
- Complete technical breakdown
- All code paths with exact line numbers
- Detailed freezing point mechanisms
- Complete execution timeline
- Comparison tables
2. ANALYSIS_SUMMARY.md (226 lines)
- Executive summary
- Quick-reference guide
- Visual diagrams and flows
- What needs to be fixed
- Implementation requirements
ABSOLUTE PATHS:
C:\Users\DG2210\OneDrive - Gemeente Vught\Documenten\Git\I-SecretUpdate\analysis.txt
C:\Users\DG2210\OneDrive - Gemeente Vught\Documenten\Git\I-SecretUpdate\ANALYSIS_SUMMARY.md
================================================================================
THE PROBLEM IN ONE SENTENCE
================================================================================
Config is loaded ONCE at startup and frozen in immutable data structures,
with NO code to reload it during runtime.
================================================================================
THE SOLUTION IN ONE SENTENCE
================================================================================
Implement file watching, config reloading, and mutable storage patterns to
enable hot-reload of configuration changes.
================================================================================
CRITICAL FINDINGS CHECKLIST
================================================================================
[✓] Config loaded once in main.rs:54
[✓] Config values COPIED into BackgroundRenderer (app.rs:28-34)
[✓] BackgroundRenderer stored in Arc (immutable)
[✓] Config stored in AzureAppManager.config field (app.rs:49)
[✓] Every frame reads same frozen values (app.rs:375-457)
[✓] No reload mechanism exists (zero file watching code)
[✓] No polling of config.toml (no modification tracking)
[✓] Both cargo run and standalone behave identically
[✓] File location is the ONLY difference between them
[✓] Users perceive difference due to process restart frequency
================================================================================
CODE CHANGES NEEDED FOR HOT-RELOAD
================================================================================
1. Add to Cargo.toml:
notify = "6.0"
2. Modify src/app.rs:
- Change: config: Config
- To: config: Arc<Mutex<Config>>
- Change: state.background_renderer storage
- To: Arc<Mutex<BackgroundRenderer>> or recreate every frame
3. Add config reload function in src/config/window_config.rs:
- pub fn reload_config_from_disk() -> Config
4. Add file watcher in main loop:
- Detect config.toml changes
- Call reload_config_from_disk()
- Update Arc<Mutex<Config>>
- Call ctx.request_repaint()
5. Add synchronization:
- Lock config before reading
- Lock config before writing
- Handle lock contention
================================================================================
EVIDENCE FOR ONE-TIME INITIALIZATION
================================================================================
SEARCH 1: "load_config" calls
Result: Found in main.rs:54 only
Conclusion: Called ONCE during startup
SEARCH 2: "config =" mutations
Result: Configuration field never reassigned after app.rs:49
Conclusion: Never updated after initialization
SEARCH 3: "notify", "watch", "polling", "reload"
Result: NOT FOUND in codebase
Conclusion: No hot-reload mechanism exists
SEARCH 4: "Arc<Mutex<Config>>"
Result: NOT FOUND
Conclusion: No mutable shared access pattern used
SEARCH 5: "file.metadata", "modification_time"
Result: NOT FOUND
Conclusion: No file change detection exists
================================================================================
EVIDENCE FOR FROZEN BACKGROUND RENDERER
================================================================================
FREEZE POINT: src/app.rs:28-34
let background_renderer = Some(Arc::new(BackgroundRenderer::new(
&cc.egui_ctx,
&config.appearance.background_image,
config.appearance.background_opacity, // ← COPIED
config.appearance.fallback_color, // ← COPIED
config.appearance.fallback_color_opacity, // ← COPIED
)));
RESULT:
- Values copied into struct at this point
- Stored in Arc (prevents mutation)
- Never recreated
- Never updated
USAGE: src/app.rs:506
if let Some(bg) = &self.state.background_renderer {
bg.render_fullscreen(ctx); // Uses frozen values
}
CALLED: Every frame in update() method
RESULT: Same frozen values rendered 60+ times per second
================================================================================
EVIDENCE FOR FROZEN CONFIG FIELD
================================================================================
STORAGE POINT: src/app.rs:49
pub struct AzureAppManager {
config: Config, // ← Stored as owned field
// ...
}
Self {
config, // ← Stored once, never updated
// ...
}
RESULT:
- Config struct stored as owned value
- Not Arc, not Mutex, not mutable reference
- Can only be read, never mutated
- Never reloaded from disk
USAGE POINTS:
- app.rs:380 - Read colors: let colors = self.config.colors.as_ref();
- app.rs:467 - Read text sizing: &self.config.text_sizing
- app.rs:489 - Read position: self.config.window.position_offset_x
CALLED: Every frame in update() method
RESULT: Same frozen config values read 60+ times per second
================================================================================
PATH RESOLUTION FOR BOTH ENVIRONMENTS
================================================================================
Code Location: src/config/window_config.rs:234-240
let config_path = if let Ok(exe_path) = std::env::current_exe() {
exe_path
.parent()
.map(|p| p.join("config.toml"))
.unwrap_or_else(|| Path::new("./config.toml").to_path_buf())
} else {
Path::new("./config.toml").to_path_buf()
};
CARGO RUN:
Step 1: std::env::current_exe() returns target\debug\create-app-secret.exe
Step 2: parent() = target\debug\
Step 3: Look for target\debug\config.toml → NOT FOUND
Step 4: Fall back to ./config.toml
Step 5: Resolve in repo root → FOUND ✓
Step 6: Load config from repo root
Result: Config found and loaded from repo root
STANDALONE EXE:
Step 1: std::env::current_exe() returns exe directory path
Step 2: parent() = exe directory
Step 3: Look for config.toml in exe directory → MAYBE FOUND
Step 4: If not found, fall back to ./config.toml
Step 5: Resolve relative path
Step 6: Load config if found
Result: Config found in exe directory OR user's working directory
KEY POINT: Different file locations, but same code executes in both cases
================================================================================
WHY CARGO RUN APPEARS TO WORK
================================================================================
User Perspective:
1. cargo run
- Application starts
- Config loaded from repo root
- Application displays settings
2. User modifies config.toml
- Still running in background
- Changes have NO effect (no reload mechanism)
3. cargo run (run command again)
- OLD PROCESS EXITS
- NEW PROCESS STARTS
- load_config() called in new process
- Reads UPDATED config.toml
- Application displays new settings
4. User thinks: "Config changes work with cargo run!"
Reality: The application didn't reload. The user created a new process.
================================================================================
WHY STANDALONE EXE APPEARS BROKEN
================================================================================
User Perspective:
1. Run Create-App-Secret.exe
- Application starts
- Config loaded
- Application displays settings
2. User modifies config.toml
- Application still running
- Changes have NO effect (no reload mechanism)
- User expects application to update automatically
3. Application does NOT update
+212
View File
@@ -0,0 +1,212 @@
================================================================================
DETAILED ANALYSIS: Config Struct Usage - cargo run vs Standalone EXE
================================================================================
EXECUTIVE SUMMARY
================================================================================
Config modifications don't work in either cargo run OR standalone EXE after
startup. They only appear to work in cargo run because each run is a NEW
process that reloads the config file from disk.
ROOT CAUSE: One-time initialization with frozen values, no reload mechanism.
KEY FINDINGS:
1. config.toml is loaded ONCE in main.rs:54
2. Config values COPIED into BackgroundRenderer (immutable Arc)
3. Config STORED in AzureAppManager (never updated)
4. Every frame reads SAME frozen values
5. ZERO reload/hot-update mechanism exists
6. Working directory only affects WHERE file is looked for, not HOW changes propagate
================================================================================
1. CONFIG LOADING - src/main.rs Lines 35-80
================================================================================
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// LINE 54: LOADED ONCE
let app_config = config::load_config();
// LINES 60-64: VALUES APPLIED TO EFRAME
let viewport_builder = egui::ViewportBuilder::default()
.with_inner_size([app_config.window.width, app_config.window.height])
.with_min_inner_size([app_config.window.min_width, app_config.window.min_height])
.with_transparent(app_config.window.use_transparency)
.with_icon(icon_data);
// LINES 73-77: MOVED INTO MANAGER (ownership transferred, never accessed in main again)
eframe::run_native(
"Create App Secret",
options,
Box::new(move |cc| Ok(Box::new(AzureAppManager::new(cc, auth, app_config)))),
)?;
Ok(())
}
CRITICAL: The 'move' keyword transfers ownership to the closure. After this
point, no other code in main can access app_config.
================================================================================
2. CONFIG FILE PATH RESOLUTION - src/config/window_config.rs Lines 232-295
================================================================================
pub fn load_config() -> Config {
let config_path = if let Ok(exe_path) = std::env::current_exe() {
exe_path
.parent()
.map(|p| p.join("config.toml"))
.unwrap_or_else(|| Path::new("./config.toml").to_path_buf())
} else {
Path::new("./config.toml").to_path_buf()
};
if config_path.exists() {
// Load and parse TOML
} else {
// Create defaults
}
Config::default() // Fallback
}
CARGO RUN PATH:
current_exe() = C:\...\I-SecretUpdate\target\debug\create-app-secret.exe
parent = C:\...\I-SecretUpdate\target\debug\
looks for C:\...\I-SecretUpdate\target\debug\config.toml ❌ NOT FOUND
falls back to ./config.toml
resolves to C:\...\I-SecretUpdate\config.toml ✓ FOUND
STANDALONE EXE PATH:
current_exe() = C:\wherever\user\puts\Create-App-Secret.exe
parent = C:\wherever\user\puts\
looks for C:\wherever\user\puts\config.toml ✓ OR ❌
if not found, falls back to ./config.toml
KEY POINT: Both use identical code and path resolution logic. The file
location is the ONLY difference, not how changes are handled.
================================================================================
3. FIRST INITIALIZATION - FREEZE POINT 1 & 2 - src/app.rs Lines 20-52
================================================================================
pub struct AzureAppManager {
state: AppState,
auth: Arc<AzureAuthenticator>,
graph_client: GraphApiClient,
keyvault_client: KeyVaultClient,
vault_discovery: VaultDiscovery,
config: Config, // ← FREEZE POINT 2: Stored, never mutated
position_applied: bool,
}
impl AzureAppManager {
pub fn new(cc: &eframe::CreationContext<'_>,
auth: Arc<AzureAuthenticator>,
config: Config) -> Self {
let mut state = AppState::new();
// FREEZE POINT 1: Create BackgroundRenderer with COPIED config values
let background_renderer = Some(Arc::new(BackgroundRenderer::new(
&cc.egui_ctx,
&config.appearance.background_image,
config.appearance.background_opacity, // ← COPIED HERE
config.appearance.fallback_color, // ← COPIED HERE
config.appearance.fallback_color_opacity, // ← COPIED HERE
)));
state.background_renderer = background_renderer;
Self {
state,
auth,
graph_client,
keyvault_client,
vault_discovery,
config, // ← FREEZE POINT 2: Stored as owned, never updated
position_applied: false,
}
}
}
WHAT GETS FROZEN:
FREEZE POINT 1 (BackgroundRenderer):
- opacity value COPIED
- fallback_color value COPIED
- fallback_color_opacity value COPIED
- These are stored in Arc<BackgroundRenderer> (immutable)
- Used every frame via render_fullscreen()
- NEVER RECALCULATED OR UPDATED
FREEZE POINT 2 (Config Field):
- Entire Config struct stored
- Not mutable reference, not Arc<Mutex<>>
- Owned field that cannot be changed
- Read every frame in update()
- NEVER RELOADED FROM DISK
================================================================================
4. BACKGROUND RENDERING - EVERY FRAME - src/ui/background.rs Lines 106-152
================================================================================
pub fn render_fullscreen(&self, ctx: &Context) {
let screen_rect = ctx.screen_rect();
let painter = ctx.layer_painter(egui::LayerId::background());
// Render with FROZEN fallback_color_opacity
let fallback_with_opacity = Color32::from_rgba_unmultiplied(
self.fallback_color.r(),
self.fallback_color.g(),
self.fallback_color.b(),
(self.fallback_color_opacity * 255.0) as u8, // ← FROZEN VALUE
);
painter.rect_filled(screen_rect, 0.0, fallback_with_opacity);
if let Some(texture) = &self.texture {
// Render with FROZEN opacity
let tint = Color32::from_rgba_unmultiplied(
255, 255, 255,
(self.opacity * 255.0) as u8 // ← FROZEN VALUE
);
painter.image(texture.id(), screen_rect, uv_rect, tint);
}
}
CALLED EVERY FRAME FROM: src/app.rs Line 506
if let Some(bg) = &self.state.background_renderer {
bg.render_fullscreen(ctx); // Uses same frozen values
}
RESULT: Background renders with identical opacity and colors every frame.
User modifies config.toml, but application still renders with original values.
================================================================================
5. COLOR APPLICATION - EVERY FRAME - src/app.rs Lines 375-457
================================================================================
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let mut visuals = egui::Visuals::dark();
// LINE 380: READ FROM FROZEN CONFIG
let colors = self.config.colors.as_ref();
if let Some(color_config) = colors {
let slider = &color_config.slider;
let text = &color_config.text;
// Apply colors
visuals.widgets.noninteractive.fg_stroke.color =
egui::Color32::from_rgb(text.normal[0], text.normal[1], text.normal[2]);
visuals.widgets.inactive.fg_stroke.color =
egui::Color32::from_rgb(text.inactive[0], text.inactive[1], text.inactive[2]);
visuals.widgets.hovered.fg_stroke.color =
egui::Color32::from_rgb(text.hover[0], text.hover[1], text.hover[2]);
// ... more color applications ...
// Apply slider colors
visuals.widgets.noninteractive.bg_fill =
egui::Color32::from_rgb(slider.inactive[0], slider.inactive[1], slider.inactive[2]);
// ... more slider col