Prevent Windows console window for yaaknode and yaakprotoc

Add new_xplatform_command() helper in yaak-common that creates a
tokio::process::Command with CREATE_NO_WINDOW flag set on Windows.

Also converts git commands to async for consistency.
This commit is contained in:
Gregory Schier
2026-01-11 15:07:56 -08:00
parent 72a7e6963d
commit 42143249a2
15 changed files with 102 additions and 69 deletions

4
Cargo.lock generated
View File

@@ -8075,6 +8075,7 @@ name = "yaak-common"
version = "0.1.0"
dependencies = [
"serde_json",
"tokio",
]
[[package]]
@@ -8121,8 +8122,10 @@ dependencies = [
"serde_json",
"serde_yaml",
"thiserror 2.0.17",
"tokio",
"ts-rs",
"url",
"yaak-common",
"yaak-models",
"yaak-sync",
]
@@ -8149,6 +8152,7 @@ dependencies = [
"tonic",
"tonic-reflection",
"uuid",
"yaak-common",
"yaak-tls",
]

View File

@@ -6,11 +6,10 @@ use crate::error::Result;
use std::path::{Path, PathBuf};
use tauri::command;
use yaak_git::{
GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult,
git_add, git_add_credential, git_add_remote, git_checkout_branch, git_commit,
git_create_branch, git_delete_branch, git_fetch_all, git_init, git_log,
git_merge_branch, git_pull, git_push, git_remotes, git_rm_remote, git_status,
git_unstage,
GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult, git_add, git_add_credential,
git_add_remote, git_checkout_branch, git_commit, git_create_branch, git_delete_branch,
git_fetch_all, git_init, git_log, git_merge_branch, git_pull, git_push, git_remotes,
git_rm_remote, git_status, git_unstage,
};
// NOTE: All of these commands are async to prevent blocking work from locking up the UI
@@ -52,22 +51,22 @@ pub async fn cmd_git_initialize(dir: &Path) -> Result<()> {
#[command]
pub async fn cmd_git_commit(dir: &Path, message: &str) -> Result<()> {
Ok(git_commit(dir, message)?)
Ok(git_commit(dir, message).await?)
}
#[command]
pub async fn cmd_git_fetch_all(dir: &Path) -> Result<()> {
Ok(git_fetch_all(dir)?)
Ok(git_fetch_all(dir).await?)
}
#[command]
pub async fn cmd_git_push(dir: &Path) -> Result<PushResult> {
Ok(git_push(dir)?)
Ok(git_push(dir).await?)
}
#[command]
pub async fn cmd_git_pull(dir: &Path) -> Result<PullResult> {
Ok(git_pull(dir)?)
Ok(git_pull(dir).await?)
}
#[command]

View File

@@ -6,3 +6,4 @@ publish = false
[dependencies]
serde_json = { workspace = true }
tokio = { workspace = true, features = ["process"] }

View File

@@ -0,0 +1,16 @@
use std::ffi::OsStr;
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
/// Creates a new `tokio::process::Command` that won't spawn a console window on Windows.
pub fn new_xplatform_command<S: AsRef<OsStr>>(program: S) -> tokio::process::Command {
#[allow(unused_mut)]
let mut cmd = tokio::process::Command::new(program);
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd
}

View File

@@ -1,2 +1,3 @@
pub mod command;
pub mod platform;
pub mod serde;

View File

@@ -12,7 +12,9 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = "0.9.34"
thiserror = { workspace = true }
tokio = { workspace = true, features = ["io-util"] }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
url = "2"
yaak-common = { workspace = true }
yaak-models = { workspace = true }
yaak-sync = { workspace = true }

View File

@@ -1,38 +1,24 @@
use crate::error::Error::GitNotFound;
use crate::error::Result;
use std::path::Path;
use std::process::{Command, Stdio};
use std::process::Stdio;
use tokio::process::Command;
use yaak_common::command::new_xplatform_command;
use crate::error::Error::GitNotFound;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
pub(crate) fn new_binary_command(dir: &Path) -> Result<Command> {
pub(crate) async fn new_binary_command(dir: &Path) -> Result<Command> {
// 1. Probe that `git` exists and is runnable
let mut probe = Command::new("git");
let mut probe = new_xplatform_command("git");
probe.arg("--version").stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
#[cfg(target_os = "windows")]
{
probe.creation_flags(CREATE_NO_WINDOW);
}
let status = probe.status().map_err(|_| GitNotFound)?;
let status = probe.status().await.map_err(|_| GitNotFound)?;
if !status.success() {
return Err(GitNotFound);
}
// 2. Build the reusable git command
let mut cmd = Command::new("git");
let mut cmd = new_xplatform_command("git");
cmd.arg("-C").arg(dir);
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}
Ok(cmd)
}

View File

@@ -3,8 +3,9 @@ use crate::error::Error::GenericError;
use log::info;
use std::path::Path;
pub fn git_commit(dir: &Path, message: &str) -> crate::error::Result<()> {
let out = new_binary_command(dir)?.args(["commit", "--message", message]).output()?;
pub async fn git_commit(dir: &Path, message: &str) -> crate::error::Result<()> {
let out =
new_binary_command(dir).await?.args(["commit", "--message", message]).output().await?;
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);

View File

@@ -1,9 +1,9 @@
use crate::binary::new_binary_command;
use crate::error::Error::GenericError;
use crate::error::Result;
use std::io::Write;
use std::path::Path;
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
use url::Url;
pub async fn git_add_credential(
@@ -18,7 +18,8 @@ pub async fn git_add_credential(
let host = url.host_str().unwrap();
let path = Some(url.path());
let mut child = new_binary_command(dir)?
let mut child = new_binary_command(dir)
.await?
.args(["credential", "approve"])
.stdin(Stdio::piped())
.stdout(Stdio::null())
@@ -26,19 +27,21 @@ pub async fn git_add_credential(
{
let stdin = child.stdin.as_mut().unwrap();
writeln!(stdin, "protocol={}", protocol)?;
writeln!(stdin, "host={}", host)?;
stdin.write_all(format!("protocol={}\n", protocol).as_bytes()).await?;
stdin.write_all(format!("host={}\n", host).as_bytes()).await?;
if let Some(path) = path {
if !path.is_empty() {
writeln!(stdin, "path={}", path.trim_start_matches('/'))?;
stdin
.write_all(format!("path={}\n", path.trim_start_matches('/')).as_bytes())
.await?;
}
}
writeln!(stdin, "username={}", username)?;
writeln!(stdin, "password={}", password)?;
writeln!(stdin)?; // blank line terminator
stdin.write_all(format!("username={}\n", username).as_bytes()).await?;
stdin.write_all(format!("password={}\n", password).as_bytes()).await?;
stdin.write_all(b"\n").await?; // blank line terminator
}
let status = child.wait()?;
let status = child.wait().await?;
if !status.success() {
return Err(GenericError("Failed to approve git credential".to_string()));
}

View File

@@ -3,10 +3,12 @@ use crate::error::Error::GenericError;
use crate::error::Result;
use std::path::Path;
pub fn git_fetch_all(dir: &Path) -> Result<()> {
let out = new_binary_command(dir)?
pub async fn git_fetch_all(dir: &Path) -> Result<()> {
let out = new_binary_command(dir)
.await?
.args(["fetch", "--all", "--prune", "--tags"])
.output()
.await
.map_err(|e| GenericError(format!("failed to run git pull: {e}")))?;
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);

View File

@@ -17,17 +17,25 @@ pub enum PullResult {
NeedsCredentials { url: String, error: Option<String> },
}
pub fn git_pull(dir: &Path) -> Result<PullResult> {
let repo = open_repo(dir)?;
let branch_name = get_current_branch_name(&repo)?;
let remote = get_default_remote_in_repo(&repo)?;
let remote_name = remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?;
let remote_url = remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?;
pub async fn git_pull(dir: &Path) -> Result<PullResult> {
// Extract all git2 data before any await points (git2 types are not Send)
let (branch_name, remote_name, remote_url) = {
let repo = open_repo(dir)?;
let branch_name = get_current_branch_name(&repo)?;
let remote = get_default_remote_in_repo(&repo)?;
let remote_name =
remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?.to_string();
let remote_url =
remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?.to_string();
(branch_name, remote_name, remote_url)
};
let out = new_binary_command(dir)?
let out = new_binary_command(dir)
.await?
.args(["pull", &remote_name, &branch_name])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.await
.map_err(|e| GenericError(format!("failed to run git pull: {e}")))?;
let stdout = String::from_utf8_lossy(&out.stdout);

View File

@@ -17,17 +17,25 @@ pub enum PushResult {
NeedsCredentials { url: String, error: Option<String> },
}
pub fn git_push(dir: &Path) -> Result<PushResult> {
let repo = open_repo(dir)?;
let branch_name = get_current_branch_name(&repo)?;
let remote = get_default_remote_for_push_in_repo(&repo)?;
let remote_name = remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?;
let remote_url = remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?;
pub async fn git_push(dir: &Path) -> Result<PushResult> {
// Extract all git2 data before any await points (git2 types are not Send)
let (branch_name, remote_name, remote_url) = {
let repo = open_repo(dir)?;
let branch_name = get_current_branch_name(&repo)?;
let remote = get_default_remote_for_push_in_repo(&repo)?;
let remote_name =
remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?.to_string();
let remote_url =
remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?.to_string();
(branch_name, remote_name, remote_url)
};
let out = new_binary_command(dir)?
let out = new_binary_command(dir)
.await?
.args(["push", &remote_name, &branch_name])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.await
.map_err(|e| GenericError(format!("failed to run git push: {e}")))?;
let stdout = String::from_utf8_lossy(&out.stdout);

View File

@@ -22,5 +22,6 @@ tokio-stream = "0.1.14"
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
tonic-reflection = "0.12.3"
uuid = { version = "1.7.0", features = ["v4"] }
yaak-common = { workspace = true }
yaak-tls = { workspace = true }
thiserror = "2.0.17"

View File

@@ -16,12 +16,12 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use tokio::fs;
use tokio::process::Command;
use tokio::sync::RwLock;
use tonic::codegen::http::uri::PathAndQuery;
use tonic::transport::Uri;
use tonic_reflection::pb::v1::server_reflection_request::MessageRequest;
use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;
use yaak_common::command::new_xplatform_command;
use yaak_tls::ClientCertificateConfig;
pub async fn fill_pool_from_files(
@@ -91,11 +91,11 @@ pub async fn fill_pool_from_files(
info!("Invoking protoc with {}", args.join(" "));
let out = Command::new(&config.protoc_bin_path)
.args(&args)
.output()
.await
.map_err(|e| GenericError(format!("Failed to run protoc: {}", e)))?;
let mut cmd = new_xplatform_command(&config.protoc_bin_path);
cmd.args(&args);
let out =
cmd.output().await.map_err(|e| GenericError(format!("Failed to run protoc: {}", e)))?;
if !out.status.success() {
return Err(GenericError(format!(

View File

@@ -4,8 +4,8 @@ use std::net::SocketAddr;
use std::path::Path;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tokio::sync::watch::Receiver;
use yaak_common::command::new_xplatform_command;
/// Start the Node.js plugin runtime process.
///
@@ -30,13 +30,14 @@ pub async fn start_nodejs_plugin_runtime(
plugin_runtime_main_str
);
let mut child = Command::new(node_bin_path)
.env("HOST", addr.ip().to_string())
let mut cmd = new_xplatform_command(node_bin_path);
cmd.env("HOST", addr.ip().to_string())
.env("PORT", addr.port().to_string())
.arg(&plugin_runtime_main_str)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
.stderr(Stdio::piped());
let mut child = cmd.spawn()?;
info!("Spawned plugin runtime");