mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 06:32:24 +00:00
* feat(daemon): introduce daemon host configuration management - Updated the daemon service to support a new configuration structure for managing tray visibility. - Added functions to load and save daemon host configuration from a JSON file. - Implemented Tauri commands to retrieve and update the daemon host configuration. - Enhanced the service management logic to account for legacy application labels and improve compatibility on macOS. - Refactored executable resolution logic to streamline the process of locating the daemon executable across platforms. * feat(daemon): enhance daemon host configuration with tray visibility settings - Added functionality to load and save daemon host configuration, specifically for managing the visibility of the daemon tray icon. - Implemented UI components in both DaemonHealthPanel and TauriCommandsPanel to toggle the tray visibility setting. - Integrated Tauri commands to retrieve and update the daemon host configuration, improving user control over the daemon's display options. - Enhanced loading states and error handling for better user feedback during configuration updates. * fix(local-ai): update default model IDs for local AI configuration - Changed default model IDs from `qwen2.5:1.5b` and `qwen3-vl:2b` to `gemma3:4b-it-qat` for chat and vision models, ensuring consistency in local AI settings. * feat(core): enhance macOS service management and CLI thread stack size - Added dynamic configuration for thread stack size in the CLI, allowing customization via the `OPENHUMAN_CORE_THREAD_STACK_SIZE` environment variable. - Improved macOS service management by validating the LaunchAgent plist and ensuring it is installed before starting the service. - Enhanced error handling and logging for service loading and plist validation, improving user feedback and reliability. * feat(app): initialize Tauri + React + TypeScript project structure - Added essential project files including package.json, tsconfig.json, and Vite configuration for a Tauri application using React and TypeScript. - Created initial HTML template and CSS styles for the application interface. - Included .gitignore to exclude build artifacts and environment-specific files. - Established basic README documentation to guide setup and development. * feat(cli): refactor command structure for core CLI - Replaced the existing CLI command structure with a new design using `clap` for better organization and extensibility. - Introduced a `CoreCli` struct with subcommands for various operations including server management, health checks, and configuration settings. - Updated command handling to support new subcommands for settings and accessibility operations, enhancing the CLI's functionality. - Modified the core process handling to reflect the new command structure, ensuring compatibility with the updated CLI design. * chore(eslint): add 'app/**' to ignored paths in ESLint configuration - Updated the ESLint configuration to include the 'app/**' directory in the list of ignored paths, ensuring that files in this directory are not linted during the development process. * chore(prettier): add 'app' directory to .prettierignore - Updated the .prettierignore file to include the 'app' directory, preventing formatting checks on files within this path during development. * feat(cli): integrate clap_complete for shell command completions - Added support for generating shell completion scripts using `clap_complete`, enhancing the CLI's usability. - Introduced new subcommands for generating completions and updated command structures to accommodate this feature. - Implemented a new `capture_image_ref` command in the accessibility module for direct image reference capture. - Enhanced the `Tools` command structure to include screenshot functionalities, improving CLI tool management.
286 lines
9.8 KiB
Rust
286 lines
9.8 KiB
Rust
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use tokio::process::{Child, Command};
|
|
use tokio::sync::Mutex;
|
|
use tokio::task::JoinHandle;
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum CoreRunMode {
|
|
InProcess,
|
|
ChildProcess,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct CoreProcessHandle {
|
|
child: Arc<Mutex<Option<Child>>>,
|
|
task: Arc<Mutex<Option<JoinHandle<()>>>>,
|
|
port: u16,
|
|
core_bin: Option<PathBuf>,
|
|
run_mode: CoreRunMode,
|
|
}
|
|
|
|
impl CoreProcessHandle {
|
|
pub fn new(port: u16, core_bin: Option<PathBuf>, run_mode: CoreRunMode) -> Self {
|
|
Self {
|
|
child: Arc::new(Mutex::new(None)),
|
|
task: Arc::new(Mutex::new(None)),
|
|
port,
|
|
core_bin,
|
|
run_mode,
|
|
}
|
|
}
|
|
|
|
pub fn rpc_url(&self) -> String {
|
|
format!("http://127.0.0.1:{}/rpc", self.port)
|
|
}
|
|
|
|
pub async fn ensure_running(&self) -> Result<(), String> {
|
|
if crate::core_rpc::ping().await {
|
|
log::info!(
|
|
"[core] found existing core rpc endpoint at {}",
|
|
self.rpc_url()
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
match self.run_mode {
|
|
CoreRunMode::InProcess => {
|
|
let mut guard = self.task.lock().await;
|
|
if guard.is_none() {
|
|
let port = self.port;
|
|
log::info!("[core] launching in-process core server on port {}", port);
|
|
let task = tokio::spawn(async move {
|
|
if let Err(err) = openhuman_core::core_server::run_server(Some(port)).await
|
|
{
|
|
log::error!("[core] in-process core server exited with error: {err}");
|
|
} else {
|
|
log::warn!("[core] in-process core server exited");
|
|
}
|
|
});
|
|
*guard = Some(task);
|
|
}
|
|
}
|
|
CoreRunMode::ChildProcess => {
|
|
let mut guard = self.child.lock().await;
|
|
if guard.is_none() {
|
|
let mut cmd = if let Some(core_bin) = &self.core_bin {
|
|
let mut cmd = Command::new(core_bin);
|
|
if is_current_exe_path(core_bin) {
|
|
// Safety: if core_bin resolves to this GUI executable, force the
|
|
// explicit subcommand path so we don't accidentally relaunch clients.
|
|
cmd.arg("core");
|
|
}
|
|
cmd.arg("run").arg("--port").arg(self.port.to_string());
|
|
log::info!(
|
|
"[core] spawning dedicated core binary: {:?} run --port {}",
|
|
cmd.as_std().get_program(),
|
|
self.port
|
|
);
|
|
cmd
|
|
} else {
|
|
let exe = std::env::current_exe()
|
|
.map_err(|e| format!("failed to resolve current executable: {e}"))?;
|
|
let mut cmd = Command::new(exe);
|
|
cmd.arg("core")
|
|
.arg("run")
|
|
.arg("--port")
|
|
.arg(self.port.to_string());
|
|
log::warn!(
|
|
"[core] dedicated core binary not found; falling back to self subcommand"
|
|
);
|
|
cmd
|
|
};
|
|
|
|
let child = cmd
|
|
.spawn()
|
|
.map_err(|e| format!("failed to spawn core process: {e}"))?;
|
|
|
|
*guard = Some(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
for _ in 0..40 {
|
|
if crate::core_rpc::ping().await {
|
|
log::info!("[core] core rpc became ready at {}", self.rpc_url());
|
|
return Ok(());
|
|
}
|
|
|
|
match self.run_mode {
|
|
CoreRunMode::InProcess => {
|
|
let mut guard = self.task.lock().await;
|
|
if let Some(task) = guard.as_ref() {
|
|
if task.is_finished() {
|
|
let task = guard.take().expect("checked is_some");
|
|
drop(guard);
|
|
match task.await {
|
|
Ok(_) => {
|
|
return Err(
|
|
"in-process core server exited before becoming ready"
|
|
.to_string(),
|
|
)
|
|
}
|
|
Err(err) => {
|
|
return Err(format!(
|
|
"in-process core server task failed before ready: {err}"
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CoreRunMode::ChildProcess => {
|
|
let mut guard = self.child.lock().await;
|
|
if let Some(child) = guard.as_mut() {
|
|
match child.try_wait() {
|
|
Ok(Some(status)) => {
|
|
return Err(format!("core process exited before ready: {status}"));
|
|
}
|
|
Ok(None) => {}
|
|
Err(e) => {
|
|
return Err(format!("failed checking core process status: {e}"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
}
|
|
|
|
Err("core process did not become ready".to_string())
|
|
}
|
|
|
|
pub async fn shutdown(&self) {
|
|
let mut child_guard = self.child.lock().await;
|
|
if let Some(child) = child_guard.as_mut() {
|
|
let _ = child.kill().await;
|
|
}
|
|
*child_guard = None;
|
|
drop(child_guard);
|
|
|
|
let mut task_guard = self.task.lock().await;
|
|
if let Some(task) = task_guard.take() {
|
|
task.abort();
|
|
let _ = task.await;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_current_exe_path(candidate: &std::path::Path) -> bool {
|
|
let Ok(current) = std::env::current_exe() else {
|
|
return false;
|
|
};
|
|
same_executable_path(candidate, ¤t)
|
|
}
|
|
|
|
fn same_executable_path(a: &std::path::Path, b: &std::path::Path) -> bool {
|
|
if a == b {
|
|
return true;
|
|
}
|
|
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
|
|
(Ok(a_real), Ok(b_real)) => a_real == b_real,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn default_core_port() -> u16 {
|
|
std::env::var("OPENHUMAN_CORE_PORT")
|
|
.ok()
|
|
.and_then(|v| v.parse::<u16>().ok())
|
|
.unwrap_or(7788)
|
|
}
|
|
|
|
pub fn default_core_run_mode(_daemon_mode: bool) -> CoreRunMode {
|
|
if let Ok(value) = std::env::var("OPENHUMAN_CORE_RUN_MODE") {
|
|
let normalized = value.trim().to_ascii_lowercase();
|
|
if matches!(normalized.as_str(), "inprocess" | "in-process" | "internal") {
|
|
return CoreRunMode::InProcess;
|
|
}
|
|
if matches!(
|
|
normalized.as_str(),
|
|
"child" | "process" | "external" | "sidecar"
|
|
) {
|
|
return CoreRunMode::ChildProcess;
|
|
}
|
|
}
|
|
|
|
// Default to a dedicated core process so app and core lifecycles are separated.
|
|
CoreRunMode::ChildProcess
|
|
}
|
|
|
|
pub fn default_core_bin() -> Option<PathBuf> {
|
|
if let Ok(path) = std::env::var("OPENHUMAN_CORE_BIN") {
|
|
let candidate = PathBuf::from(path);
|
|
if candidate.exists() {
|
|
return Some(candidate);
|
|
}
|
|
}
|
|
|
|
// Dev ergonomics: in debug builds, prefer spawning this same executable with
|
|
// `core run` so Cargo recompiles core logic changes as part of tauri dev.
|
|
// Sidecar discovery remains enabled for packaged/release builds.
|
|
if cfg!(debug_assertions) {
|
|
return None;
|
|
}
|
|
|
|
let exe = std::env::current_exe().ok()?;
|
|
let exe_dir = exe.parent()?;
|
|
|
|
#[cfg(windows)]
|
|
let standalone = exe_dir.join("openhuman.exe");
|
|
#[cfg(not(windows))]
|
|
let standalone = exe_dir.join("openhuman");
|
|
|
|
if standalone.exists() && !same_executable_path(&standalone, &exe) {
|
|
return Some(standalone);
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
let legacy_standalone = exe_dir.join("openhuman-core.exe");
|
|
#[cfg(not(windows))]
|
|
let legacy_standalone = exe_dir.join("openhuman-core");
|
|
|
|
if legacy_standalone.exists() && !same_executable_path(&legacy_standalone, &exe) {
|
|
return Some(legacy_standalone);
|
|
}
|
|
|
|
// Sidecar layout: bundle.externalBin("binaries/openhuman") is emitted as
|
|
// openhuman-<target-triple>(.exe) under app resources.
|
|
let mut search_dirs = vec![exe_dir.to_path_buf()];
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let Some(resources_dir) = exe_dir.parent().map(|p| p.join("Resources")) {
|
|
search_dirs.push(resources_dir);
|
|
}
|
|
}
|
|
|
|
for dir in search_dirs {
|
|
let Ok(entries) = std::fs::read_dir(&dir) else {
|
|
continue;
|
|
};
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if !path.is_file() {
|
|
continue;
|
|
}
|
|
let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
|
|
continue;
|
|
};
|
|
|
|
#[cfg(windows)]
|
|
let matches = (file_name.starts_with("openhuman-") && file_name.ends_with(".exe"))
|
|
|| (file_name.starts_with("openhuman-core-") && file_name.ends_with(".exe"));
|
|
#[cfg(not(windows))]
|
|
let matches =
|
|
file_name.starts_with("openhuman-") || file_name.starts_with("openhuman-core-");
|
|
|
|
if matches && !same_executable_path(&path, &exe) {
|
|
return Some(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|