1
//! Process-wide snapshot of the app-level configuration that ENGINE services
2
//! (updater, system dialogs) need outside any callback: the app's name and
3
//! version, where the update manifest lives, the changelog URL, the support
4
//! mailbox.
5
//!
6
//! `AppConfig` is the source of truth; `App::run` publishes it here once at
7
//! startup (same pattern as `window::set_global_system_animations`). Layout
8
//! examples and tests that never go through `App::run` call [`set_app_env`]
9
//! directly.
10

            
11
use std::sync::RwLock;
12

            
13
use azul_core::resources::UpdateMode;
14

            
15
/// The published snapshot. All fields plain owned data — this is read from
16
/// worker threads.
17
#[derive(Debug, Clone, PartialEq, Eq)]
18
pub struct AppEnv {
19
    /// Directory-safe app name (updater state dir, problem-report dir).
20
    pub app_name: String,
21
    /// The RUNNING version, the compare target for update checks.
22
    pub current_version: String,
23
    /// Requested update behaviour (clamped by install kind at check time).
24
    pub update_mode: UpdateMode,
25
    /// Update manifest URL; `None` = checks report a configuration error.
26
    pub update_manifest: Option<String>,
27
    /// App changelog (Markdown) URL, the `UpdateVersion` dialog's fallback when
28
    /// a release has no changelog link of its own.
29
    pub changelog_md: Option<String>,
30
    /// Support mailbox for problem reports; `None` = reports save to disk.
31
    pub report_problem: Option<String>,
32
    /// `AppConfig.updates.root_public_key` — the compiled-in minisign root
33
    /// key that arms the update signature chain (None = digest-only).
34
    pub update_root_public_key: Option<String>,
35
    /// `AppConfig.updates.channel` — the release channel this binary
36
    /// follows ("" = stable).
37
    pub update_channel: String,
38
}
39

            
40
impl Default for AppEnv {
41
2
    fn default() -> Self {
42
2
        Self {
43
2
            app_name: "azul-app".to_owned(),
44
2
            current_version: "0.0.0".to_owned(),
45
2
            update_mode: UpdateMode::NotifyOnly,
46
2
            update_manifest: None,
47
2
            changelog_md: None,
48
2
            report_problem: None,
49
2
            update_root_public_key: None,
50
2
            update_channel: String::new(),
51
2
        }
52
2
    }
53
}
54

            
55
impl AppEnv {
56
    /// The snapshot an [`azul_core::resources::AppConfig`] describes.
57
    #[must_use]
58
2
    pub fn from_config(config: &azul_core::resources::AppConfig) -> Self {
59
4
        let opt = |s: &azul_css::OptionString| {
60
4
            s.as_ref().map(|v| v.as_str().to_owned()).filter(|v| !v.is_empty())
61
4
        };
62
        Self {
63
2
            app_name: config.updates.app_name.as_str().to_owned(),
64
2
            current_version: config.updates.current_version.as_str().to_owned(),
65
2
            update_mode: config.updates.mode,
66
2
            update_manifest: opt(&config.updates.manifest_url),
67
            update_root_public_key: {
68
2
                let k = config.updates.root_public_key.as_str();
69
2
                if k.is_empty() { None } else { Some(k.to_owned()) }
70
            },
71
2
            update_channel: config.updates.channel.as_str().to_owned(),
72
2
            changelog_md: opt(&config.changelog_md),
73
2
            report_problem: match &config.report_problem {
74
1
                azul_core::resources::OptionEmailAddress::Some(e) => {
75
1
                    let a = e.address.as_str();
76
1
                    if a.is_empty() { None } else { Some(a.to_owned()) }
77
                }
78
1
                azul_core::resources::OptionEmailAddress::None => None,
79
            },
80
        }
81
2
    }
82
}
83

            
84
static APP_ENV: RwLock<Option<AppEnv>> = RwLock::new(None);
85

            
86
/// Publishes the snapshot (called by `App::run`; re-callable — tests and
87
/// multi-`App` processes overwrite the previous value).
88
1
pub fn set_app_env(env: AppEnv) {
89
1
    if let Ok(mut slot) = APP_ENV.write() {
90
1
        *slot = Some(env);
91
1
    }
92
1
}
93

            
94
/// The current snapshot; a default (no manifest, no mailbox) when nothing
95
/// was published.
96
#[must_use]
97
5
pub fn app_env() -> AppEnv {
98
5
    APP_ENV
99
5
        .read()
100
5
        .ok()
101
5
        .and_then(|slot| slot.clone())
102
5
        .unwrap_or_default()
103
5
}
104

            
105
#[cfg(test)]
106
mod tests {
107
    use super::*;
108

            
109
    #[test]
110
1
    fn config_round_trips_into_the_env() {
111
1
        let mut config = azul_core::resources::AppConfig::create();
112
1
        config.updates.app_name = "testapp".into();
113
1
        config.updates.current_version = "1.2.3".into();
114
1
        config.updates.mode = UpdateMode::SelfUpdate;
115
1
        config.updates.manifest_url =
116
1
            azul_css::OptionString::Some("http://localhost:1/manifest.json".into());
117
1
        config.changelog_md = azul_css::OptionString::Some("http://localhost:1/CHANGELOG.md".into());
118
1
        config.report_problem = azul_core::resources::OptionEmailAddress::Some(
119
1
            azul_core::resources::EmailAddress::new("support@example.test".into()),
120
1
        );
121
1
        let env = AppEnv::from_config(&config);
122
1
        assert_eq!(env.app_name, "testapp");
123
1
        assert_eq!(env.current_version, "1.2.3");
124
1
        assert_eq!(env.update_mode, UpdateMode::SelfUpdate);
125
1
        assert_eq!(env.update_manifest.as_deref(), Some("http://localhost:1/manifest.json"));
126
1
        assert_eq!(env.changelog_md.as_deref(), Some("http://localhost:1/CHANGELOG.md"));
127
1
        assert_eq!(env.report_problem.as_deref(), Some("support@example.test"));
128
1
    }
129

            
130
    #[test]
131
1
    fn empty_strings_read_as_unset_not_as_empty_urls() {
132
        // An empty manifest URL must NOT produce Some("") — the check would
133
        // then try to fetch "" instead of reporting "not configured".
134
1
        let config = azul_core::resources::AppConfig::create();
135
1
        let env = AppEnv::from_config(&config);
136
1
        assert_eq!(env.update_manifest, None);
137
1
        assert_eq!(env.changelog_md, None);
138
1
        assert_eq!(env.report_problem, None);
139
1
    }
140
}
141

            
142
/// What the shell's GL probe found, published the moment `query_gpu_info`
143
/// runs (the chokepoint every GL-probing platform shares). `None` until a
144
/// probe runs — a CPU-only session simply never probes.
145
#[derive(Debug, Clone, Default, PartialEq, Eq)]
146
pub struct GpuStatus {
147
    /// `GL_VENDOR`.
148
    pub vendor: String,
149
    /// `GL_RENDERER`.
150
    pub renderer: String,
151
    /// `GL_VERSION`.
152
    pub version: String,
153
    /// `GL_SHADING_LANGUAGE_VERSION`.
154
    pub glsl_version: String,
155
    /// Human-readable verdict: `ok`, `blacklisted: <reason>`, or
156
    /// `query failed: <reason>`.
157
    pub verdict: String,
158
    /// Whether GPU rendering is actually usable.
159
    pub ok: bool,
160
}
161

            
162
static GPU_STATUS: RwLock<Option<GpuStatus>> = RwLock::new(None);
163

            
164
/// Publishes the probe outcome (called from the shell's GL init).
165
pub fn set_gpu_status(status: GpuStatus) {
166
    if let Ok(mut slot) = GPU_STATUS.write() {
167
        *slot = Some(status);
168
    }
169
}
170

            
171
/// The last published probe outcome, if any probe ran.
172
#[must_use]
173
pub fn gpu_status() -> Option<GpuStatus> {
174
    GPU_STATUS.read().ok().and_then(|slot| slot.clone())
175
}
176

            
177
/// A readiness report from the driver-provisioning machinery — the layout
178
/// mirror of `azul_dll::unified::video_codec::provision::VideoStartupCheck`
179
/// (the dialogs live BELOW the dll, so the dll hands them fn pointers
180
/// instead of the type).
181
// Four independent readiness flags — that IS the report.
182
#[allow(clippy::struct_excessive_bools)]
183
#[derive(Debug, Clone, Default, PartialEq, Eq)]
184
pub struct GpuProvisionReport {
185
    /// Hardware video decode is usable right now.
186
    pub hw_decode_ready: bool,
187
    /// A fresh boot reaches a usable desktop (bootable kernel AND a display
188
    /// that lights up). `false` is URGENT — the machine is one reboot away
189
    /// from an initramfs shell or a black screen.
190
    pub boot_safe: bool,
191
    /// An automatic remediation exists (driver install and/or kernel repair).
192
    pub can_remediate: bool,
193
    /// Applying the remediation will require a reboot.
194
    pub needs_reboot: bool,
195
    /// One-line status.
196
    pub summary: String,
197
    /// Full multi-line report, including the exact commands a remediation
198
    /// would run — this is what the user consents to.
199
    pub detail: String,
200
}
201

            
202
/// What a remediation did — the mirror of `VideoProvisionOutcome`.
203
#[derive(Debug, Clone, Default, PartialEq, Eq)]
204
pub struct GpuProvisionOutcome {
205
    /// Everything applied cleanly.
206
    pub ok: bool,
207
    /// A reboot is needed before the change takes effect.
208
    pub reboot_required: bool,
209
    /// Human-readable result.
210
    pub message: String,
211
}
212

            
213
/// Progress sink a remediation reports through:
214
/// `(commands_finished, total, running_command)`.
215
pub type GpuProvisionProgressFn<'a> = &'a mut dyn FnMut(usize, usize, &str);
216

            
217
/// The dll's provisioning entry points, published by `App::run`. `check` is
218
/// INSPECTION ONLY; `remediate` is side-effecting (pkexec) and must never be
219
/// called without explicit user consent.
220
#[derive(Debug, Copy, Clone)]
221
pub struct GpuProvisionHooks {
222
    /// Runs the readiness checks. Blocking — call it on a thread.
223
    pub check: fn() -> GpuProvisionReport,
224
    /// Applies what `check` found. Blocking, side-effecting, consent-gated.
225
    /// Reports progress before each command it runs:
226
    /// `on_step(commands_finished, total, running_command)`.
227
    pub remediate: fn(GpuProvisionProgressFn<'_>) -> GpuProvisionOutcome,
228
}
229

            
230
static GPU_PROVISION: RwLock<Option<GpuProvisionHooks>> = RwLock::new(None);
231

            
232
/// Publishes the provisioning hooks (called by `App::run`).
233
pub fn set_gpu_provision_hooks(hooks: GpuProvisionHooks) {
234
    if let Ok(mut slot) = GPU_PROVISION.write() {
235
        *slot = Some(hooks);
236
    }
237
}
238

            
239
/// The provisioning hooks, if a shell published them.
240
#[must_use]
241
pub fn gpu_provision_hooks() -> Option<GpuProvisionHooks> {
242
    GPU_PROVISION.read().ok().and_then(|slot| *slot)
243
}