Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e610bdb4b | |||
| 9dfd1829e9 |
@ -40,7 +40,7 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
|
|||||||
- `users.rs` — Aktuellen User via nix getuid, Avatar-Loading mit Symlink-Rejection
|
- `users.rs` — Aktuellen User via nix getuid, Avatar-Loading mit Symlink-Rejection
|
||||||
- `power.rs` — Reboot/Shutdown via /usr/bin/systemctl
|
- `power.rs` — Reboot/Shutdown via /usr/bin/systemctl
|
||||||
- `i18n.rs` — Locale-Erkennung (OnceLock-cached) und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts
|
- `i18n.rs` — Locale-Erkennung (OnceLock-cached) und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts
|
||||||
- `config.rs` — TOML-Config (background_path, background_blur clamped [0,100], fingerprint_enabled als Option<bool>) + Wallpaper-Fallback + Symlink-Rejection via symlink_metadata + Parse-Error-Logging
|
- `config.rs` — TOML-Config (background_path, background_blur clamped [0,200], fingerprint_enabled als Option<bool>) + Wallpaper-Fallback + Symlink-Rejection via symlink_metadata + Parse-Error-Logging
|
||||||
- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking mit 30s Timeout und Generation Counter, FP-Label/Start separat verdrahtet mit pam_acct_mgmt-Check und auto-resume, Zeroizing<String> für Passwort, Power-Confirm, GPU-Blur via GskBlurNode (Downscale auf max 1920px), Blur/Avatar-Cache für Multi-Monitor
|
- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking mit 30s Timeout und Generation Counter, FP-Label/Start separat verdrahtet mit pam_acct_mgmt-Check und auto-resume, Zeroizing<String> für Passwort, Power-Confirm, GPU-Blur via GskBlurNode (Downscale auf max 1920px), Blur/Avatar-Cache für Multi-Monitor
|
||||||
- `main.rs` — Entry Point, Panic-Hook (vor Logging), Root-Check, ext-session-lock-v1 (Pflicht in Release), Monitor-Hotplug via `connect_monitor`-Signal (v1_2), shared Blur/Avatar-Caches in Rc, systemd-Journal-Logging, Debug-Level per `MOONLOCK_DEBUG` Env-Var, async fprintd-Init nach window.present()
|
- `main.rs` — Entry Point, Panic-Hook (vor Logging), Root-Check, ext-session-lock-v1 (Pflicht in Release), Monitor-Hotplug via `connect_monitor`-Signal (v1_2), shared Blur/Avatar-Caches in Rc, systemd-Journal-Logging, Debug-Level per `MOONLOCK_DEBUG` Env-Var, async fprintd-Init nach window.present()
|
||||||
|
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -575,7 +575,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moonlock"
|
name = "moonlock"
|
||||||
version = "0.6.9"
|
version = "0.6.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moonlock"
|
name = "moonlock"
|
||||||
version = "0.6.10"
|
version = "0.6.12"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@ -28,6 +28,6 @@ tempfile = "3"
|
|||||||
glib-build-tools = "0.22"
|
glib-build-tools = "0.22"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "fat"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
14
DECISIONS.md
14
DECISIONS.md
@ -2,6 +2,20 @@
|
|||||||
|
|
||||||
Architectural and design decisions for Moonlock, in reverse chronological order.
|
Architectural and design decisions for Moonlock, in reverse chronological order.
|
||||||
|
|
||||||
|
## 2026-04-24 – Audit LOW fixes: docs, rustdoc, check_account scope, debug gating, lto fat (v0.6.12)
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: Six LOW findings cleared in a single pass. (1) Docs referenced the old `[0,100]` blur range; code clamps `[0,200]` since v0.6.8. (2) The `MAX_BLUR_DIMENSION` doc comment was split by a `// SYNC:` block, producing a truncated sentence in rustdoc. (3) `check_account` was `pub` and relied on callers only ever passing `getuid()`-derived usernames; the contract was not enforced by the type system. (4) `MOONLOCK_DEBUG` env var flipped log verbosity to Debug in release builds, letting a compromised session script escalate journal noise about fprintd / D-Bus. (5) `pam_setcred` absence was undocumented. (6) `[profile.release]` used `lto = "thin"` — fine for most crates, but for a latency-critical auth binary compiled once per release, fat LTO's extra cross-crate inlining is worth the ~1 min build hit.
|
||||||
|
- **Tradeoffs**: `lto = "fat"` roughly doubles release build time (~30 s → ~60 s) for slightly better inlining across PAM FFI wrappers and the i18n/status paths. `#[cfg(debug_assertions)]` on the debug-level selector means you have to run a debug build to raise log level — inconvenient for live troubleshooting, but aligned with the security-first posture.
|
||||||
|
- **How**: (1) `CLAUDE.md` + `README.md` updated to `[0,200]`. (2) `// SYNC:` block moved above the `///` doc so rustdoc renders one coherent paragraph. (3) `check_account` visibility narrowed to `pub(crate)` with a `Precondition` paragraph explaining the username contract. (4) Debug-level selection wrapped in `#[cfg(debug_assertions)]`; release builds always run at `LevelFilter::Info`. (5) Added a comment block in `authenticate()` documenting why `pam_setcred` is deliberately absent and where it would hook in if needed. (6) `lto = "fat"` in `Cargo.toml`.
|
||||||
|
|
||||||
|
## 2026-04-24 – Audit MEDIUM fixes: D-Bus cleanup race, TOCTOU open, FP reset, GTK entry clear (v0.6.11)
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: Second round after the HIGH fixes, addressing the four MEDIUM findings. (1) `cleanup_dbus` spawned VerifyStop + Release as fire-and-forget, then `resume_async` called Claim after only a 2 s timeout — shorter than the 3 s D-Bus timeout, so on a slow bus the Claim could race the Release and fprintd would reject it, leaving the FP listener permanently dead. (2) `load_background_texture` relied on the caller's `symlink_metadata` check, re-opening the path via `gdk::Texture::from_file` — a classic TOCTOU window. (3) `resume_async` unconditionally reset `failed_attempts`, allowing an attacker with sensor control to evade the 10-attempt cap by cycling verify-match → `check_account` fail → resume. (4) The GTK `PasswordEntry` buffer was only cleared on timeout or auth failure, leaving the password in GLib malloc'd memory longer than necessary.
|
||||||
|
- **Tradeoffs**: The D-Bus cleanup is now split into a synchronous helper (`take_cleanup_proxy` — signal disconnect + flag clear) and an async helper (`perform_dbus_cleanup` — VerifyStop + Release), so `resume_async` can await the release while `stop()` stays fire-and-forget. Dropping the `failed_attempts` reset means a flaky sensor could reach 10 failures faster, but the correct remedy is a new lock session (construction) rather than a reset that also helps attackers.
|
||||||
|
- **How**: (1) Split `cleanup_dbus` into `take_cleanup_proxy()` (sync) + `perform_dbus_cleanup(proxy)` (async). `resume_async` now awaits `perform_dbus_cleanup` before `begin_verification`. `stop()` still spawns the cleanup fire-and-forget. (2) `load_background_texture` opens with `O_NOFOLLOW` via `std::fs::OpenOptions::custom_flags`, reads to bytes, and builds the texture via `gdk::Texture::from_bytes`. (3) Removed `listener.borrow_mut().failed_attempts = 0` from `resume_async`. (4) `password_entry.set_text("")` now fires right after the `Zeroizing::new(entry.text().to_string())` extraction, shortening the GTK-side window.
|
||||||
|
|
||||||
## 2026-04-24 – Audit fixes: RefCell borrow across await, async avatar decode
|
## 2026-04-24 – Audit fixes: RefCell borrow across await, async avatar decode
|
||||||
|
|
||||||
- **Who**: ClaudeCode, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
|
|||||||
@ -9,7 +9,7 @@ Part of the Moonarch ecosystem.
|
|||||||
- **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`) with 30s timeout and generation counter
|
- **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`) with 30s timeout and generation counter
|
||||||
- **Fingerprint unlock** — fprintd D-Bus integration with sender validation, async init (window appears instantly), `pam_acct_mgmt` check after verify, auto-resume on transient errors
|
- **Fingerprint unlock** — fprintd D-Bus integration with sender validation, async init (window appears instantly), `pam_acct_mgmt` check after verify, auto-resume on transient errors
|
||||||
- **Multi-monitor + hotplug** — Lockscreen on every monitor with shared blur and avatar caches; monitors added after suspend/resume get windows automatically via `connect_monitor` signal
|
- **Multi-monitor + hotplug** — Lockscreen on every monitor with shared blur and avatar caches; monitors added after suspend/resume get windows automatically via `connect_monitor` signal
|
||||||
- **GPU blur** — Background blur via GskBlurNode (downscale to max 1920px, configurable 0–100)
|
- **GPU blur** — Background blur via GskBlurNode (downscale to max 1920px, configurable 0–200)
|
||||||
- **i18n** — German and English (auto-detected)
|
- **i18n** — German and English (auto-detected)
|
||||||
- **Faillock warning** — Progressive UI warning after failed attempts, PAM decides lockout
|
- **Faillock warning** — Progressive UI warning after failed attempts, PAM decides lockout
|
||||||
- **Panic safety** — Panic hook logs but never unlocks (installed before logging)
|
- **Panic safety** — Panic hook logs but never unlocks (installed before logging)
|
||||||
@ -48,7 +48,7 @@ Create `/etc/moonlock/moonlock.toml` or `~/.config/moonlock/moonlock.toml`:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
background_path = "/usr/share/wallpapers/moon.jpg"
|
background_path = "/usr/share/wallpapers/moon.jpg"
|
||||||
background_blur = 40.0 # 0.0–100.0, optional
|
background_blur = 40.0 # 0.0–200.0, optional
|
||||||
fingerprint_enabled = true
|
fingerprint_enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
14
src/auth.rs
14
src/auth.rs
@ -191,7 +191,12 @@ pub fn authenticate(username: &str, password: &str) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safety: handle is valid and non-null after successful pam_start
|
// Safety: handle is valid and non-null after successful pam_start.
|
||||||
|
// Note: pam_setcred is intentionally NOT called here. A lockscreen unlocks
|
||||||
|
// an existing session whose credentials were already established at login;
|
||||||
|
// refreshing them would duplicate work done by the session's login manager.
|
||||||
|
// If per-unlock credential refresh (Kerberos tickets, pam_gnome_keyring)
|
||||||
|
// is ever desired, hook it here with PAM_ESTABLISH_CRED.
|
||||||
let auth_ret = unsafe { pam_authenticate(handle, 0) };
|
let auth_ret = unsafe { pam_authenticate(handle, 0) };
|
||||||
let acct_ret = if auth_ret == PAM_SUCCESS {
|
let acct_ret = if auth_ret == PAM_SUCCESS {
|
||||||
// Safety: handle is valid, check account restrictions
|
// Safety: handle is valid, check account restrictions
|
||||||
@ -211,7 +216,12 @@ pub fn authenticate(username: &str, password: &str) -> bool {
|
|||||||
/// Used after fingerprint unlock to enforce account policies (lockout, expiry)
|
/// Used after fingerprint unlock to enforce account policies (lockout, expiry)
|
||||||
/// that would otherwise be bypassed when not going through pam_authenticate.
|
/// that would otherwise be bypassed when not going through pam_authenticate.
|
||||||
/// Returns true if the account is valid and allowed to log in.
|
/// Returns true if the account is valid and allowed to log in.
|
||||||
pub fn check_account(username: &str) -> bool {
|
///
|
||||||
|
/// **Precondition**: `username` must be the authenticated system user, derived
|
||||||
|
/// via `users::get_current_user()` (which reads `getuid()`). Calling this with
|
||||||
|
/// an attacker-controlled username is unsafe — `pam_acct_mgmt` returns SUCCESS
|
||||||
|
/// for any valid unlocked account, giving a trivial unlock bypass.
|
||||||
|
pub(crate) fn check_account(username: &str) -> bool {
|
||||||
let service = match CString::new("moonlock") {
|
let service = match CString::new("moonlock") {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return false,
|
Err(_) => return false,
|
||||||
|
|||||||
@ -177,12 +177,24 @@ impl FingerprintListener {
|
|||||||
|
|
||||||
/// Resume fingerprint verification after a transient interruption (e.g. failed
|
/// Resume fingerprint verification after a transient interruption (e.g. failed
|
||||||
/// PAM account check). Reuses previously stored callbacks. Re-claims the device
|
/// PAM account check). Reuses previously stored callbacks. Re-claims the device
|
||||||
/// and restarts verification from scratch.
|
/// and restarts verification from scratch. Awaits any in-flight VerifyStop +
|
||||||
|
/// Release before re-claiming the device so fprintd does not reject the Claim
|
||||||
|
/// while the previous session is still being torn down.
|
||||||
pub async fn resume_async(
|
pub async fn resume_async(
|
||||||
listener: &Rc<RefCell<FingerprintListener>>,
|
listener: &Rc<RefCell<FingerprintListener>>,
|
||||||
username: &str,
|
username: &str,
|
||||||
) {
|
) {
|
||||||
listener.borrow_mut().failed_attempts = 0;
|
// Drain in-flight cleanup so the device is actually released before Claim.
|
||||||
|
// Without this, a fast resume after on_verify_status's fire-and-forget
|
||||||
|
// cleanup races the Release call and fprintd returns "already claimed".
|
||||||
|
let proxy = listener.borrow_mut().take_cleanup_proxy();
|
||||||
|
if let Some(proxy) = proxy {
|
||||||
|
Self::perform_dbus_cleanup(proxy).await;
|
||||||
|
}
|
||||||
|
// Deliberately do NOT reset failed_attempts here. An attacker with sensor
|
||||||
|
// control could otherwise cycle verify-match → check_account fail → resume,
|
||||||
|
// and the 10-attempt cap would never trigger. The counter decays only via
|
||||||
|
// a fresh lock session (listener construction).
|
||||||
Self::begin_verification(listener, username).await;
|
Self::begin_verification(listener, username).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,26 +364,37 @@ impl FingerprintListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disconnect the signal handler and send VerifyStop + Release to fprintd.
|
/// Disconnect the signal handler and clear running flags. Returns the proxy
|
||||||
/// Signal disconnect is synchronous to prevent further callbacks.
|
/// the caller should use for the async D-Bus cleanup (VerifyStop + Release).
|
||||||
/// D-Bus cleanup is fire-and-forget to avoid blocking the UI.
|
///
|
||||||
fn cleanup_dbus(&mut self) {
|
/// Split into a sync part (signal disconnect, flags) and an async part
|
||||||
|
/// (`perform_dbus_cleanup`) so callers can either spawn the async work
|
||||||
|
/// fire-and-forget (via `cleanup_dbus`) or await it to serialize with a
|
||||||
|
/// subsequent Claim (via `resume_async`).
|
||||||
|
fn take_cleanup_proxy(&mut self) -> Option<gio::DBusProxy> {
|
||||||
self.running = false;
|
self.running = false;
|
||||||
self.running_flag.set(false);
|
self.running_flag.set(false);
|
||||||
|
|
||||||
if let Some(ref proxy) = self.device_proxy {
|
let proxy = self.device_proxy.clone()?;
|
||||||
if let Some(id) = self.signal_id.take() {
|
if let Some(id) = self.signal_id.take() {
|
||||||
proxy.disconnect(id);
|
proxy.disconnect(id);
|
||||||
}
|
}
|
||||||
let proxy = proxy.clone();
|
Some(proxy)
|
||||||
glib::spawn_future_local(async move {
|
}
|
||||||
let _ = proxy
|
|
||||||
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
async fn perform_dbus_cleanup(proxy: gio::DBusProxy) {
|
||||||
.await;
|
let _ = proxy
|
||||||
let _ = proxy
|
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
.await;
|
||||||
.await;
|
let _ = proxy
|
||||||
});
|
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fire-and-forget cleanup for code paths that cannot await (e.g. drop, stop).
|
||||||
|
fn cleanup_dbus(&mut self) {
|
||||||
|
if let Some(proxy) = self.take_cleanup_proxy() {
|
||||||
|
glib::spawn_future_local(Self::perform_dbus_cleanup(proxy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -244,6 +244,10 @@ pub fn create_lockscreen_window(
|
|||||||
if password.is_empty() {
|
if password.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Clear the GTK entry's internal buffer as early as possible. GTK allocates
|
||||||
|
// the backing GString via libc malloc, which zeroize cannot reach — the
|
||||||
|
// best we can do is shorten the window during which it resides in memory.
|
||||||
|
entry.set_text("");
|
||||||
|
|
||||||
entry.set_sensitive(false);
|
entry.set_sensitive(false);
|
||||||
let username = username.clone();
|
let username = username.clone();
|
||||||
@ -518,12 +522,35 @@ pub fn start_fingerprint(
|
|||||||
/// Load the wallpaper as a texture once, for sharing across all windows.
|
/// Load the wallpaper as a texture once, for sharing across all windows.
|
||||||
/// Returns None if no wallpaper path is provided or the file cannot be loaded.
|
/// Returns None if no wallpaper path is provided or the file cannot be loaded.
|
||||||
/// Blur is applied at render time via GPU (GskBlurNode), not here.
|
/// Blur is applied at render time via GPU (GskBlurNode), not here.
|
||||||
|
///
|
||||||
|
/// Opens the file with O_NOFOLLOW to close the TOCTOU window between the
|
||||||
|
/// symlink check in `resolve_background_path_with` and this read. If the path
|
||||||
|
/// was swapped for a symlink after the check, `open` fails with ELOOP.
|
||||||
pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
||||||
let file = gio::File::for_path(bg_path);
|
use std::io::Read;
|
||||||
match gdk::Texture::from_file(&file) {
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
|
||||||
|
let mut file = match std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.custom_flags(libc::O_NOFOLLOW)
|
||||||
|
.open(bg_path)
|
||||||
|
{
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to open wallpaper {}: {e}", bg_path.display());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
if let Err(e) = file.read_to_end(&mut bytes) {
|
||||||
|
log::warn!("Failed to read wallpaper {}: {e}", bg_path.display());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let glib_bytes = glib::Bytes::from_owned(bytes);
|
||||||
|
match gdk::Texture::from_bytes(&glib_bytes) {
|
||||||
Ok(texture) => Some(texture),
|
Ok(texture) => Some(texture),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to load wallpaper {}: {e}", bg_path.display());
|
log::warn!("Failed to decode wallpaper {}: {e}", bg_path.display());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -565,11 +592,11 @@ fn create_background_picture(
|
|||||||
background
|
background
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximum texture dimension for blur input. Textures larger than this are
|
|
||||||
// SYNC: MAX_BLUR_DIMENSION, render_blurred_texture, and create_background_picture
|
// SYNC: MAX_BLUR_DIMENSION, render_blurred_texture, and create_background_picture
|
||||||
// are duplicated in moongreet/src/greeter.rs and moonset/src/panel.rs.
|
// are duplicated in moongreet/src/greeter.rs and moonset/src/panel.rs.
|
||||||
// Changes here must be mirrored to the other two projects.
|
// Changes here must be mirrored to the other two projects.
|
||||||
|
|
||||||
|
/// Maximum texture dimension for blur input. Textures larger than this are
|
||||||
/// downscaled before blurring — the blur destroys detail anyway, so there is
|
/// downscaled before blurring — the blur destroys detail anyway, so there is
|
||||||
/// no visible quality loss, but GPU work is reduced significantly.
|
/// no visible quality loss, but GPU work is reduced significantly.
|
||||||
const MAX_BLUR_DIMENSION: f32 = 1920.0;
|
const MAX_BLUR_DIMENSION: f32 = 1920.0;
|
||||||
|
|||||||
@ -250,11 +250,17 @@ fn setup_logging() {
|
|||||||
eprintln!("Failed to create journal logger: {e}");
|
eprintln!("Failed to create journal logger: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Debug level is only selectable in debug builds. Release binaries ignore
|
||||||
|
// MOONLOCK_DEBUG so a session script cannot escalate log verbosity to leak
|
||||||
|
// fprintd / D-Bus internals into the journal.
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
let level = if std::env::var("MOONLOCK_DEBUG").is_ok() {
|
let level = if std::env::var("MOONLOCK_DEBUG").is_ok() {
|
||||||
log::LevelFilter::Debug
|
log::LevelFilter::Debug
|
||||||
} else {
|
} else {
|
||||||
log::LevelFilter::Info
|
log::LevelFilter::Info
|
||||||
};
|
};
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let level = log::LevelFilter::Info;
|
||||||
log::set_max_level(level);
|
log::set_max_level(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user