Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion app/src/system/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,31 @@ impl SystemInfo {
.with_cpu()
}

/// Returns the [`sysinfo::ProcessRefreshKind`] to use for a full
/// process-table sweep where we only need process identity (e.g. to check
/// whether a process with a given name is running).
///
/// Unlike [`Self::refresh_kind`], this deliberately excludes CPU and memory
/// sampling. On Windows, sampling per-process CPU usage issues an
/// `NtQueryInformationProcess(ProcessCycleTime)` call for *every* process,
/// and each such call forces `KeFlushProcessWriteBuffers` — a synchronous
/// inter-processor interrupt broadcast to all logical cores. Doing that
/// across the entire process table (potentially in bursts, e.g. once per
/// shell-session bootstrap) can keep every core spinning at
/// `DISPATCH_LEVEL` long enough to trip the DPC watchdog and bugcheck the
/// machine on high-core-count systems. Enumerating names only avoids those
/// per-process syscalls.
#[cfg_attr(not(windows), allow(dead_code))]
fn all_processes_refresh_kind() -> sysinfo::ProcessRefreshKind {
sysinfo::ProcessRefreshKind::nothing()
}

#[cfg_attr(not(windows), allow(dead_code))]
pub fn refresh_all_processes(&mut self) {
self.system.refresh_processes_specifics(
ProcessesToUpdate::All,
true, /* remove_dead_processes */
Self::refresh_kind(),
Self::all_processes_refresh_kind(),
);
}

Expand Down
30 changes: 30 additions & 0 deletions app/src/system/info_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,33 @@ fn test_memory_usage_stats_construction() {
assert_eq!(stats.inactive_24h_stats.num_blocks, 1);
assert_eq!(stats.inactive_24h_stats.num_lines, 0);
}

/// Regression test for the Windows DPC_WATCHDOG_VIOLATION caused by
/// enumerating the full process table with per-process CPU sampling.
///
/// The full-table sweep ([`SystemInfo::refresh_all_processes`], used only to
/// check whether a process with a given name is running) must NOT request CPU
/// or memory data: on Windows, per-process CPU sampling issues
/// `NtQueryInformationProcess(ProcessCycleTime)` for every process, each
/// forcing an all-core `KeFlushProcessWriteBuffers` IPI. The single-PID
/// self-poll ([`SystemInfo::refresh_kind`]) legitimately still samples both.
#[test]
fn all_processes_refresh_kind_does_not_sample_cpu_or_memory() {
let all = SystemInfo::all_processes_refresh_kind();
assert!(
!all.cpu(),
"full process-table sweep must not sample per-process CPU (forces \
NtQueryInformationProcess(ProcessCycleTime) -> all-core IPI per process)"
);
assert!(
!all.memory(),
"full process-table sweep only needs process names, not memory"
);

// The single-PID self-poll should still gather CPU/memory (cheap: one PID).
let self_poll = SystemInfo::refresh_kind();
assert!(
self_poll.cpu() && self_poll.memory(),
"current-process self-poll should still sample CPU and memory"
);
}
26 changes: 23 additions & 3 deletions app/src/util/windows.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::sync::{LazyLock, OnceLock};
use std::{env, path};

use anyhow::{Result, anyhow};
Expand Down Expand Up @@ -198,14 +198,34 @@ fn microsoft_store_app_path() -> Option<PathBuf> {
Some(microsoft_store_app_path)
}

/// Caches whether Kaspersky was detected, computed once per process launch.
///
/// See [`is_kaspersky_running`] for why this is cached.
static KASPERSKY_RUNNING: OnceLock<bool> = OnceLock::new();

/// Determines if Kaspersky is currently running by checking if there is a
/// process with the name "avp" running.
///
/// The result is cached for the lifetime of the process. Antivirus presence
/// does not meaningfully change during a Warp session, and the underlying
/// check enumerates the entire process table (see
/// [`SystemInfo::refresh_all_processes`]) — expensive on Windows. Without this
/// cache the sweep ran on *every* session bootstrap (every tab/pane/subshell),
/// which on high-core-count machines could trip the DPC watchdog. Caching
/// turns it into a single one-time check.
pub fn is_kaspersky_running(ctx: &mut AppContext) -> bool {
SystemInfo::handle(ctx).update(ctx, |system_info, _| {
if let Some(cached) = KASPERSKY_RUNNING.get() {
return *cached;
}

let running = SystemInfo::handle(ctx).update(ctx, |system_info, _| {
system_info.refresh_all_processes();
system_info
.processes_by_name(KASPERSKY_PROCESS_NAME)
.next()
.is_some()
})
});

// If another caller raced us, keep the first value that was stored.
*KASPERSKY_RUNNING.get_or_init(|| running)
}