fix(windows): silence conhost flashes from core-side spawns (#731/#1338 follow-up) (#1498)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
sanil-23
2026-05-11 12:07:30 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 089ffe0d06
commit f0606ba703
9 changed files with 121 additions and 45 deletions
+22 -14
View File
@@ -509,17 +509,20 @@ fn available_disk_space_mb_windows(path: &Path) -> Option<u64> {
// PowerShell is ubiquitous on supported Windows; `Get-PSDrive` needs no admin
// and returns free bytes as a single integer line.
let script = format!("(Get-PSDrive -Name {letter} -ErrorAction Stop).Free");
let output = std::process::Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
&script,
])
.output()
.ok()?;
let mut cmd = std::process::Command::new("powershell");
cmd.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
&script,
]);
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
let output = cmd.output().ok()?;
if !output.status.success() {
return None;
}
@@ -729,12 +732,17 @@ fn check_command_available(
cat: &'static str,
items: &mut Vec<DiagnosticItem>,
) {
match std::process::Command::new(cmd)
let mut child_cmd = std::process::Command::new(cmd);
child_cmd
.args(args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.stderr(std::process::Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
child_cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
match child_cmd.output() {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout)
.lines()
+9 -5
View File
@@ -99,12 +99,16 @@ fn detect_gpu(cpu_brand: &str, os_name: &str) -> (bool, Option<String>) {
/// Probe for an NVIDIA GPU by running `nvidia-smi --query-gpu=name --format=csv,noheader`.
/// Returns `Some("NVIDIA <name> (CUDA)")` on success, `None` if nvidia-smi is not available.
fn probe_nvidia_smi() -> Option<String> {
let output = std::process::Command::new("nvidia-smi")
.args(["--query-gpu=name", "--format=csv,noheader"])
let mut cmd = std::process::Command::new("nvidia-smi");
cmd.args(["--query-gpu=name", "--format=csv,noheader"])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
.stderr(std::process::Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
let output = cmd.output().ok()?;
if !output.status.success() {
return None;
+1
View File
@@ -37,6 +37,7 @@ fn build_install_command(install_dir: &Path) -> Result<tokio::process::Command,
#[cfg(target_os = "windows")]
{
let mut cmd = tokio::process::Command::new("powershell");
crate::openhuman::local_ai::process_util::apply_no_window(&mut cmd);
cmd.env("OPENHUMAN_OLLAMA_INSTALL_DIR", install_dir);
cmd.args([
"-NoProfile",
+1
View File
@@ -15,6 +15,7 @@ pub mod sentiment;
mod install;
pub(crate) mod model_ids;
mod ollama_api;
mod process_util;
pub(crate) use ollama_api::{ollama_base_url, OLLAMA_BASE_URL};
mod parse;
pub(crate) mod paths;
+40
View File
@@ -0,0 +1,40 @@
//! Process-spawn helpers for local AI subsystems.
//!
//! On Windows every `Command::spawn` allocates a conhost for the child
//! before stdio inheritance is applied — so a `Stdio::null()` redirect
//! still flashes a console window. `apply_no_window` sets the
//! `CREATE_NO_WINDOW` (0x0800_0000) process-creation flag which tells
//! the OS to skip conhost allocation entirely.
//!
//! Mirrors the pattern established by #731 and #1338 for the Tauri-shell
//! side (see `app/src-tauri/src/core_process.rs` and `process_kill.rs`).
//! Without this every Ollama health-check / install attempt flashes a
//! console on Windows; on a fresh install without Ollama present the
//! flashes are continuous because the resolve-or-install loop retries.
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
#[cfg(windows)]
pub(crate) fn apply_no_window(cmd: &mut tokio::process::Command) {
use std::os::windows::process::CommandExt;
cmd.creation_flags(CREATE_NO_WINDOW);
}
#[cfg(not(windows))]
pub(crate) fn apply_no_window(_cmd: &mut tokio::process::Command) {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_no_window_is_callable_on_every_platform() {
// The function is a no-op on non-Windows. On Windows it sets a
// creation flag we cannot directly read back from
// `tokio::process::Command`, so this test just guarantees the
// helper compiles and is callable from generic code.
let mut cmd = tokio::process::Command::new("does-not-need-to-exist");
apply_no_window(&mut cmd);
}
}
+21 -21
View File
@@ -11,6 +11,7 @@ use crate::openhuman::local_ai::ollama_api::{
};
use crate::openhuman::local_ai::paths::{find_workspace_ollama_binary, workspace_ollama_binary};
use crate::openhuman::local_ai::presets::{self, VisionMode};
use crate::openhuman::local_ai::process_util::apply_no_window;
use super::LocalAiService;
@@ -59,25 +60,26 @@ impl LocalAiService {
return Ok(());
}
if let Err(err) = tokio::process::Command::new(ollama_cmd)
let mut version_cmd = tokio::process::Command::new(ollama_cmd);
version_cmd
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
{
.stderr(std::process::Stdio::null());
apply_no_window(&mut version_cmd);
if let Err(err) = version_cmd.status().await {
return Err(format!(
"Ollama binary not available ({}; error: {err}).",
ollama_cmd.display()
));
}
match tokio::process::Command::new(ollama_cmd)
let mut serve_cmd = tokio::process::Command::new(ollama_cmd);
serve_cmd
.arg("serve")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
.stderr(std::process::Stdio::null());
apply_no_window(&mut serve_cmd);
match serve_cmd.spawn() {
Ok(_) => {
log::debug!(
"[local_ai] spawned `ollama serve` from {}",
@@ -170,14 +172,12 @@ impl LocalAiService {
}
async fn command_works(&self, command: &Path) -> bool {
tokio::process::Command::new(command)
.arg("--version")
let mut cmd = tokio::process::Command::new(command);
cmd.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false)
.stderr(std::process::Stdio::null());
apply_no_window(&mut cmd);
cmd.status().await.map(|s| s.success()).unwrap_or(false)
}
async fn download_and_install_ollama(&self, config: &Config) -> Result<(), String> {
@@ -895,12 +895,12 @@ impl LocalAiService {
}
#[cfg(windows)]
{
let _ = tokio::process::Command::new("taskkill")
.args(["/F", "/IM", "ollama.exe"])
let mut cmd = tokio::process::Command::new("taskkill");
cmd.args(["/F", "/IM", "ollama.exe"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await;
.stderr(std::process::Stdio::null());
apply_no_window(&mut cmd);
let _ = cmd.status().await;
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
}
+9 -5
View File
@@ -208,13 +208,17 @@ fn probe_subcommand_version(path: &std::path::Path, label: &str) -> Option<Strin
use std::io::Read;
use wait_timeout::ChildExt;
let mut child = Command::new(path)
.arg("--version")
let mut cmd = Command::new(path);
cmd.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
let mut child = cmd.spawn().ok()?;
let timeout = Duration::from_secs(5);
let status = match child.wait_timeout(timeout).ok()? {
+16
View File
@@ -102,7 +102,20 @@ pub(crate) fn xml_escape(input: &str) -> String {
.replace('\'', "&apos;")
}
/// Suppress conhost allocation for Windows command spawns. Without this,
/// every `schtasks /Query` polled by service status checks flashes a
/// console — visible to users (#1475 follow-up to #731 + #1338).
#[cfg(windows)]
fn no_window(cmd: &mut Command) {
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
#[cfg(not(windows))]
fn no_window(_cmd: &mut Command) {}
pub(crate) fn run_checked(cmd: &mut Command) -> Result<()> {
no_window(cmd);
let status = cmd.status()?;
if !status.success() {
anyhow::bail!("command failed with status {status}");
@@ -111,6 +124,7 @@ pub(crate) fn run_checked(cmd: &mut Command) -> Result<()> {
}
pub(crate) fn run_capture(cmd: &mut Command) -> Result<String> {
no_window(cmd);
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!("command failed with status {}", output.status);
@@ -120,6 +134,7 @@ pub(crate) fn run_capture(cmd: &mut Command) -> Result<String> {
}
pub(crate) fn run_best_effort(cmd: &mut Command) {
no_window(cmd);
match cmd.stdout(Stdio::null()).stderr(Stdio::null()).status() {
Ok(status) => {
if !status.success() {
@@ -133,6 +148,7 @@ pub(crate) fn run_best_effort(cmd: &mut Command) {
}
pub(crate) fn run_check_silent(cmd: &mut Command) -> bool {
no_window(cmd);
cmd.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
+2
View File
@@ -131,8 +131,10 @@ pub(crate) fn uninstall(config: &Config) -> Result<ServiceStatus> {
}
fn is_task_exists_windows(task_name: &str) -> Result<bool> {
use std::os::windows::process::CommandExt;
let result = Command::new("schtasks")
.args(["/Query", "/TN", task_name])
.creation_flags(0x0800_0000) // CREATE_NO_WINDOW
.output();
match result {