mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
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:
co-authored by
Claude Opus 4.7
parent
089ffe0d06
commit
f0606ba703
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()? {
|
||||
|
||||
@@ -102,7 +102,20 @@ pub(crate) fn xml_escape(input: &str) -> String {
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
/// 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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user