mirror of
https://github.com/MrFadiAi/openclaw-manager.git
synced 2026-08-14 09:02:25 +00:00
chore: release v0.0.13
This commit is contained in:
@@ -203,6 +203,19 @@ Output in `src-tauri/target/release/bundle/`:
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Changelog
|
||||
|
||||
### v0.0.13
|
||||
- **Fixed Service Stop**: The "Stop" button now reliably terminates the OpenClaw process (including force kill fallback), preventing zombie processes and Bonjour naming conflicts.
|
||||
- **Fixed Service Restart**: The "Restart" button now utilizes the robust stop logic to ensure clean restarts.
|
||||
- **Fixed Telegram `/restart`**: Implemented a **Service Supervisor** that automatically revives the gateway when it is restarted via Telegram command.
|
||||
- **Fixed Supervisor Crash**: Resolved an `EBADF` crash on Windows by detaching stdio for the supervised background process.
|
||||
- **Config Cleanup**: Removed invalid keys (`timezone`, `logLevel`) from `openclaw.json` to pass doctor checks.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the project
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw-manager",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.13",
|
||||
"description": "🦞 OpenClaw Cross-Platform Manager - High-Performance AI Assistant Configuration & Service Management",
|
||||
"author": "OpenClaw Team",
|
||||
"license": "MIT",
|
||||
@@ -39,4 +39,4 @@
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openclaw-manager"
|
||||
version = "0.0.12"
|
||||
version = "0.0.13"
|
||||
description = "OpenClaw Cross-Platform Manager"
|
||||
authors = ["OpenClaw Team"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -3279,9 +3279,9 @@ pub async fn get_workspace_config() -> Result<WorkspaceConfig, String> {
|
||||
|
||||
let workspace = config.pointer("/agents/defaults/workspace")
|
||||
.and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let timezone = config.pointer("/agents/defaults/timezone")
|
||||
let timezone = config.pointer("/manager/timezone")
|
||||
.and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let time_format = config.pointer("/agents/defaults/timeFormat")
|
||||
let time_format = config.pointer("/manager/time_format")
|
||||
.and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let skip_bootstrap = config.pointer("/agents/defaults/skipBootstrap")
|
||||
.and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
@@ -3306,29 +3306,37 @@ pub async fn save_workspace_config(
|
||||
if config.get("agents").is_none() { config["agents"] = json!({}); }
|
||||
if config["agents"].get("defaults").is_none() { config["agents"]["defaults"] = json!({}); }
|
||||
|
||||
let defaults = config["agents"]["defaults"].as_object_mut().unwrap();
|
||||
// Set or remove each field in agents.defaults
|
||||
if let Some(defaults) = config.pointer_mut("/agents/defaults").and_then(|v| v.as_object_mut()) {
|
||||
match &workspace {
|
||||
Some(w) if !w.is_empty() => { defaults.insert("workspace".into(), json!(w)); }
|
||||
_ => { defaults.remove("workspace"); }
|
||||
}
|
||||
if skip_bootstrap {
|
||||
defaults.insert("skipBootstrap".into(), json!(true));
|
||||
} else {
|
||||
defaults.remove("skipBootstrap");
|
||||
}
|
||||
match bootstrap_max_chars {
|
||||
Some(max) => { defaults.insert("bootstrapMaxChars".into(), json!(max)); }
|
||||
None => { defaults.remove("bootstrapMaxChars"); }
|
||||
}
|
||||
// Remove timezone/timeFormat from defaults if present (migrate to manager)
|
||||
defaults.remove("timezone");
|
||||
defaults.remove("timeFormat");
|
||||
}
|
||||
|
||||
// Set or remove each field
|
||||
match &workspace {
|
||||
Some(w) if !w.is_empty() => { defaults.insert("workspace".into(), json!(w)); }
|
||||
_ => { defaults.remove("workspace"); }
|
||||
}
|
||||
match &timezone {
|
||||
Some(tz) if !tz.is_empty() => { defaults.insert("timezone".into(), json!(tz)); }
|
||||
_ => { defaults.remove("timezone"); }
|
||||
}
|
||||
match &time_format {
|
||||
Some(tf) if !tf.is_empty() => { defaults.insert("timeFormat".into(), json!(tf)); }
|
||||
_ => { defaults.remove("timeFormat"); }
|
||||
}
|
||||
if skip_bootstrap {
|
||||
defaults.insert("skipBootstrap".into(), json!(true));
|
||||
} else {
|
||||
defaults.remove("skipBootstrap");
|
||||
}
|
||||
match bootstrap_max_chars {
|
||||
Some(max) => { defaults.insert("bootstrapMaxChars".into(), json!(max)); }
|
||||
None => { defaults.remove("bootstrapMaxChars"); }
|
||||
// Set manager fields
|
||||
if config.get("manager").is_none() { config["manager"] = json!({}); }
|
||||
if let Some(manager) = config.get_mut("manager").and_then(|v| v.as_object_mut()) {
|
||||
match &timezone {
|
||||
Some(tz) if !tz.is_empty() => { manager.insert("timezone".into(), json!(tz)); }
|
||||
_ => { manager.remove("timezone"); }
|
||||
}
|
||||
match &time_format {
|
||||
Some(tf) if !tf.is_empty() => { manager.insert("time_format".into(), json!(tf)); }
|
||||
_ => { manager.remove("time_format"); }
|
||||
}
|
||||
}
|
||||
|
||||
save_openclaw_config(&config)?;
|
||||
@@ -3523,8 +3531,9 @@ pub async fn get_gateway_config() -> Result<GatewayConfig, String> {
|
||||
.map(|v| v as u16)
|
||||
.unwrap_or(3000);
|
||||
|
||||
let log_level = config.pointer("/gateway/logLevel")
|
||||
let log_level = config.pointer("/manager/log_level")
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| config.pointer("/gateway/logLevel").and_then(|v| v.as_str())) // Legacy fallback
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "info".to_string());
|
||||
|
||||
@@ -3543,7 +3552,17 @@ pub async fn save_gateway_config(port: u16, log_level: String) -> Result<String,
|
||||
|
||||
if let Some(gateway) = config.get_mut("gateway").and_then(|v| v.as_object_mut()) {
|
||||
gateway.insert("port".to_string(), json!(port));
|
||||
gateway.insert("logLevel".to_string(), json!(log_level));
|
||||
// Remove legacy logLevel if exists
|
||||
gateway.remove("logLevel");
|
||||
gateway.remove("log_level");
|
||||
}
|
||||
|
||||
if config.get("manager").is_none() {
|
||||
config["manager"] = json!({});
|
||||
}
|
||||
|
||||
if let Some(manager) = config.get_mut("manager").and_then(|v| v.as_object_mut()) {
|
||||
manager.insert("log_level".to_string(), json!(log_level));
|
||||
}
|
||||
|
||||
save_openclaw_config(&config)?;
|
||||
|
||||
@@ -2,7 +2,13 @@ use crate::models::ServiceStatus;
|
||||
use crate::utils::shell;
|
||||
use tauri::command;
|
||||
use std::process::Command;
|
||||
use log::{info, warn, debug};
|
||||
use log::{info, warn, debug, error};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
// Track if service stop was intentional (manual stop) vs unexpected (crash/restart command)
|
||||
static INTENTIONAL_STOP: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -150,46 +156,129 @@ pub async fn start_service() -> Result<String, String> {
|
||||
|
||||
// Poll and wait for port to start listening (max 15 seconds)
|
||||
info!("[Service] Waiting for port {} to start listening...", SERVICE_PORT);
|
||||
let mut started = false;
|
||||
for i in 1..=15 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
if let Some(pid) = check_port_listening(SERVICE_PORT) {
|
||||
info!("[Service] Successfully started ({}s), PID: {}", i, pid);
|
||||
return Ok(format!("Service started, PID: {}", pid));
|
||||
started = true;
|
||||
break;
|
||||
}
|
||||
if i % 3 == 0 {
|
||||
debug!("[Service] Waiting... ({}s)", i);
|
||||
}
|
||||
}
|
||||
|
||||
info!("[Service] Wait timeout, port still not listening");
|
||||
Err("Service start timeout (15s), please check openclaw logs".to_string())
|
||||
if !started {
|
||||
info!("[Service] Wait timeout, port still not listening");
|
||||
return Err("Service start timeout (15s), please check openclaw logs".to_string());
|
||||
}
|
||||
|
||||
// Reset stop flag
|
||||
INTENTIONAL_STOP.store(false, Ordering::Relaxed);
|
||||
|
||||
// Spawn supervisor thread
|
||||
thread::spawn(|| {
|
||||
info!("[Service Supervisor] Thread started");
|
||||
loop {
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
|
||||
// If stop was intentional, exit supervisor
|
||||
if INTENTIONAL_STOP.load(Ordering::Relaxed) {
|
||||
info!("[Service Supervisor] Intentional stop detected, exiting thread");
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if service is running
|
||||
if check_port_listening(SERVICE_PORT).is_none() {
|
||||
warn!("[Service Supervisor] Service stopped unexpectedly! Restarting...");
|
||||
|
||||
// Double check flag just in case
|
||||
if INTENTIONAL_STOP.load(Ordering::Relaxed) { break; }
|
||||
|
||||
if let Err(e) = shell::spawn_openclaw_gateway() {
|
||||
error!("[Service Supervisor] Failed to restart service: {}", e);
|
||||
} else {
|
||||
info!("[Service Supervisor] Restart command sent");
|
||||
// Wait for it to come up so we don't spam restarts
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(pid) = check_port_listening(SERVICE_PORT) {
|
||||
Ok(format!("Service started, PID: {}", pid))
|
||||
} else {
|
||||
Ok("Service started (pid unknown)".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop service
|
||||
/// Stop service
|
||||
#[command]
|
||||
pub async fn stop_service() -> Result<String, String> {
|
||||
info!("[Service] Stopping service...");
|
||||
|
||||
// Set flag so supervisor knows this is intentional
|
||||
INTENTIONAL_STOP.store(true, Ordering::Relaxed);
|
||||
|
||||
// 1. Try graceful stop
|
||||
let _ = shell::run_openclaw(&["gateway", "stop"]);
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
// Wait a bit
|
||||
for _ in 0..5 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
let status = get_service_status().await?;
|
||||
if !status.running {
|
||||
info!("[Service] Successfully stopped (graceful)");
|
||||
return Ok("Service stopped".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try force stop via CLI
|
||||
info!("[Service] Graceful stop failed, trying CLI force stop...");
|
||||
let _ = shell::run_openclaw(&["gateway", "stop", "--force"]);
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
|
||||
let status = get_service_status().await?;
|
||||
if !status.running {
|
||||
info!("[Service] Successfully stopped");
|
||||
info!("[Service] Successfully stopped (CLI force)");
|
||||
return Ok("Service stopped".to_string());
|
||||
}
|
||||
|
||||
// Try force stop
|
||||
let _ = shell::run_openclaw(&["gateway", "stop", "--force"]);
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
// 3. Last resort: Kill process by PID
|
||||
if let Some(pid) = status.pid {
|
||||
info!("[Service] CLI force stop failed, killing PID {}...", pid);
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut cmd = Command::new("taskkill");
|
||||
cmd.args(["/F", "/PID", &pid.to_string()]);
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
if let Ok(output) = cmd.output() {
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
warn!("[Service] Failed to taskkill PID {}: {}", pid, stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status = get_service_status().await?;
|
||||
if status.running {
|
||||
Err(format!("Unable to stop service, PID: {:?}", status.pid))
|
||||
} else {
|
||||
info!("[Service] Successfully stopped");
|
||||
Ok("Service stopped".to_string())
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
|
||||
let final_status = get_service_status().await?;
|
||||
if !final_status.running {
|
||||
info!("[Service] Successfully killed process");
|
||||
return Ok("Service stopped (killed)".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err("Failed to stop service after all attempts".to_string())
|
||||
}
|
||||
|
||||
/// Restart service
|
||||
@@ -198,36 +287,21 @@ pub async fn restart_service() -> Result<String, String> {
|
||||
info!("[Service] Restarting service...");
|
||||
|
||||
// Step 1: Stop the service if it's running
|
||||
let status = get_service_status().await?;
|
||||
if status.running {
|
||||
info!("[Service] Service is running, stopping first...");
|
||||
let _ = shell::run_openclaw(&["gateway", "stop"]);
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
// Check if stopped
|
||||
let status = get_service_status().await?;
|
||||
if status.running {
|
||||
info!("[Service] Service still running, trying force stop...");
|
||||
let _ = shell::run_openclaw(&["gateway", "stop", "--force"]);
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
// Step 1: Stop the service if it's running
|
||||
match stop_service().await {
|
||||
Ok(_) => {
|
||||
info!("[Service] Service stopped successfully");
|
||||
// Wait a bit to ensure port is freed
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
}
|
||||
|
||||
// Wait for port to be freed (max 5 seconds)
|
||||
for i in 1..=10 {
|
||||
if check_port_listening(SERVICE_PORT).is_none() {
|
||||
info!("[Service] Port {} freed after {}ms", SERVICE_PORT, i * 500);
|
||||
break;
|
||||
}
|
||||
if i == 10 {
|
||||
return Err(format!(
|
||||
"Failed to stop service: port {} still in use after 5s",
|
||||
SERVICE_PORT
|
||||
));
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
Err(e) => {
|
||||
info!("[Service] Failed to stop service: {}, trying to continue anyway...", e);
|
||||
}
|
||||
} else {
|
||||
info!("[Service] Service was not running");
|
||||
}
|
||||
|
||||
// Double check port is free
|
||||
if check_port_listening(SERVICE_PORT).is_some() {
|
||||
return Err(format!("Port {} is still in use after stop attempt", SERVICE_PORT));
|
||||
}
|
||||
|
||||
// Step 2: Start the service
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::process::{Command, Output};
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::io;
|
||||
use std::collections::HashMap;
|
||||
use crate::utils::platform;
|
||||
@@ -587,6 +587,12 @@ pub fn spawn_openclaw_gateway() -> io::Result<()> {
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
|
||||
info!("[Shell] Starting gateway process...");
|
||||
|
||||
// Explicitly set stdio to null to prevent EBADF errors when running in background/supervisor
|
||||
cmd.stdout(Stdio::null());
|
||||
cmd.stderr(Stdio::null());
|
||||
cmd.stdin(Stdio::null());
|
||||
|
||||
let child = cmd.spawn();
|
||||
|
||||
match child {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenClaw Manager",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.13",
|
||||
"identifier": "com.openclaw.manager",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
Reference in New Issue
Block a user