This guide covers the development workflow and architecture of the Cobalt Engine component. The Engine is a Rust-based background process that tracks application usage and responds to alerts and reminders.
The Engine's responsibilities are:
- Watching foreground windows, user interactions and system events
- Insert app, session, and usage records into the database when events occur
- Insert user interaction periods and system events into the database
- Coordinate with Resolver to get app info
- Periodically check for alerts and reminders and execute actions
src/
├── engine/ # Main engine code
│ ├── src/
│ │ ├── engine.rs # Core state machine
│ │ ├── sentry.rs # Alert manager
│ │ ├── resolver.rs # App information resolver
│ │ ├── desktop.rs # In-memory state of the Desktop
│ │ ├── ...
│ └── Cargo.toml
├── platform/ # Platform-specific abstractions
│ ├── src/
│ │ ├── objects/ # Window, Process, Timestamp abstractions
│ │ ├── events/ # Event watchers
│ │ ├── web/ # Web browser abstractions
│ │ ├── ...
│ │ └── error.rs # Error handling
│ └── Cargo.toml
├── data/ # Database access layer
│ ├── src/
│ │ ├── db/ # Database connection management and queries
│ │ ├── migrations/ # Schema migrations
│ │ └── entities.rs # Data models
│ └── Cargo.toml
└── util/ # Utility functions and common dependencies
└── Cargo.toml
The Engine is the main state machine that processes events from the platform layer. It's implemented in src/engine/src/engine.rs.
Key responsibilities:
- Tracking application sessions and usages
- Recording user interaction periods
- Coordinating with Resolver and Sentry
Sentry checks for triggered alerts and reminders and runs their respective actions (kill processes, send toast messages, dim windows, and send reminder toast messages with progress), marking them as completed if necessary in a loop.
Key responsibilities:
- Checking for triggerable alerts and reminders
- Executing configured actions (kill processes, display messages, dim windows)
- Marking alerts as completed when triggered
Resolver gathers additional information about applications. It is activated by the Engine and runs asynchronously, saving the resolved information to the database, and also runs during engine startup to resolve app info for existing apps that are not initialized, in case of previous Resolver failures.
Key responsibilities:
- Resolving application metadata (name, description, company, icon)
- Storing resolved information in the database
DesktopState is a simple in-memory cache that stores the resolved information about apps and sessions, along with their associated processes and windows. There is only one instance of this cache around, and it is accessed by the Engine and Resolver.
It is essentially the current state of the desktop.
Key responsibilities:
- Storing app information
- Tracking active sessions and processes
Platform-specific objects are defined in platform/src/objects/mod.rs, like Window, Process and Timestamp. There is liberal use of unsafe here, as we are interfacing with the OS and need to handle pointers and memory allocation.
Platform-specific objects like Windows, Processes, and Timestamps are defined in src/platform/src/objects/mod.rs.
Web browser abstractions are defined in src/platform/src/web/mod.rs. Communication with Edge and Chrome through UI Automation.
Platform-specific event watchers are defined in src/platform/src/events/mod.rs like ForegroundEventWatcher, InteractionWatcher, etc.
These are intended to be polled at a regular interval to check for changes in the system. However, it is possible for it to be event-driven rather than poll-based by using the Win32 API (SetWindowsHookEx and SetWinEventHook).
We have our own Buffer trait that boils down to a &[T] slice, including a WideBuffer trait for &[u16] (which can be converted to a String). The common use case would be used via the buf method to create a new SmartBuf<T>, which can be stack or heap-allocated depending on the size.
Errors are defined in /src/platform/src/errors.rs and are used throughout the platform layer.
There are two types of errors: Win32Error and NtError (Win32 GetLastError and NTSTATUS error codes, respectively). We don't need to represent HRESULT errors as they are already handled by windows-rs. We also have win32! macros to handle Win32 operations and adapt_size! when we need to repeat operations until the size is correct.
The Data layer manages database operations and schema migrations.
The main database connection and query execution is handled in src/data/src/db/mod.rs. It uses the sqlx crate for database operations,
using SQLite Connection Pooling for connections.
Migrations are handled when the Database is initialized, in data/src/migrations/mod.rs, by implementing the Migration trait (e.g. migrationX.rs) and specifying a version() larger than the other migrations. Initialization of Database automatically runs migrations that are not yet applied. The Database holds
the connection and runs the queries, and is able to create new transactions.
Database is used by specific objects like UsageInserter, AppUpdater, AlertManager and Repository. Repository holds more
complicated queries used by the UI, and all output returned by the function must be Serializable to that it can be sent to the UI.
The Engine mainly uses two threads:
- Main Thread: Runs the Engine, Sentry, and Resolver using tokio single-threaded runtime for async operations.
- Win32 Event Loop: A dedicated thread for handling platform-specific events and timers.
See DEV_GUIDE.md for Development Commands.
- Visual Studio Code: Use the Rust extension with launch configurations for debugging.
- DataGrip: For looking at query plans, debugging SQL and analyzing performance.
- Win32 API Calls: Minimize frequent API calls, especially those that might be expensive.
- Memory Usage: Monitor memory usage, especially for the in-memory cache - it's usually around 5MB, max 10MB.
- CPU Usage: Ensure polling intervals are balanced for responsiveness vs. CPU load.
- Security: Use
unsafeonly when necessary, and ensure proper error handling. - Fault Tolerance: The Engine is designed to be fault-tolerant, but it's not immune to all errors. Catch and retry fallible operations or provide defaults (especially the Win32 API operations), or crash gracefully.