From 7ec7852bb1dceb553646ab101bd0ff3edf45ba6d Mon Sep 17 00:00:00 2001 From: QuakeWang <1677980708@qq.com> Date: Fri, 26 Dec 2025 16:02:18 +0800 Subject: [PATCH 1/2] feat: implement MySQL defaults file creation and command building --- .gitignore | 2 + src/tools/mysql/client.rs | 125 +++++++++++++++++++++++++++++++++----- 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index ad67955..9ab350a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +AGENTS.md \ No newline at end of file diff --git a/src/tools/mysql/client.rs b/src/tools/mysql/client.rs index 3978fee..48deb2f 100644 --- a/src/tools/mysql/client.rs +++ b/src/tools/mysql/client.rs @@ -1,6 +1,9 @@ use crate::config_loader::Environment; use crate::config_loader::process_detector; use crate::error::{CliError, Result}; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; use std::process::Command; pub struct MySQLTool; @@ -14,6 +17,88 @@ enum OutputMode { Raw, } +struct TempMySqlDefaultsFile { + path: PathBuf, +} + +impl TempMySqlDefaultsFile { + fn path(&self) -> &Path { + &self.path + } + + fn create(host: &str, port: u16, user: &str, password: &str) -> Result { + let random: [u8; 16] = rand::random(); + let suffix = random + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); + let filename = format!("cloud-cli-mysql-{suffix}.cnf"); + let path = std::env::temp_dir().join(filename); + + let mut open_options = fs::OpenOptions::new(); + open_options.write(true).create_new(true); + + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + open_options.mode(0o600); + } + + let mut file = open_options.open(&path).map_err(|e| { + CliError::ToolExecutionFailed(format!( + "Failed to create mysql defaults file at {}: {e}", + path.display() + )) + })?; + + let user = escape_mysql_option_value(user); + let host = escape_mysql_option_value(host); + let password = escape_mysql_option_value(password); + + writeln!(file, "[client]").map_err(CliError::IoError)?; + writeln!(file, "user=\"{user}\"").map_err(CliError::IoError)?; + writeln!(file, "host=\"{host}\"").map_err(CliError::IoError)?; + writeln!(file, "port={port}").map_err(CliError::IoError)?; + writeln!(file, "protocol=tcp").map_err(CliError::IoError)?; + if !password.is_empty() { + writeln!(file, "password=\"{password}\"").map_err(CliError::IoError)?; + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&path) + .map_err(CliError::IoError)? + .permissions(); + perms.set_mode(0o600); + fs::set_permissions(&path, perms).map_err(CliError::IoError)?; + } + + Ok(Self { path }) + } +} + +impl Drop for TempMySqlDefaultsFile { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + } +} + +fn escape_mysql_option_value(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + for ch in input.chars() { + match ch { + '\\' => out.push_str("\\\\"), + '"' => out.push_str("\\\""), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + _ => out.push(ch), + } + } + out +} + impl MySQLTool { pub fn detect_fe_process() -> Result { process_detector::get_pid_by_env(Environment::FE) @@ -110,35 +195,45 @@ impl MySQLTool { query: &str, mode: OutputMode, ) -> Result { - let mut command = Command::new("mysql"); - command.arg("-h").arg(host); - command.arg("-P").arg(port.to_string()); - command.arg("-u").arg(user); + let (mut command, _defaults_file) = + Self::build_mysql_command(host, port, user, password, query, mode)?; - if !password.is_empty() { - command.arg(format!("-p{password}")); - } + command + .output() + .map_err(|e| CliError::ToolExecutionFailed(format!("Failed to execute mysql: {e}"))) + } + + fn build_mysql_command( + host: &str, + port: u16, + user: &str, + password: &str, + query: &str, + mode: OutputMode, + ) -> Result<(Command, TempMySqlDefaultsFile)> { + let defaults_file = TempMySqlDefaultsFile::create(host, port, user, password)?; + + let mut command = Command::new("mysql"); + command.arg(format!( + "--defaults-extra-file={}", + defaults_file.path().display() + )); match mode { OutputMode::Standard => { command.arg("-A"); } OutputMode::Raw => { - command.arg("-N"); - command.arg("-B"); - command.arg("-r"); - command.arg("-A"); + command.args(["-N", "-B", "-r", "-A"]); } } command.arg("-e").arg(query); - // Prevent mysql from prompting for a password interactively + // Prevent mysql from prompting for a password interactively. command.stdin(std::process::Stdio::null()); - command - .output() - .map_err(|e| CliError::ToolExecutionFailed(format!("Failed to execute mysql: {e}"))) + Ok((command, defaults_file)) } /// Lists databases (excluding system databases) using raw mysql output From 599e4866e13bcf4e706b5ff1601f2f3f4bee2dfa Mon Sep 17 00:00:00 2001 From: QuakeWang <1677980708@qq.com> Date: Fri, 26 Dec 2025 17:01:55 +0800 Subject: [PATCH 2/2] fix: correct MySQL command argument for defaults file handling --- src/config_loader/process_detector.rs | 2 +- src/tools/mysql/client.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/config_loader/process_detector.rs b/src/config_loader/process_detector.rs index 1eaa2a2..5444286 100644 --- a/src/config_loader/process_detector.rs +++ b/src/config_loader/process_detector.rs @@ -181,7 +181,7 @@ pub fn get_paths(env: Environment) -> Result<(PathBuf, PathBuf)> { let (install_path, jdk_path) = get_paths_by_pid(pid); // Verify that we have a valid DORIS_HOME path - if install_path == PathBuf::from("/opt/selectdb") { + if install_path.as_path() == Path::new("/opt/selectdb") { return Err(CliError::ConfigError(format!( "DORIS_HOME not found in {env} process environment" ))); diff --git a/src/tools/mysql/client.rs b/src/tools/mysql/client.rs index 48deb2f..4f8d1f1 100644 --- a/src/tools/mysql/client.rs +++ b/src/tools/mysql/client.rs @@ -214,8 +214,10 @@ impl MySQLTool { let defaults_file = TempMySqlDefaultsFile::create(host, port, user, password)?; let mut command = Command::new("mysql"); + // Must be the first argument to ensure MySQL reads only this file and does not + // merge/override with system/user option files such as ~/.my.cnf. command.arg(format!( - "--defaults-extra-file={}", + "--defaults-file={}", defaults_file.path().display() ));