Skip to content

Commit 3ac658d

Browse files
committed
feat: init cloud-cli
1 parent 0db7141 commit 3ac658d

File tree

18 files changed

+2066
-0
lines changed

18 files changed

+2066
-0
lines changed

Cargo.lock

Lines changed: 622 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "cloud-cli"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[[bin]]
7+
name = "cloud-cli"
8+
path = "src/main.rs"
9+
10+
[dependencies]
11+
anyhow = "1.0"
12+
chrono = "0.4"
13+
dialoguer = "0.11"
14+
console = "0.15"
15+
wait-timeout = "0.2.1"

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# cloud-cli
2+
3+
`cloud-cli` is a command-line tool designed to simplify the management and diagnosis of server-side applications. It provides an interactive menu to access various diagnostic tools, making it easier for developers and system administrators to troubleshoot processes.
4+
5+
## Features
6+
7+
The tool is organized into two main categories:
8+
9+
### FE (Frontend/Java Applications)
10+
11+
- **`jstack`**: Prints Java thread stack traces for a given Java process, helping to diagnose hangs and deadlocks.
12+
- **`jmap`**: Generates heap dumps and provides memory statistics for a Java process, useful for analyzing memory leaks.
13+
14+
### BE (Backend/General Processes)
15+
16+
- **`pstack`**: Displays the stack trace for any running process, offering insights into its execution state.
17+
- **`get_be_vars`**: Retrieves and displays the environment variables of a running process.
18+
19+
## Usage
20+
21+
To run the application, execute the binary. An interactive menu will appear, allowing you to select the desired diagnostic tool.
22+
23+
```sh
24+
./cloud-cli
25+
```
26+
27+
## Releases
28+
29+
This project uses GitHub Actions to automatically build and release binaries for Linux (`x86_64` and `aarch64`). When a new version is tagged (e.g., `v1.0.0`), a new release is created.
30+
31+
You can download the latest pre-compiled binaries from the [GitHub Releases](https://github.com/QuakeWang/cloud-cli/releases) page.

src/config.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
use crate::error::{CliError, Result};
2+
use std::env;
3+
use std::path::PathBuf;
4+
5+
/// Configuration for the cloud-cli application
6+
#[derive(Debug, Clone)]
7+
pub struct Config {
8+
pub jdk_path: PathBuf,
9+
pub output_dir: PathBuf,
10+
pub timeout_seconds: u64,
11+
pub no_progress_animation: bool,
12+
}
13+
14+
// Environment variable names
15+
const ENV_JDK_PATH: &str = "JDK_PATH";
16+
const ENV_OUTPUT_DIR: &str = "OUTPUT_DIR";
17+
const ENV_TIMEOUT: &str = "CLOUD_CLI_TIMEOUT";
18+
const ENV_NO_PROGRESS: &str = "CLOUD_CLI_NO_PROGRESS";
19+
20+
impl Default for Config {
21+
fn default() -> Self {
22+
Self {
23+
jdk_path: PathBuf::from("/opt/jdk"),
24+
output_dir: PathBuf::from("/opt/selectdb/information"),
25+
timeout_seconds: 60,
26+
no_progress_animation: false,
27+
}
28+
}
29+
}
30+
31+
impl Config {
32+
/// Creates a new configuration instance from environment variables
33+
pub fn new() -> Self {
34+
let mut config = Self::default();
35+
config.load_from_env();
36+
config
37+
}
38+
39+
/// Loads configuration from environment variables
40+
fn load_from_env(&mut self) {
41+
if let Ok(jdk_path) = env::var(ENV_JDK_PATH) {
42+
self.jdk_path = PathBuf::from(jdk_path);
43+
}
44+
45+
if let Ok(output_dir) = env::var(ENV_OUTPUT_DIR) {
46+
self.output_dir = PathBuf::from(output_dir);
47+
}
48+
49+
if let Ok(timeout) = env::var(ENV_TIMEOUT) {
50+
if let Ok(timeout) = timeout.parse::<u64>() {
51+
self.timeout_seconds = timeout;
52+
}
53+
}
54+
55+
self.no_progress_animation = env::var(ENV_NO_PROGRESS)
56+
.map(|v| v == "1" || v.to_lowercase() == "true")
57+
.unwrap_or(false);
58+
}
59+
60+
pub fn with_jdk_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
61+
self.jdk_path = path.into();
62+
self
63+
}
64+
65+
pub fn with_output_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
66+
self.output_dir = path.into();
67+
self
68+
}
69+
70+
pub fn with_timeout(mut self, seconds: u64) -> Self {
71+
self.timeout_seconds = seconds;
72+
self
73+
}
74+
75+
pub fn with_progress_animation(mut self, enable: bool) -> Self {
76+
self.no_progress_animation = !enable;
77+
self
78+
}
79+
80+
pub fn validate(&self) -> Result<()> {
81+
self.validate_jdk_path()?;
82+
self.validate_output_dir()?;
83+
self.validate_timeout()?;
84+
Ok(())
85+
}
86+
87+
fn validate_jdk_path(&self) -> Result<()> {
88+
if !self.jdk_path.exists() {
89+
return Err(CliError::ConfigError(format!(
90+
"JDK path does not exist: {}. Set {ENV_JDK_PATH} environment variable or ensure default path exists.",
91+
self.jdk_path.display()
92+
)));
93+
}
94+
95+
let jmap_path = self.get_jmap_path();
96+
let jstack_path = self.get_jstack_path();
97+
98+
if !jmap_path.exists() {
99+
return Err(CliError::ConfigError(format!(
100+
"jmap not found: {}. Please verify JDK installation.",
101+
jmap_path.display()
102+
)));
103+
}
104+
105+
if !jstack_path.exists() {
106+
return Err(CliError::ConfigError(format!(
107+
"jstack not found: {}. Please verify JDK installation.",
108+
jstack_path.display()
109+
)));
110+
}
111+
112+
Ok(())
113+
}
114+
115+
fn validate_output_dir(&self) -> Result<()> {
116+
if self.output_dir.exists() {
117+
let test_file = self.output_dir.join(".write_test");
118+
match std::fs::File::create(&test_file) {
119+
Ok(_) => {
120+
let _ = std::fs::remove_file(test_file);
121+
}
122+
Err(e) => {
123+
return Err(CliError::ConfigError(format!(
124+
"Output directory is not writable: {}. Error: {e}",
125+
self.output_dir.display()
126+
)));
127+
}
128+
}
129+
}
130+
Ok(())
131+
}
132+
133+
fn validate_timeout(&self) -> Result<()> {
134+
if self.timeout_seconds == 0 {
135+
return Err(CliError::ConfigError("Timeout cannot be zero".to_string()));
136+
}
137+
if self.timeout_seconds > 3600 {
138+
return Err(CliError::ConfigError(
139+
"Timeout cannot exceed 3600 seconds (1 hour)".to_string(),
140+
));
141+
}
142+
Ok(())
143+
}
144+
145+
pub fn ensure_output_dir(&self) -> Result<()> {
146+
if let Err(e) = std::fs::create_dir_all(&self.output_dir) {
147+
return Err(CliError::ConfigError(format!(
148+
"Failed to create output directory: {}. Error: {e}",
149+
self.output_dir.display()
150+
)));
151+
}
152+
Ok(())
153+
}
154+
155+
pub fn get_jmap_path(&self) -> PathBuf {
156+
self.jdk_path.join("bin/jmap")
157+
}
158+
159+
pub fn get_jstack_path(&self) -> PathBuf {
160+
self.jdk_path.join("bin/jstack")
161+
}
162+
163+
pub fn get_timeout_millis(&self) -> u64 {
164+
self.timeout_seconds * 1000
165+
}
166+
}

src/error.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use dialoguer;
2+
use std::fmt;
3+
4+
#[derive(Debug)]
5+
pub enum CliError {
6+
ProcessNotFound(String),
7+
ProcessExecutionFailed(String),
8+
ToolExecutionFailed(String),
9+
IoError(std::io::Error),
10+
InvalidInput(String),
11+
ConfigError(String),
12+
GracefulExit,
13+
}
14+
15+
impl fmt::Display for CliError {
16+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17+
match self {
18+
CliError::ProcessNotFound(msg) => write!(f, "Process not found: {msg}"),
19+
CliError::ProcessExecutionFailed(msg) => write!(f, "Process execution failed: {msg}"),
20+
CliError::ToolExecutionFailed(msg) => write!(f, "Tool execution failed: {msg}"),
21+
CliError::IoError(err) => write!(f, "IO error: {err}"),
22+
CliError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
23+
CliError::ConfigError(msg) => write!(f, "Configuration error: {msg}"),
24+
CliError::GracefulExit => write!(f, "Graceful exit"),
25+
}
26+
}
27+
}
28+
29+
impl std::error::Error for CliError {}
30+
31+
impl From<std::io::Error> for CliError {
32+
fn from(err: std::io::Error) -> Self {
33+
CliError::IoError(err)
34+
}
35+
}
36+
37+
impl From<anyhow::Error> for CliError {
38+
fn from(err: anyhow::Error) -> Self {
39+
CliError::ToolExecutionFailed(err.to_string())
40+
}
41+
}
42+
43+
impl From<dialoguer::Error> for CliError {
44+
fn from(err: dialoguer::Error) -> Self {
45+
CliError::InvalidInput(err.to_string())
46+
}
47+
}
48+
49+
pub type Result<T> = std::result::Result<T, CliError>;

src/executor.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use crate::config::Config;
2+
use crate::error::{CliError, Result};
3+
use std::process::{Command, Output};
4+
use std::time::Duration;
5+
use wait_timeout::ChildExt;
6+
7+
/// Executes a command with standardized error handling
8+
pub fn execute_command(command: &mut Command, tool_name: &str) -> Result<Output> {
9+
let output = command.output().map_err(|e| {
10+
CliError::ToolExecutionFailed(format!("Failed to execute {tool_name}: {e}"))
11+
})?;
12+
13+
if !output.status.success() {
14+
let stderr = String::from_utf8_lossy(&output.stderr);
15+
let stdout = String::from_utf8_lossy(&output.stdout);
16+
let error_msg = if !stderr.is_empty() {
17+
stderr.to_string()
18+
} else if !stdout.is_empty() {
19+
stdout.to_string()
20+
} else {
21+
format!(
22+
"Command failed with exit code: {}",
23+
output.status.code().unwrap_or(-1)
24+
)
25+
};
26+
return Err(CliError::ToolExecutionFailed(format!(
27+
"{tool_name} failed: {error_msg}"
28+
)));
29+
}
30+
31+
Ok(output)
32+
}
33+
34+
/// Executes a command with timeout based on configuration
35+
pub fn execute_command_with_timeout(
36+
command: &mut Command,
37+
tool_name: &str,
38+
config: &Config,
39+
) -> Result<Output> {
40+
let mut child = command
41+
.spawn()
42+
.map_err(|e| CliError::ToolExecutionFailed(format!("Failed to start {tool_name}: {e}")))?;
43+
44+
let timeout = Duration::from_millis(config.get_timeout_millis());
45+
46+
match child.wait_timeout(timeout).map_err(|e| {
47+
CliError::ToolExecutionFailed(format!("Error waiting for {tool_name} process: {e}"))
48+
})? {
49+
// Process completed within timeout
50+
Some(status) => {
51+
if !status.success() {
52+
return Err(CliError::ToolExecutionFailed(format!(
53+
"{tool_name} failed with exit code: {}",
54+
status.code().unwrap_or(-1)
55+
)));
56+
}
57+
58+
Ok(Output {
59+
status,
60+
stdout: Vec::new(),
61+
stderr: Vec::new(),
62+
})
63+
}
64+
None => {
65+
// Kill the process
66+
let _ = child.kill();
67+
68+
Err(CliError::ToolExecutionFailed(format!(
69+
"{tool_name} timed out after {} seconds",
70+
config.timeout_seconds
71+
)))
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)