1. Overview
The FirstRunServicesWindow module implements BastionGuard’s first-run “services wizard” UI. Its goal is to provide a guided, one-time configuration experience where the user selects which protection services should be enabled at login.
The module is implemented in GTKmm (GTK4) and orchestrates both:
- User-scoped systemd units managed via
systemctl --user - System-scoped systemd units managed via privileged
pkexec systemctl
The wizard persists completion state using a flag file, and stores a structured JSON configuration snapshot for auditing and future UI defaults.
2. Persistent State and Files
2.1 First-Run Completion Flag
To ensure the wizard is shown only once, completion is tracked using a flag file:
~/.config/BastionGuard/first-run-services-done
If the flag exists, maybe_show() returns immediately and does not instantiate the window.
2.2 Configuration Snapshot (config.json)
After applying selections, the module writes a configuration snapshot to:
~/.config/BastionGuard/config.json
The file includes:
wizard_completed– boolean markerinstalled_at– UTC timestamp (ISO-8601 format)services– object mapping systemd unit name → boolean enabled state
3. Units and Privilege Model
3.1 System Units Set
The wizard maintains a static allowlist of system-level units:
SYSTEM_UNITS = {
"BastionGuard-phishing-scanner.service",
"BastionGuard-phishing-updater.service",
"BastionGuard-phishing-updater.timer",
"Bastionguard-privhelper.service",
"bastionguard-sanesecurity.service",
"bastionguard-sanesecurity.timer",
"BastionGuard-ransomware-realtime.service",
"BastionGuard-restart-on-graphical.service"
};
Membership is tested using is_system_unit(unit_name). Units in this set are managed via pkexec systemctl.
3.2 User Units
Units not present in SYSTEM_UNITS are treated as user-level services and are managed through:
systemctl --user ...
3.3 Privilege Separation
The module enforces a strict separation:
- User service operations are executed without privilege escalation
- System service operations require elevated authorization via
pkexec
This ensures that only explicitly flagged system units can trigger privileged actions.
4. Command Execution Helpers
4.1 run_cmd(argv)
The module defines a robust command runner using Gio::Subprocess:
- Supports optional stdout capture
- Supports stderr silencing or piping
- Waits synchronously for completion
- Returns
truewhen the subprocess completes successfully
This helper is used for both user systemctl operations and privileged wrappers.
4.2 pkexec_systemctl(args)
Privileged system unit management is executed through a dedicated wrapper:
pkexec systemctl <args...>
The wrapper constructs the full argument vector and delegates execution to run_cmd().
5. Directory Creation and Robust Flag Handling
5.1 ensure_config_dir()
Ensures the configuration directory exists:
~/.config/BastionGuard
If missing, the module creates it via make_directory_with_parents(). Exceptions are logged as diagnostics.
5.2 ensure_flag_dir()
Ensures the directory for the first-run flag exists. The path is the same user config directory, but a separate function is used to guarantee correctness and maintain separation of concerns.
5.3 touch_flag_file()
After the wizard is dismissed, the module writes the completion flag by truncating/creating:
~/.config/BastionGuard/first-run-services-done
This write is performed using an ofstream open with std::ios::trunc to ensure idempotent operation.
6. Window Lifecycle and One-Time Display
6.1 Construction and Initialization
The window constructor configures basic window properties and initializes all UI structures:
- Modal window
- Resizable
- Default and minimum size constraints
Initialization sequence:
build_headerbar()build_ui()load_current_states()
6.2 maybe_show()
The static entry point maybe_show() enforces one-time display semantics:
- If the flag file exists → do not show the wizard
- Otherwise → create a window instance and show it
The window connects to signal_hide() so that when it is hidden (Apply/Close), it:
- Writes the completion flag
- Deletes the window instance
7. Header Bar and Window Controls
7.1 Custom Header Bar
The wizard uses a custom header bar:
- Default title buttons disabled
- Decoration layout cleared
- CSS class
custom-headerbarapplied
The title widget is a centered label:
"BastionGuard - Initial configuration"
(Localized via _().)
7.2 Custom Buttons
The header includes two custom buttons:
- Minimize – loads
icon-minimize.png - Close – loads
icon-close.png
If icons cannot be loaded, diagnostics are logged and the UI remains functional.
8. Main UI Layout and Service Inventory
8.1 Layout Structure
The UI is composed of:
- A left sidebar with branding
- A right content container holding selection controls
The sidebar includes:
- Logo loaded from
logo-sidebar.png - Brand label “BastionGuard”
- A vertical spacer to keep header content pinned
8.2 Services Grid
Services are rendered as a two-column grid:
- Column 1: a check button with the service display label
- Column 2: a wrapped description label with ellipsization
Each entry is represented by:
- Systemd unit name (string)
- Localized label (UI title)
- Localized description (UI help text)
Check buttons are defaulted to active at creation time, then immediately overwritten by the real system state during load_current_states().
8.3 Wizard Actions
The footer contains three primary actions:
- Minimal configuration – applies a curated allowlist of essential services
- Enable all – sets all service toggles to active
- Apply and continue – applies changes and closes the window
9. State Discovery and Synchronization
9.1 load_current_states()
On startup, the wizard queries the current enabled state of each unit and synchronizes the corresponding checkbox.
Logic:
- If
is_system_unit(unit)→ callis_system_service_enabled() - Else → call
is_user_service_enabled()
9.2 Enabled-State Query Semantics
Enabled state is determined using:
systemctl --user is-enabled <unit>for user unitssystemctl is-enabled <unit>for system units
The module:
- Captures stdout and stderr
- Trims newline characters from stdout
- Checks for the literal value
enabled
If the command fails, the unit is treated as not enabled.
10. Selection Presets
10.1 select_all()
Sets all service check buttons to active.
10.2 select_minimal()
Applies a minimal configuration based on a curated set of unit names. Only those units in the minimal allowlist are enabled; all others are disabled.
The minimal set is defined in-code and can include both user and system units.
11. Applying Changes
11.1 apply_and_close()
When the user applies changes, the wizard:
- For each unit, computes the desired state (
want) from the checkbox - Queries the current enabled state (
current) - Skips units where
want == current - Otherwise enables/disables the unit using the correct privilege path
- Writes
config.jsonsnapshot - Closes the window via
hide()
This design minimizes systemctl calls and reduces privileged prompts to only the units requiring changes.
12. systemd Operations
12.1 User Units
Enable operation:
systemctl --user enable --now <unit>
systemctl --user start <unit>
Disable operation:
systemctl --user stop <unit>
systemctl --user disable --now <unit>
Explicit start and stop calls are executed for additional robustness even though enable --now and disable --now already imply activation changes.
12.2 System Units
Enable operation (privileged):
pkexec systemctl enable --now <unit>
pkexec systemctl start <unit>
Disable operation (privileged):
pkexec systemctl stop <unit>
pkexec systemctl disable --now <unit>
Stderr is not silenced for system operations to improve diagnostics during privileged failures.
13. Close Semantics and Event Handling
13.1 Header Close Button
The custom close button triggers apply_and_close(), ensuring changes are applied consistently even if the user closes via the header.
13.2 Close Request Handling
The window implements on_close_request() and also calls apply_and_close(), ensuring consistent persistence and service reconciliation on window close events.
13.3 Minimize Behavior
The minimize button hides the window conditionally based on GTKmm version guard:
- For GTKmm ≥ 4.10, minimize action is implemented as
hide()
14. Configuration Persistence
14.1 save_config()
The module writes a structured JSON document containing wizard completion metadata and service toggle states.
Timestamping is generated using UTC time:
Glib::DateTime::create_now_utc()
The JSON is saved with indentation:
config.dump(2)
On write failures, a diagnostic is logged.
15. Runtime and Security Considerations
- Privilege separation: only units explicitly classified as system units invoke
pkexec, reducing accidental privilege escalation - Allowlist-based privilege: system units are gated through a static allowlist (
SYSTEM_UNITS) rather than dynamic naming - Operational safety: state reconciliation skips unchanged units, reducing systemctl churn and unnecessary prompts
- One-time semantics: completion flag prevents repeated presentation while still allowing later adjustments through Settings
- Auditability:
config.jsonprovides a stable snapshot of initial service enablement choices - Crash tolerance: directory creation uses “create with parents” and failure is logged without hard crash
- Localization readiness: user-visible strings are wrapped via
_() - Synchronous systemctl: systemctl calls are blocking; if large numbers of units are added, a progress UI or background execution may be required
- Polkit UX: enabling/disabling system units may prompt for authorization; callers should ensure the wizard messaging prepares the user appropriately