mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
ghidra setup command
This commit is contained in:
Generated
+1128
-8
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,12 @@ strsim = "0.11"
|
|||||||
dunce = "1.0" # Windows path handling
|
dunce = "1.0" # Windows path handling
|
||||||
atty = "0.2" # TTY detection
|
atty = "0.2" # TTY detection
|
||||||
|
|
||||||
|
# Setup command dependencies
|
||||||
|
reqwest = { version = "0.11", features = ["json", "stream", "rustls-tls"] }
|
||||||
|
zip = "0.6"
|
||||||
|
futures-util = "0.3"
|
||||||
|
indicatif = "0.17"
|
||||||
|
|
||||||
# Daemonization (Unix only)
|
# Daemonization (Unix only)
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
daemonize = "0.5"
|
daemonize = "0.5"
|
||||||
|
|||||||
@@ -1 +1,83 @@
|
|||||||
|
# Setup Command Implementation Notes
|
||||||
|
|
||||||
|
## Codebase Analysis
|
||||||
|
|
||||||
|
### Current Structure
|
||||||
|
- **Cargo.toml**: Already has tokio, clap, serde, dirs, anyhow dependencies
|
||||||
|
- **src/cli.rs**: Commands enum on line 20-127, need to add Setup variant
|
||||||
|
- **src/main.rs**:
|
||||||
|
- `run()` sync function handles most commands (line 49-72)
|
||||||
|
- `run_async()` handles daemon commands (line 74-80)
|
||||||
|
- `run_with_daemon_check()` routes commands through daemon if running (line 82-125)
|
||||||
|
- **src/ghidra/mod.rs**: GhidraClient with `verify_installation()` - can reuse for verification
|
||||||
|
- **src/config.rs**: Config struct with `save()` method and `ghidra_install_dir` field
|
||||||
|
|
||||||
|
### Key Insights
|
||||||
|
1. Setup command should be treated as async like daemon commands (uses reqwest for HTTP)
|
||||||
|
2. Need to route `Commands::Setup` through `run_async()` rather than sync `run()`
|
||||||
|
3. Can reuse existing `Config::save()` to persist ghidra_install_dir after installation
|
||||||
|
4. Can reuse `GhidraClient::verify_installation()` to verify the installation
|
||||||
|
|
||||||
|
### Dependencies Added
|
||||||
|
```toml
|
||||||
|
reqwest = { version = "0.11", features = ["json", "stream", "rustls-tls"] }
|
||||||
|
zip = "0.6"
|
||||||
|
futures-util = "0.3"
|
||||||
|
indicatif = "0.17"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Progress
|
||||||
|
|
||||||
|
### Phase 1: Dependencies ✅
|
||||||
|
Added reqwest, zip, futures-util, and indicatif to Cargo.toml
|
||||||
|
|
||||||
|
### Phase 2: CLI Definition ✅
|
||||||
|
- Added `Setup(SetupArgs)` variant to Commands enum
|
||||||
|
- Added `SetupArgs` struct with version, dir, and force fields
|
||||||
|
|
||||||
|
### Phase 3: Setup Module ✅
|
||||||
|
Created `src/ghidra/setup.rs` with:
|
||||||
|
- `check_java_requirement()` - runs `java -version` and checks for JDK 17+
|
||||||
|
- `resolve_version_url()` - queries GitHub API for release URL
|
||||||
|
- `download_file()` - streams download with indicatif progress bar
|
||||||
|
- `extract_zip()` - extracts with progress bar, handles Unix permissions
|
||||||
|
- `install_ghidra()` - orchestrates the full installation flow
|
||||||
|
|
||||||
|
### Phase 4: Main Integration ✅
|
||||||
|
- Updated imports to include SetupArgs
|
||||||
|
- Modified main() to route Setup through run_async()
|
||||||
|
- Updated run_async() to handle Commands::Setup
|
||||||
|
- Added handle_setup() async function
|
||||||
|
|
||||||
|
## Testing Notes
|
||||||
|
|
||||||
|
### Build Verification
|
||||||
|
```
|
||||||
|
cargo build # SUCCESS - only lint warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
### Help Output
|
||||||
|
```
|
||||||
|
$ ghidra setup --help
|
||||||
|
Download and setup Ghidra automatically
|
||||||
|
|
||||||
|
Usage: ghidra setup [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version <VERSION> Specific Ghidra version to install (e.g., "11.0"). Defaults to latest
|
||||||
|
-d, --dir <DIR> Installation directory. Defaults to standard data directory
|
||||||
|
--force Skip Java check
|
||||||
|
-v, --verbose Enable verbose output
|
||||||
|
-q, --quiet Suppress non-essential output
|
||||||
|
-h, --help Print help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
All existing tests pass (cargo test).
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
- `Cargo.toml` - Added 4 new dependencies
|
||||||
|
- `src/cli.rs` - Added SetupArgs struct and Setup variant
|
||||||
|
- `src/ghidra/mod.rs` - Added `pub mod setup;`
|
||||||
|
- `src/ghidra/setup.rs` - NEW FILE - 230 lines
|
||||||
|
- `src/main.rs` - Updated routing and added handle_setup function
|
||||||
|
|||||||
+18
@@ -124,6 +124,9 @@ pub enum Commands {
|
|||||||
/// Daemon management commands
|
/// Daemon management commands
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
Daemon(DaemonCommands),
|
Daemon(DaemonCommands),
|
||||||
|
|
||||||
|
/// Download and setup Ghidra automatically
|
||||||
|
Setup(SetupArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
@@ -783,3 +786,18 @@ pub enum DaemonCommands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Arguments for the setup command
|
||||||
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SetupArgs {
|
||||||
|
/// Specific Ghidra version to install (e.g., "11.0"). Defaults to latest.
|
||||||
|
#[arg(long)]
|
||||||
|
pub version: Option<String>,
|
||||||
|
|
||||||
|
/// Installation directory. Defaults to standard data directory.
|
||||||
|
#[arg(long, short = 'd')]
|
||||||
|
pub dir: Option<String>,
|
||||||
|
|
||||||
|
/// Skip Java check
|
||||||
|
#[arg(long)]
|
||||||
|
pub force: bool,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod headless;
|
pub mod headless;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
pub mod scripts;
|
pub mod scripts;
|
||||||
|
pub mod setup;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::{Read, Write, Seek};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// GitHub release asset information
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct GithubAsset {
|
||||||
|
name: String,
|
||||||
|
browser_download_url: String,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GitHub release information
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct GithubRelease {
|
||||||
|
tag_name: String,
|
||||||
|
assets: Vec<GithubAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if Java is installed and meets the minimum version requirement (JDK 17+).
|
||||||
|
pub fn check_java_requirement() -> Result<()> {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let output = Command::new("java")
|
||||||
|
.arg("-version")
|
||||||
|
.output()
|
||||||
|
.context("Failed to execute 'java -version'. Is Java installed and in PATH?")?;
|
||||||
|
|
||||||
|
// Java outputs version info to stderr
|
||||||
|
let version_output = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
// Look for version pattern like "17.0.x" or "21.0.x" in the output
|
||||||
|
// Java version string format is usually: 'java version "17.0.1"' or 'openjdk version "17.0.1"'
|
||||||
|
if version_output.is_empty() {
|
||||||
|
return Err(anyhow!("Could not determine Java version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract version number
|
||||||
|
let version_regex = regex::Regex::new(r#"version "(\d+)"#)?;
|
||||||
|
if let Some(captures) = version_regex.captures(&version_output) {
|
||||||
|
if let Some(major_version) = captures.get(1) {
|
||||||
|
let major: u32 = major_version.as_str().parse().unwrap_or(0);
|
||||||
|
if major >= 17 {
|
||||||
|
println!("✓ Java {} detected", major);
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Java {} detected, but Ghidra requires JDK 17 or higher",
|
||||||
|
major
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: if we got output but couldn't parse, warn but continue
|
||||||
|
println!("⚠ Could not parse Java version, but Java appears installed");
|
||||||
|
println!(" Output: {}", version_output.lines().next().unwrap_or(""));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the download URL for a Ghidra release.
|
||||||
|
/// If version is None, fetches the latest release.
|
||||||
|
pub async fn resolve_version_url(version: Option<String>) -> Result<(String, String, String)> {
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.user_agent("ghidra-cli")
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let release: GithubRelease = if let Some(ver) = version {
|
||||||
|
// Fetch specific version
|
||||||
|
let url = format!(
|
||||||
|
"https://api.github.com/repos/NationalSecurityAgency/ghidra/releases/tags/Ghidra_{}",
|
||||||
|
ver
|
||||||
|
);
|
||||||
|
println!("Fetching release info for Ghidra {}...", ver);
|
||||||
|
client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()
|
||||||
|
.context(format!("Could not find Ghidra version {}", ver))?
|
||||||
|
.json()
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
// Fetch latest release
|
||||||
|
let url = "https://api.github.com/repos/NationalSecurityAgency/ghidra/releases/latest";
|
||||||
|
println!("Fetching latest Ghidra release info...");
|
||||||
|
client
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json()
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Found release: {}", release.tag_name);
|
||||||
|
|
||||||
|
// Find the zip file in assets
|
||||||
|
let zip_asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.ends_with(".zip") && !a.name.contains("src"))
|
||||||
|
.ok_or_else(|| anyhow!("No zip distribution found in release assets"))?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
zip_asset.browser_download_url.clone(),
|
||||||
|
zip_asset.name.clone(),
|
||||||
|
release.tag_name,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download a file with progress bar.
|
||||||
|
pub async fn download_file(url: &str, path: &Path) -> Result<()> {
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.user_agent("ghidra-cli")
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()
|
||||||
|
.context("Download request failed")?;
|
||||||
|
|
||||||
|
let total_size = res.content_length().unwrap_or(0);
|
||||||
|
|
||||||
|
let pb = ProgressBar::new(total_size);
|
||||||
|
pb.set_style(ProgressStyle::default_bar()
|
||||||
|
.template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")?
|
||||||
|
.progress_chars("#>-"));
|
||||||
|
pb.set_message(format!("Downloading {}", path.file_name().unwrap_or_default().to_string_lossy()));
|
||||||
|
|
||||||
|
let mut file = File::create(path)?;
|
||||||
|
let mut stream = res.bytes_stream();
|
||||||
|
|
||||||
|
while let Some(item) = stream.next().await {
|
||||||
|
let chunk = item.context("Error reading download stream")?;
|
||||||
|
file.write_all(&chunk)?;
|
||||||
|
pb.inc(chunk.len() as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
pb.finish_with_message("Download complete");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a zip file to the target directory.
|
||||||
|
/// Returns the path to the extracted Ghidra directory.
|
||||||
|
pub fn extract_zip(zip_path: &Path, target_dir: &Path) -> Result<PathBuf> {
|
||||||
|
println!("Extracting...");
|
||||||
|
|
||||||
|
let file = File::open(zip_path)?;
|
||||||
|
let mut archive = zip::ZipArchive::new(file)?;
|
||||||
|
|
||||||
|
let total_files = archive.len();
|
||||||
|
let pb = ProgressBar::new(total_files as u64);
|
||||||
|
pb.set_style(ProgressStyle::default_bar()
|
||||||
|
.template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len}")?
|
||||||
|
.progress_chars("#>-"));
|
||||||
|
pb.set_message("Extracting files");
|
||||||
|
|
||||||
|
// Track the root directory from the archive
|
||||||
|
let mut root_dir: Option<PathBuf> = None;
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
let outpath = match file.enclosed_name() {
|
||||||
|
Some(path) => target_dir.join(path),
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Capture the root directory (first path component)
|
||||||
|
if root_dir.is_none() {
|
||||||
|
if let Some(first_component) = file.enclosed_name().and_then(|p| p.components().next()) {
|
||||||
|
root_dir = Some(target_dir.join(first_component.as_os_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.name().ends_with('/') {
|
||||||
|
std::fs::create_dir_all(&outpath)?;
|
||||||
|
} else {
|
||||||
|
if let Some(p) = outpath.parent() {
|
||||||
|
if !p.exists() {
|
||||||
|
std::fs::create_dir_all(p)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut outfile = File::create(&outpath)?;
|
||||||
|
std::io::copy(&mut file, &mut outfile)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set permissions on Unix
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
if let Some(mode) = file.unix_mode() {
|
||||||
|
std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode)).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pb.inc(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pb.finish_with_message("Extraction complete");
|
||||||
|
|
||||||
|
root_dir.ok_or_else(|| anyhow!("Could not determine extracted directory"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install Ghidra to the specified directory.
|
||||||
|
/// Returns the path to the installed Ghidra directory.
|
||||||
|
pub async fn install_ghidra(version: Option<String>, target_dir: PathBuf) -> Result<PathBuf> {
|
||||||
|
// Resolve version and get download URL
|
||||||
|
let (download_url, filename, tag) = resolve_version_url(version).await?;
|
||||||
|
|
||||||
|
println!("Installing Ghidra {} to: {}", tag, target_dir.display());
|
||||||
|
|
||||||
|
// Download the zip file
|
||||||
|
let zip_path = target_dir.join(&filename);
|
||||||
|
download_file(&download_url, &zip_path).await?;
|
||||||
|
|
||||||
|
// Extract the zip
|
||||||
|
let install_path = extract_zip(&zip_path, &target_dir)?;
|
||||||
|
|
||||||
|
// Cleanup zip file
|
||||||
|
if let Err(e) = std::fs::remove_file(&zip_path) {
|
||||||
|
println!("⚠ Could not remove zip file: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(install_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_check_java_fails_gracefully() {
|
||||||
|
// This just ensures the function doesn't panic
|
||||||
|
// It may succeed or fail depending on the system
|
||||||
|
let _ = check_java_requirement();
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-9
@@ -8,7 +8,7 @@ mod ghidra;
|
|||||||
mod query;
|
mod query;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands, DaemonCommands, QueryArgs, QueryOptions};
|
use cli::{Cli, Commands, DaemonCommands, QueryArgs, QueryOptions, SetupArgs};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use daemon::process::{get_data_dir, get_running_daemon_info, ensure_not_running};
|
use daemon::process::{get_data_dir, get_running_daemon_info, ensure_not_running};
|
||||||
use daemon::rpc as daemon_rpc;
|
use daemon::rpc as daemon_rpc;
|
||||||
@@ -32,12 +32,15 @@ async fn main() {
|
|||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let result = if let Commands::Daemon(_) = &cli.command {
|
let result = match &cli.command {
|
||||||
// Daemon commands are async
|
Commands::Daemon(_) | Commands::Setup(_) => {
|
||||||
run_async(cli).await
|
// Daemon and Setup commands are async
|
||||||
} else {
|
run_async(cli).await
|
||||||
// Other commands can be sync or we check if daemon is running
|
}
|
||||||
run_with_daemon_check(cli).await
|
_ => {
|
||||||
|
// Other commands can be sync or we check if daemon is running
|
||||||
|
run_with_daemon_check(cli).await
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
@@ -71,11 +74,12 @@ fn run(cli: Cli) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run async commands (daemon management).
|
/// Run async commands (daemon management, setup).
|
||||||
async fn run_async(cli: Cli) -> anyhow::Result<()> {
|
async fn run_async(cli: Cli) -> anyhow::Result<()> {
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Daemon(cmd) => handle_daemon_command(cmd).await,
|
Commands::Daemon(cmd) => handle_daemon_command(cmd).await,
|
||||||
_ => unreachable!("run_async called with non-daemon command"),
|
Commands::Setup(args) => handle_setup(args).await,
|
||||||
|
_ => unreachable!("run_async called with non-async command"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,6 +331,61 @@ async fn handle_daemon_clear_cache(project: Option<String>) -> anyhow::Result<()
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle the setup command - download and install Ghidra.
|
||||||
|
async fn handle_setup(args: SetupArgs) -> anyhow::Result<()> {
|
||||||
|
println!("Ghidra Setup Wizard");
|
||||||
|
println!("===================\n");
|
||||||
|
|
||||||
|
// 1. Check Java
|
||||||
|
if !args.force {
|
||||||
|
if let Err(e) = ghidra::setup::check_java_requirement() {
|
||||||
|
eprintln!("⚠ Java prerequisite check failed: {}", e);
|
||||||
|
eprintln!("Ghidra requires JDK 17+. Use --force to continue anyway.");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("⚠ Skipping Java check (--force specified)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Determine Install Directory
|
||||||
|
let install_base = if let Some(d) = args.dir {
|
||||||
|
PathBuf::from(d)
|
||||||
|
} else {
|
||||||
|
// Default to XDG_DATA_HOME/ghidra-cli/ghidra
|
||||||
|
dirs::data_local_dir()
|
||||||
|
.ok_or(anyhow::anyhow!("Could not determine data directory"))?
|
||||||
|
.join("ghidra-cli")
|
||||||
|
.join("ghidra")
|
||||||
|
};
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&install_base)?;
|
||||||
|
|
||||||
|
// 3. Install
|
||||||
|
println!("\nInstalling to: {}", install_base.display());
|
||||||
|
let final_path = ghidra::setup::install_ghidra(args.version, install_base).await?;
|
||||||
|
|
||||||
|
// 4. Update Config
|
||||||
|
let mut config = Config::load()?;
|
||||||
|
config.ghidra_install_dir = Some(final_path.clone());
|
||||||
|
config.save()?;
|
||||||
|
|
||||||
|
println!("\n✓ Success! Ghidra installed at: {}", final_path.display());
|
||||||
|
println!("✓ Configuration updated.");
|
||||||
|
|
||||||
|
// 5. Verify
|
||||||
|
println!("\nVerifying installation...");
|
||||||
|
let client = GhidraClient::new(config)?;
|
||||||
|
if client.verify_installation().is_ok() {
|
||||||
|
println!("✓ Verification passed!");
|
||||||
|
println!("\nYou can now run: ghidra quick <binary>");
|
||||||
|
} else {
|
||||||
|
println!("⚠ Verification failed - analyzeHeadless not found");
|
||||||
|
println!(" The installation may be incomplete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Daemonize on Unix systems using fork and detach.
|
/// Daemonize on Unix systems using fork and detach.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn daemonize_unix(daemon_config: DaemonConfig) -> anyhow::Result<()> {
|
fn daemonize_unix(daemon_config: DaemonConfig) -> anyhow::Result<()> {
|
||||||
|
|||||||
Reference in New Issue
Block a user