use super::env;
use crate::cli::generate::generate_bot_content;
use crate::log::StyledText;
use crate::utils::process_utils;
use anyhow::{Context, Result};
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{self, Receiver};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::signal;
use tokio::time::sleep;
use tracing::{debug, error, info, warn};
pub struct BotRunner {
bot_file: PathBuf,
python_executable: String,
auto_reload: bool,
work_dir: PathBuf,
current_process: Arc<Mutex<Option<Child>>>,
#[allow(unused)]
watcher: Option<RecommendedWatcher>,
watch_rx: Option<Receiver<Event>>,
}
impl BotRunner {
pub fn new(
bot_file: PathBuf,
python_executable: String,
auto_reload: bool,
work_dir: PathBuf,
) -> Result<Self> {
let current_process = Arc::new(Mutex::new(None));
let (watcher, watch_rx) = if auto_reload {
let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new(
move |res: notify::Result<Event>| {
if let Ok(event) = res
&& let Err(e) = tx.send(event)
{
error!("Failed to send file watch event: {}", e);
}
},
Config::default(),
)
.context("Failed to create file watcher")?;
watcher
.watch(&work_dir, RecursiveMode::Recursive)
.context("Failed to watch directory")?;
(Some(watcher), Some(rx))
} else {
(None, None)
};
let runner = Self {
bot_file,
python_executable,
auto_reload,
work_dir,
current_process,
watcher,
watch_rx,
};
Ok(runner)
}
pub async fn run(&mut self) -> Result<()> {
let process_handle = Arc::clone(&self.current_process);
tokio::spawn(async move {
let _ = signal::ctrl_c().await;
warn!("Received interrupt signal, shutting down...");
if let Ok(mut process) = process_handle.lock()
&& let Some(mut child) = process.take()
{
let _ = child.kill();
let _ = child.wait();
}
sleep(Duration::from_secs(2)).await;
std::process::exit(0);
});
if self.auto_reload {
self.run_with_reload().await
} else {
self.run_once().await
}
}
async fn run_once(&mut self) -> Result<()> {
let mut process = self.start_bot_process()?;
let exit_status = process.wait().context("Failed to wait for process")?;
if exit_status.success() {
info!("Bot process exited successfully");
} else {
let exit_code = exit_status.code().unwrap_or(-1);
error!("Bot process failed with exit code: {}", exit_code);
}
Ok(())
}
async fn run_with_reload(&mut self) -> Result<()> {
let mut last_restart = std::time::Instant::now();
const MAX_RAPID_RESTARTS: u32 = 5;
const RAPID_RESTART_THRESHOLD: Duration = Duration::from_secs(10);
let mut reload_needed = false;
loop {
match self.start_bot_process() {
Ok(process) => {
{
let mut current = self.current_process.lock().unwrap();
*current = Some(process);
}
info!("Bot started successfully with auto-reload enabled");
let mut restart_count = 0;
reload_needed = self.wait_for_reload_trigger().await?;
self.kill_current_process();
if !reload_needed {
break;
}
let now = std::time::Instant::now();
if now.duration_since(last_restart) < RAPID_RESTART_THRESHOLD {
restart_count += 1;
if restart_count >= MAX_RAPID_RESTARTS {
warn!("Too many rapid restarts, adding delay 5s...");
sleep(Duration::from_secs(5)).await;
}
}
last_restart = now;
debug!("Starting bot process...");
}
Err(e) => {
error!("Failed to start bot process: {}", e);
if !reload_needed {
break;
}
sleep(Duration::from_secs(2)).await;
}
}
}
Ok(())
}
async fn wait_for_reload_trigger(&self) -> Result<bool> {
let Some(watch_rx) = self.watch_rx.as_ref() else {
return Ok(false);
};
loop {
{
let mut process_guard = self.current_process.lock().unwrap();
if let Some(process) = process_guard.as_mut() {
match process.try_wait() {
Ok(Some(status)) => {
info!("Bot process exited with status: {}", status);
return Ok(false); }
Ok(None) => {} Err(e) => {
error!("Checking bot process status: {}", e);
return Ok(false);
}
}
}
}
match watch_rx.try_recv() {
Ok(event) => {
if self.should_reload_for_event(&event) {
info!("File change detected, reloading bot...");
while watch_rx.try_recv().is_ok() {}
return Ok(true);
}
}
Err(mpsc::TryRecvError::Empty) => {
sleep(Duration::from_millis(1000)).await;
}
Err(mpsc::TryRecvError::Disconnected) => {
error!("File watcher disconnected");
return Ok(false);
}
}
}
}
fn should_reload_for_event(&self, event: &Event) -> bool {
match event.kind {
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_) => {}
_ => return false,
}
let file_names = ["pyproject.toml", ".env", ".env.dev", ".env.prod"];
for path in &event.paths {
if let Some(name) = path.file_name().and_then(|n| n.to_str())
&& file_names.contains(&name)
{
return true;
}
if path.extension().and_then(|ext| ext.to_str()) == Some("py") {
return true;
}
}
false
}
#[allow(unused)]
fn start_bot_process_with_uv(&self) -> Result<Child> {
let mut cmd = Command::new("uv");
cmd.arg("run").arg("--no-sync");
if self.bot_file.exists() {
cmd.arg(&self.bot_file);
} else {
cmd.arg("python")
.arg("-c")
.arg(generate_bot_content(&self.work_dir)?);
}
cmd.current_dir(&self.work_dir)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let process = cmd.spawn().context("Failed to start bot process")?;
debug!("Bot process started with PID: {}", process.id());
Ok(process)
}
fn start_bot_process(&self) -> Result<Child> {
let mut cmd = Command::new(self.python_executable.clone());
if self.bot_file.exists() {
cmd.arg(&self.bot_file);
} else {
let bot_content = generate_bot_content(&self.work_dir)?;
cmd.arg("-c").arg(bot_content);
}
cmd.current_dir(&self.work_dir)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let process = cmd.spawn().context("Failed to start bot process")?;
debug!("Bot process started with PID: {}", process.id());
Ok(process)
}
fn kill_current_process(&self) {
let mut process_guard = self.current_process.lock().unwrap();
if let Some(mut process) = process_guard.take() {
debug!("Stopping bot process...");
if let Err(e) = process.kill() {
warn!("Failed to kill process gracefully: {}", e);
}
match process.wait() {
Ok(status) => {
debug!("Process exited with status: {}", status);
}
Err(e) => {
warn!("Error waiting for process to exit: {}", e);
}
}
}
}
}
impl Drop for BotRunner {
fn drop(&mut self) {
self.kill_current_process();
}
}
pub async fn handle(file: Option<String>, reload: bool) -> Result<()> {
let bot_file = file.unwrap_or("bot.py".to_string());
let work_dir = std::env::current_dir()?;
let bot_file_path = work_dir.join(bot_file);
let python_executable = env::find_python_executable(&work_dir)?;
let mut runner = BotRunner::new(bot_file_path, python_executable, reload, work_dir)?;
StyledText::new(" ")
.green("Using Python:")
.cyan_underline(&runner.python_executable)
.println_bold();
runner.run().await?;
Ok(())
}
#[allow(unused)]
async fn verify_python_environment(python_executable: &str) -> Result<()> {
debug!("Verifying Python environment...");
let version = process_utils::get_python_version(python_executable).await?;
debug!("Python version: {}", version);
if !version.contains("Python 3.1") {
anyhow::bail!("Python 3.10+ required, found: {}", version);
}
match process_utils::execute_command_with_output(
python_executable,
&["-c", "import nonebot"],
None,
60,
)
.await
{
Ok(_) => {
debug!("NoneBot is installed");
}
Err(_) => {
warn!("NoneBot doesn't seem to be installed. The bot may fail to start.");
}
}
Ok(())
}
pub fn load_environment_variables(work_dir: &Path) -> Result<HashMap<String, String>> {
let mut env_vars = HashMap::new();
let env_files = [".env", ".env.dev", ".env.prod"];
for env_file in &env_files {
let env_path = work_dir.join(env_file);
if env_path.exists() {
debug!("Loading environment variables from {}", env_path.display());
let content = fs::read_to_string(&env_path)
.with_context(|| format!("Failed to read {}", env_file))?;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(eq_pos) = line.find('=') {
let key = line[..eq_pos].trim().to_string();
let value = line[eq_pos + 1..].trim();
let value = if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
&value[1..value.len() - 1]
} else {
value
};
env_vars.insert(key, value.to_string());
}
}
}
}
debug!("Loaded {} environment variables", env_vars.len());
Ok(env_vars)
}