chore: release v0.0.13

This commit is contained in:
MrFadiAi
2026-02-20 04:53:43 +01:00
parent e31c35849b
commit 3cfc653a3a
7 changed files with 186 additions and 74 deletions
+13
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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"
+45 -26
View File
@@ -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)?;
+117 -43
View File
@@ -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
+7 -1
View File
@@ -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 -1
View File
@@ -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",