feat: optimize service control (#2)

* refactor: remove unused flags from RcloneConfig and related components; update monitoring intervals to use dynamic settings

* fix: update auto-launch title to indicate immediate effect

* refactor: remove service API token and port from AppConfig and related components

* refactor: remove auto_mount property from RcloneConfig and related components

* refactor: rename service to OpenList Core and update related components
This commit is contained in:
Kuingsmile
2025-06-27 14:02:36 +08:00
committed by GitHub
parent 8ccd04b180
commit bd8e54aa42
23 changed files with 582 additions and 766 deletions

View File

@@ -262,7 +262,6 @@ npm run tauri build
"extraFlags": ["--vfs-cache-mode", "full"]
}
},
"auto_mount": true
}
}
```
@@ -273,7 +272,6 @@ npm run tauri build
{
"app": {
"theme": "auto",
"language": "zh",
"auto_update_enabled": true,
"monitor_interval": 30000
}

View File

@@ -262,7 +262,6 @@ Add custom Rclone flags for optimal performance:
"extraFlags": ["--vfs-cache-mode", "full"]
}
},
"auto_mount": true
}
}
```
@@ -273,7 +272,6 @@ Add custom Rclone flags for optimal performance:
{
"app": {
"theme": "auto",
"language": "en",
"auto_update_enabled": true,
"monitor_interval": 30000
}

View File

@@ -1,11 +1,15 @@
use crate::core::service::check_service_status as check_service_status_impl;
use crate::core::service::install_service as install_service_impl;
use crate::core::service::restart_service as restart_service_impl;
use crate::core::service::stop_service as stop_service_impl;
use crate::core::service::start_service as start_service_impl;
use crate::core::service::uninstall_service as uninstall_service_impl;
use crate::object::structs::AppState;
use crate::utils::api::{get_api_key, get_server_port};
use reqwest;
use tauri::State;
#[tauri::command]
pub async fn check_service_status() -> Result<bool, String> {
pub async fn check_service_status() -> Result<String, String> {
check_service_status_impl().await.map_err(|e| e.to_string())
}
@@ -20,16 +24,24 @@ pub async fn uninstall_service() -> Result<bool, String> {
}
#[tauri::command]
pub async fn stop_service() -> Result<bool, String> {
stop_service_impl().await.map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn restart_service() -> Result<bool, String> {
restart_service_impl().await.map_err(|e| e.to_string())
pub async fn stop_service(state: State<'_, AppState>) -> Result<bool, String> {
let api_key = get_api_key(state);
let port = get_server_port();
let client = reqwest::Client::new();
let response = client
.post(format!("http://127.0.0.1:{}/api/v1/service/stop", port))
.header("Authorization", format!("Bearer {}", api_key))
.send()
.await
.map_err(|e| format!("Failed to send request: {}", e))?;
if response.status().is_success() {
Ok(true)
} else {
Err(format!("Failed to stop service: {}", response.status()))
}
}
#[tauri::command]
pub async fn start_service() -> Result<bool, String> {
check_service_status_impl().await.map_err(|e| e.to_string())
start_service_impl().await.map_err(|e| e.to_string())
}

View File

@@ -4,18 +4,14 @@ use serde::{Deserialize, Serialize};
pub struct AppConfig {
pub theme: Option<String>,
pub monitor_interval: Option<u64>,
pub service_api_token: Option<String>,
pub service_port: Option<u64>,
pub auto_update_enabled: Option<bool>,
}
impl AppConfig {
pub fn new() -> Self {
Self {
theme: Some("zh".into()),
theme: Some("light".to_string()),
monitor_interval: Some(5),
service_api_token: Some("yeM6PCcZGaCpapyBKAbjTp2YAhcku6cUr".into()),
service_port: Some(53211),
auto_update_enabled: Some(true),
}
}

View File

@@ -1,5 +1,6 @@
use crate::conf::rclone::RcloneConfig;
use crate::{conf::core::OpenListCoreConfig, utils::path::app_config_file_path};
use std::path::PathBuf;
use super::app::AppConfig;
use serde::{Deserialize, Serialize};
@@ -25,20 +26,67 @@ impl MergedSettings {
}
}
fn get_data_config_path() -> Result<PathBuf, String> {
let app_dir = std::env::current_exe()
.map_err(|e| format!("Failed to get current exe path: {}", e))?
.parent()
.ok_or("Failed to get parent directory")?
.to_path_buf();
Ok(app_dir.join("data").join("config.json"))
}
fn read_data_config() -> Result<serde_json::Value, String> {
let path = Self::get_data_config_path()?;
if !path.exists() {
return Err("data/config.json does not exist".to_string());
}
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
serde_json::from_str(&content).map_err(|e| e.to_string())
}
fn get_port_from_data_config() -> Result<Option<u16>, String> {
let config = Self::read_data_config()?;
Ok(config
.get("scheme")
.and_then(|scheme| scheme.get("http_port"))
.and_then(|port| port.as_u64())
.map(|port| port as u16))
}
pub fn save(&self) -> Result<(), String> {
let path = app_config_file_path().map_err(|e| e.to_string())?;
std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?;
let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
std::fs::write(&path, json).map_err(|e| e.to_string())?;
Ok(())
}
pub fn load() -> Result<Self, String> {
let path = app_config_file_path().map_err(|e| e.to_string())?;
if !path.exists() {
let mut merged_settings = if !path.exists() {
std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?;
let new_settings = Self::new();
std::fs::write(
&path,
serde_json::to_string_pretty(&Self::new()).map_err(|e| e.to_string())?,
serde_json::to_string_pretty(&new_settings).map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())?;
return Ok(Self::new());
new_settings
} else {
let config = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
serde_json::from_str(&config).map_err(|e| e.to_string())?
};
if let Ok(data_port) = Self::get_port_from_data_config() {
if let Some(port) = data_port {
if merged_settings.openlist.port != port {
merged_settings.openlist.port = port;
merged_settings.save()?;
}
}
}
let config = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let merged_settings: MergedSettings =
serde_json::from_str(&config).map_err(|e| e.to_string())?;
Ok(merged_settings)
}

View File

@@ -5,8 +5,6 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RcloneConfig {
pub config: serde_json::Value,
pub flags: Option<Vec<String>>,
pub auto_mount: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -62,8 +60,6 @@ impl RcloneConfig {
pub fn new() -> Self {
Self {
config: serde_json::Value::Object(Default::default()),
flags: None,
auto_mount: false,
}
}
}

View File

@@ -203,63 +203,8 @@ fn start_service_with_elevation(service_name: &str) -> Result<bool, Box<dyn std:
}
}
#[cfg(not(target_os = "windows"))]
pub async fn start_service() -> Result<bool, Box<dyn std::error::Error>> {
log::info!("Service start not implemented for this platform");
Ok(false)
}
#[cfg(target_os = "macos")]
pub async fn install_service() -> Result<bool, Box<dyn std::error::Error>> {
let app_dir = env::current_exe()?.parent().unwrap().to_path_buf();
let install_path = app_dir.join("install-openlist-service");
if !install_path.exists() {
error!("Service installer not found at {}", install_path.display());
return Err(Box::from(format!(
"Service installer not found at {}",
install_path.display()
)));
}
let status = StdCommand::new(&install_path).status()?;
if status.success() {
Ok(true)
} else {
Err(Box::from(format!(
"Failed to install service, exit status: {}",
status
)))
}
}
#[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<bool, Box<dyn std::error::Error>> {
let app_dir = env::current_exe()?.parent().unwrap().to_path_buf();
let uninstall_path = app_dir.join("uninstall-openlist-service");
if !uninstall_path.exists() {
error!("Uninstaller not found: {:?}", uninstall_path);
return Err(Box::from(format!(
"Uninstaller not found: {:?}",
uninstall_path
)));
}
let status = StdCommand::new(&uninstall_path).status()?;
if status.success() {
Ok(true)
} else {
Err(Box::from(format!(
"Failed to uninstall service, exit status: {}",
status
)))
}
}
#[cfg(target_os = "windows")]
pub async fn check_service_status() -> Result<bool, Box<dyn std::error::Error>> {
pub async fn start_service() -> Result<bool, Box<dyn std::error::Error>> {
use windows_service::service::{ServiceAccess, ServiceState};
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
let service_name = "openlist_desktop_service";
@@ -325,7 +270,7 @@ pub async fn check_service_status() -> Result<bool, Box<dyn std::error::Error>>
}
#[cfg(target_os = "linux")]
pub async fn check_service_status() -> Result<bool, Box<dyn std::error::Error>> {
pub async fn start_service() -> Result<bool, Box<dyn std::error::Error>> {
const SERVICE_NAME: &str = "openlist-desktop-service";
log::info!("Checking Linux service status for: {}", SERVICE_NAME);
@@ -333,42 +278,17 @@ pub async fn check_service_status() -> Result<bool, Box<dyn std::error::Error>>
let init_system = detect_linux_init_system();
match init_system.as_str() {
"systemd" => check_systemd_service_status(SERVICE_NAME).await,
"openrc" => check_openrc_service_status(SERVICE_NAME).await,
"systemd" => start_systemd_service_with_check(SERVICE_NAME).await,
"openrc" => start_openrc_service_with_check(SERVICE_NAME).await,
_ => {
log::warn!("Unknown init system: {}, assuming systemd", init_system);
check_systemd_service_status(SERVICE_NAME).await
start_systemd_service_with_check(SERVICE_NAME).await
}
}
}
#[cfg(target_os = "linux")]
fn detect_linux_init_system() -> String {
if std::path::Path::new("/run/systemd/system").exists() {
return "systemd".to_string();
}
if std::path::Path::new("/run/openrc").exists() {
return "openrc".to_string();
}
if let Ok(output) = StdCommand::new("which").arg("systemctl").output() {
if output.status.success() && !output.stdout.is_empty() {
return "systemd".to_string();
}
}
if let Ok(output) = StdCommand::new("which").arg("rc-service").output() {
if output.status.success() && !output.stdout.is_empty() {
return "openrc".to_string();
}
}
"systemd".to_string()
}
#[cfg(target_os = "linux")]
async fn check_systemd_service_status(
async fn start_systemd_service_with_check(
service_name: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
log::info!("Checking systemd service status for: {}", service_name);
@@ -429,6 +349,249 @@ async fn check_systemd_service_status(
}
}
#[cfg(target_os = "linux")]
async fn start_openrc_service_with_check(
service_name: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
log::info!("Checking OpenRC service status for: {}", service_name);
let status_output = StdCommand::new("rc-service")
.args(&[service_name, "status"])
.output();
match status_output {
Ok(output) => {
let status_str = String::from_utf8_lossy(&output.stdout).to_lowercase();
let stderr_str = String::from_utf8_lossy(&output.stderr).to_lowercase();
log::info!("OpenRC service status output: {}", status_str);
if status_str.contains("started") || status_str.contains("running") {
log::info!("Service is running");
return Ok(true);
} else if status_str.contains("stopped") || status_str.contains("inactive") {
log::info!("Service is stopped, attempting to start");
return start_openrc_service(service_name).await;
} else if stderr_str.contains("does not exist") {
log::error!("Service {} does not exist", service_name);
return Ok(false);
} else {
log::warn!("Unknown service status, attempting to start");
return start_openrc_service(service_name).await;
}
}
Err(e) => {
log::error!("Failed to check OpenRC service status: {}", e);
return start_openrc_service(service_name).await;
}
}
}
#[cfg(target_os = "macos")]
pub async fn install_service() -> Result<bool, Box<dyn std::error::Error>> {
let app_dir = env::current_exe()?.parent().unwrap().to_path_buf();
let install_path = app_dir.join("install-openlist-service");
if !install_path.exists() {
error!("Service installer not found at {}", install_path.display());
return Err(Box::from(format!(
"Service installer not found at {}",
install_path.display()
)));
}
let status = StdCommand::new(&install_path).status()?;
if status.success() {
Ok(true)
} else {
Err(Box::from(format!(
"Failed to install service, exit status: {}",
status
)))
}
}
#[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<bool, Box<dyn std::error::Error>> {
let app_dir = env::current_exe()?.parent().unwrap().to_path_buf();
let uninstall_path = app_dir.join("uninstall-openlist-service");
if !uninstall_path.exists() {
error!("Uninstaller not found: {:?}", uninstall_path);
return Err(Box::from(format!(
"Uninstaller not found: {:?}",
uninstall_path
)));
}
let status = StdCommand::new(&uninstall_path).status()?;
if status.success() {
Ok(true)
} else {
Err(Box::from(format!(
"Failed to uninstall service, exit status: {}",
status
)))
}
}
#[cfg(target_os = "windows")]
pub async fn check_service_status() -> Result<String, Box<dyn std::error::Error>> {
use windows_service::service::{ServiceAccess, ServiceState};
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
let service_name = "openlist_desktop_service";
let manager = match ServiceManager::local_computer(
None::<&str>,
ServiceManagerAccess::CONNECT | ServiceManagerAccess::ENUMERATE_SERVICE,
) {
Ok(mgr) => mgr,
Err(_) => ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?,
};
let service = match manager.open_service(service_name, ServiceAccess::QUERY_STATUS) {
Ok(svc) => svc,
Err(e) => {
log::error!("Failed to open service '{}': {:?}", service_name, e);
return Ok("not-installed".to_string());
}
};
match service.query_status() {
Ok(status) => match status.current_state {
ServiceState::Running | ServiceState::StartPending => {
return Ok("running".to_string());
}
ServiceState::StopPending => {
std::thread::sleep(std::time::Duration::from_millis(1000));
return Ok("stopped".to_string());
}
_ => {
log::info!("Service is in state: {:?}.", status.current_state);
return Ok("stopped".to_string());
}
},
Err(e) => {
log::error!("Failed to query service status: {:?}", e);
match start_service_with_elevation(service_name) {
Ok(true) => Ok("running".to_string()),
Ok(false) => {
log::error!("Failed to start service with elevation.");
Ok("stopped".to_string())
}
Err(elev_err) => {
log::error!("Error during service elevation: {:?}", elev_err);
Ok("error".to_string())
}
}
}
}
}
#[cfg(target_os = "linux")]
pub async fn check_service_status() -> Result<String, Box<dyn std::error::Error>> {
const SERVICE_NAME: &str = "openlist-desktop-service";
log::info!("Checking Linux service status for: {}", SERVICE_NAME);
let init_system = detect_linux_init_system();
match init_system.as_str() {
"systemd" => check_systemd_service_status(SERVICE_NAME).await,
"openrc" => check_openrc_service_status(SERVICE_NAME).await,
_ => {
log::warn!("Unknown init system: {}, assuming systemd", init_system);
check_systemd_service_status(SERVICE_NAME).await
}
}
}
#[cfg(target_os = "linux")]
fn detect_linux_init_system() -> String {
if std::path::Path::new("/run/systemd/system").exists() {
return "systemd".to_string();
}
if std::path::Path::new("/run/openrc").exists() {
return "openrc".to_string();
}
if let Ok(output) = StdCommand::new("which").arg("systemctl").output() {
if output.status.success() && !output.stdout.is_empty() {
return "systemd".to_string();
}
}
if let Ok(output) = StdCommand::new("which").arg("rc-service").output() {
if output.status.success() && !output.stdout.is_empty() {
return "openrc".to_string();
}
}
"systemd".to_string()
}
#[cfg(target_os = "linux")]
async fn check_systemd_service_status(
service_name: &str,
) -> Result<String, Box<dyn std::error::Error>> {
log::info!("Checking systemd service status for: {}", service_name);
let status_output = StdCommand::new("systemctl")
.args(&["is-active", service_name])
.output();
match status_output {
Ok(output) => {
let status = String::from_utf8_lossy(&output.stdout)
.trim()
.to_lowercase();
log::info!("Service {} status: {}", service_name, status);
match status.as_str() {
"active" | "activating" => {
log::info!("Service is active and running");
return Ok("running".to_string());
}
"inactive" | "failed" => {
log::info!("Service is {}", status);
return Ok("stopped".to_string());
}
"unknown" => {
log::warn!("Service status unknown, checking if service exists");
let exists_output = StdCommand::new("systemctl")
.args(&["list-unit-files", &format!("{}.service", service_name)])
.output();
match exists_output {
Ok(output) if output.status.success() => {
let output_str = String::from_utf8_lossy(&output.stdout);
if output_str.contains(service_name) {
log::info!("Service exists and not active");
return Ok("stopped".to_string());
} else {
log::error!("Service {} not found", service_name);
return Ok("not-installed".to_string());
}
}
_ => {
log::error!("Failed to check if service exists");
return Ok("error".to_string());
}
}
}
_ => {
log::warn!("Unknown service status: {}", status);
return Ok("error".to_string());
}
}
}
Err(e) => {
log::error!("Failed to check systemd service status: {}", e);
return Ok("error".to_string());
}
}
}
#[cfg(target_os = "linux")]
async fn start_systemd_service(service_name: &str) -> Result<bool, Box<dyn std::error::Error>> {
use users::get_effective_uid;
@@ -480,7 +643,7 @@ async fn start_systemd_service(service_name: &str) -> Result<bool, Box<dyn std::
#[cfg(target_os = "linux")]
async fn check_openrc_service_status(
service_name: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
) -> Result<String, Box<dyn std::error::Error>> {
log::info!("Checking OpenRC service status for: {}", service_name);
let status_output = StdCommand::new("rc-service")
@@ -496,21 +659,21 @@ async fn check_openrc_service_status(
if status_str.contains("started") || status_str.contains("running") {
log::info!("Service is running");
return Ok(true);
return Ok("running".to_string());
} else if status_str.contains("stopped") || status_str.contains("inactive") {
log::info!("Service is stopped, attempting to start");
return start_openrc_service(service_name).await;
log::info!("Service is stopped");
return Ok("stopped".to_string());
} else if stderr_str.contains("does not exist") {
log::error!("Service {} does not exist", service_name);
return Ok(false);
return Ok("not-installed".to_string());
} else {
log::warn!("Unknown service status, attempting to start");
return start_openrc_service(service_name).await;
return Ok("error".to_string());
}
}
Err(e) => {
log::error!("Failed to check OpenRC service status: {}", e);
return start_openrc_service(service_name).await;
return Ok("error".to_string());
}
}
}
@@ -562,7 +725,7 @@ async fn start_openrc_service(service_name: &str) -> Result<bool, Box<dyn std::e
}
#[cfg(target_os = "macos")]
pub async fn check_service_status() -> Result<bool, Box<dyn std::error::Error>> {
pub async fn start_service() -> Result<bool, Box<dyn std::error::Error>> {
const SERVICE_IDENTIFIER: &str = "io.github.openlistteam.openlist.service";
log::info!("Checking macOS service status for: {}", SERVICE_IDENTIFIER);
@@ -624,6 +787,64 @@ pub async fn check_service_status() -> Result<bool, Box<dyn std::error::Error>>
}
}
#[cfg(target_os = "macos")]
pub async fn check_service_status() -> Result<String, Box<dyn std::error::Error>> {
const SERVICE_IDENTIFIER: &str = "io.github.openlistteam.openlist.service";
log::info!("Checking macOS service status for: {}", SERVICE_IDENTIFIER);
let status_output = StdCommand::new("launchctl")
.args(&["list", SERVICE_IDENTIFIER])
.output();
match status_output {
Ok(output) => {
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
log::info!("launchctl list output: {}", output_str);
if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
log::info!("Extracted PID value: {}", pid_value);
if let Ok(pid) = pid_value.parse::<i32>() {
if pid > 0 {
log::info!("Service is running with PID: {}", pid);
return Ok("running".to_string());
}
}
}
if let Some(exit_status) = extract_plist_value(&output_str, "LastExitStatus") {
if let Ok(status) = exit_status.parse::<i32>() {
if status == 0 {
log::info!("Service is loaded but not running (clean exit)");
return Ok("stopped".to_string());
} else {
log::warn!("Service has non-zero exit status: {}", status);
return Ok("stopped".to_string());
}
}
}
log::info!("Service appears to be loaded but status unclear");
return Ok("error".to_string());
} else {
let stderr_str = String::from_utf8_lossy(&output.stderr);
if stderr_str.contains("Could not find service") {
log::error!("Service {} is not loaded", SERVICE_IDENTIFIER);
return Ok("not-installed".to_string());
} else {
log::warn!("launchctl list failed");
return Ok("error".to_string());
}
}
}
Err(e) => {
log::error!("Failed to check macOS service status: {}", e);
return Ok("error".to_string());
}
}
}
#[cfg(target_os = "macos")]
async fn start_macos_service(service_identifier: &str) -> Result<bool, Box<dyn std::error::Error>> {
log::info!("Attempting to start macOS service: {}", service_identifier);
@@ -694,412 +915,3 @@ fn extract_plist_value(plist_output: &str, key: &str) -> Option<String> {
None
}
#[cfg(target_os = "macos")]
async fn restart_macos_service(
service_identifier: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
log::info!(
"Attempting to restart macOS service: {}",
service_identifier
);
let _ = StdCommand::new("launchctl")
.args(&["stop", service_identifier])
.status();
std::thread::sleep(std::time::Duration::from_millis(500));
start_macos_service(service_identifier).await
}
#[cfg(target_os = "windows")]
pub async fn stop_service() -> Result<bool, Box<dyn std::error::Error>> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let service_name = "openlist_desktop_service";
log::info!("Attempting to stop Windows service: {}", service_name);
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let powershell_cmd = format!("Stop-Service -Name '{}' -Force", service_name);
let status = match level {
PrivilegeLevel::NotPrivileged => {
log::info!("Running without admin privileges, using runas for elevation");
RunasCommand::new("powershell.exe")
.args(&["-Command", &powershell_cmd])
.show(false)
.status()?
}
_ => {
log::info!("Already have admin privileges, running directly");
StdCommand::new("powershell.exe")
.args(&["-Command", &powershell_cmd])
.creation_flags(0x08000000)
.status()?
}
};
if status.success() {
log::info!("Service stopped successfully");
std::thread::sleep(std::time::Duration::from_secs(1));
Ok(true)
} else {
log::error!("Failed to stop service, exit code: {}", status);
Ok(false)
}
}
#[cfg(target_os = "linux")]
pub async fn stop_service() -> Result<bool, Box<dyn std::error::Error>> {
const SERVICE_NAME: &str = "openlist-desktop-service";
log::info!("Attempting to stop Linux service: {}", SERVICE_NAME);
let init_system = detect_linux_init_system();
match init_system.as_str() {
"systemd" => stop_systemd_service(SERVICE_NAME).await,
"openrc" => stop_openrc_service(SERVICE_NAME).await,
_ => {
log::warn!("Unknown init system: {}, assuming systemd", init_system);
stop_systemd_service(SERVICE_NAME).await
}
}
}
#[cfg(target_os = "linux")]
async fn stop_systemd_service(service_name: &str) -> Result<bool, Box<dyn std::error::Error>> {
use users::get_effective_uid;
log::info!("Attempting to stop systemd service: {}", service_name);
let status = match get_effective_uid() {
0 => StdCommand::new("systemctl")
.args(&["stop", service_name])
.status()?,
_ => {
let elevator = linux_elevator();
log::info!("Using {} for elevation", elevator);
StdCommand::new(&elevator)
.args(&["systemctl", "stop", service_name])
.status()?
}
};
if status.success() {
log::info!("Service stop command completed");
std::thread::sleep(std::time::Duration::from_secs(1));
let verify_output = StdCommand::new("systemctl")
.args(&["is-active", service_name])
.output()?;
let verify_status_str = String::from_utf8_lossy(&verify_output.stdout);
let verify_status = verify_status_str.trim();
let is_stopped = verify_status == "inactive" || verify_status == "failed";
if is_stopped {
log::info!("Service verified as stopped");
} else {
log::warn!(
"Service stop command succeeded but service is still active: {}",
verify_status
);
}
Ok(is_stopped)
} else {
log::error!("Failed to stop systemd service, exit code: {}", status);
Ok(false)
}
}
#[cfg(target_os = "linux")]
async fn stop_openrc_service(service_name: &str) -> Result<bool, Box<dyn std::error::Error>> {
use users::get_effective_uid;
log::info!("Attempting to stop OpenRC service: {}", service_name);
let status = match get_effective_uid() {
0 => StdCommand::new("rc-service")
.args(&[service_name, "stop"])
.status()?,
_ => {
let elevator = linux_elevator();
log::info!("Using {} for elevation", elevator);
StdCommand::new(&elevator)
.args(&["rc-service", service_name, "stop"])
.status()?
}
};
if status.success() {
log::info!("Service stop command completed");
std::thread::sleep(std::time::Duration::from_secs(1));
let verify_output = StdCommand::new("rc-service")
.args(&[service_name, "status"])
.output()?;
let verify_status = String::from_utf8_lossy(&verify_output.stdout).to_lowercase();
let is_stopped = verify_status.contains("stopped") || verify_status.contains("inactive");
if is_stopped {
log::info!("Service verified as stopped");
} else {
log::warn!(
"Service stop command succeeded but service is still running: {}",
verify_status
);
}
Ok(is_stopped)
} else {
log::error!("Failed to stop OpenRC service, exit code: {}", status);
Ok(false)
}
}
#[cfg(target_os = "macos")]
pub async fn stop_service() -> Result<bool, Box<dyn std::error::Error>> {
const SERVICE_IDENTIFIER: &str = "io.github.openlistteam.openlist.service";
log::info!("Attempting to stop macOS service: {}", SERVICE_IDENTIFIER);
let status = StdCommand::new("launchctl")
.args(&["stop", SERVICE_IDENTIFIER])
.status()?;
if status.success() {
log::info!("Service stop command completed");
std::thread::sleep(std::time::Duration::from_secs(1));
let verify_output = StdCommand::new("launchctl")
.args(&["list", SERVICE_IDENTIFIER])
.output()?;
if verify_output.status.success() {
let output_str = String::from_utf8_lossy(&verify_output.stdout);
log::info!("Verification output after stop: {}", output_str);
if let Some(pid_value) = extract_plist_value(&output_str, "PID") {
if let Ok(pid) = pid_value.parse::<i32>() {
let is_stopped = pid <= 0;
if is_stopped {
log::info!("Service verified as stopped");
} else {
log::warn!(
"Service stop command succeeded but service is still running with PID: {}",
pid
);
}
return Ok(is_stopped);
}
}
log::info!("No PID found in output, service appears to be stopped");
return Ok(true);
}
log::info!("Could not verify service status after stop, assuming success");
Ok(true)
} else {
log::error!("Failed to stop macOS service, exit code: {}", status);
Ok(false)
}
}
#[cfg(target_os = "windows")]
pub async fn restart_service() -> Result<bool, Box<dyn std::error::Error>> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let service_name = "openlist_desktop_service";
log::info!("Attempting to restart Windows service: {}", service_name);
let powershell_cmd = format!("Restart-Service -Name '{}' -Force", service_name);
let status = {
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
match level {
PrivilegeLevel::NotPrivileged => {
log::info!("Running without admin privileges, using runas for elevation");
RunasCommand::new("powershell.exe")
.args(&["-Command", &powershell_cmd])
.show(false)
.status()?
}
_ => {
log::info!("Already have admin privileges, running directly");
StdCommand::new("powershell.exe")
.args(&["-Command", &powershell_cmd])
.creation_flags(0x08000000)
.status()?
}
}
};
if status.success() {
log::info!("Service restart command completed");
std::thread::sleep(std::time::Duration::from_secs(2));
match check_service_status().await {
Ok(true) => {
log::info!("Service verified as running after restart");
Ok(true)
}
Ok(false) => {
log::warn!("Service restart command succeeded but service is not running");
Ok(false)
}
Err(e) => {
log::error!("Error verifying service status after restart: {}", e);
Ok(false)
}
}
} else {
log::error!("Failed to restart service, exit code: {}", status);
Ok(false)
}
}
#[cfg(target_os = "linux")]
pub async fn restart_service() -> Result<bool, Box<dyn std::error::Error>> {
const SERVICE_NAME: &str = "openlist-desktop-service";
log::info!("Attempting to restart Linux service: {}", SERVICE_NAME);
let init_system = detect_linux_init_system();
match init_system.as_str() {
"systemd" => restart_systemd_service(SERVICE_NAME).await,
"openrc" => restart_openrc_service(SERVICE_NAME).await,
_ => {
log::warn!("Unknown init system: {}, assuming systemd", init_system);
restart_systemd_service(SERVICE_NAME).await
}
}
}
#[cfg(target_os = "linux")]
async fn restart_systemd_service(service_name: &str) -> Result<bool, Box<dyn std::error::Error>> {
use users::get_effective_uid;
log::info!("Attempting to restart systemd service: {}", service_name);
let status = match get_effective_uid() {
0 => StdCommand::new("systemctl")
.args(&["restart", service_name])
.status()?,
_ => {
let elevator = linux_elevator();
log::info!("Using {} for elevation", elevator);
StdCommand::new(&elevator)
.args(&["systemctl", "restart", service_name])
.status()?
}
};
if status.success() {
log::info!("Service restart command completed");
std::thread::sleep(std::time::Duration::from_secs(2));
let verify_output = StdCommand::new("systemctl")
.args(&["is-active", service_name])
.output()?;
let verify_status_str = String::from_utf8_lossy(&verify_output.stdout);
let verify_status = verify_status_str.trim();
let is_running = verify_status == "active" || verify_status == "activating";
if is_running {
log::info!("Service verified as running after restart");
} else {
log::warn!(
"Service restart command succeeded but service is not active: {}",
verify_status
);
}
Ok(is_running)
} else {
log::error!("Failed to restart systemd service, exit code: {}", status);
Ok(false)
}
}
#[cfg(target_os = "linux")]
async fn restart_openrc_service(service_name: &str) -> Result<bool, Box<dyn std::error::Error>> {
use users::get_effective_uid;
log::info!("Attempting to restart OpenRC service: {}", service_name);
let status = match get_effective_uid() {
0 => StdCommand::new("rc-service")
.args(&[service_name, "restart"])
.status()?,
_ => {
let elevator = linux_elevator();
log::info!("Using {} for elevation", elevator);
StdCommand::new(&elevator)
.args(&["rc-service", service_name, "restart"])
.status()?
}
};
if status.success() {
log::info!("Service restart command completed");
std::thread::sleep(std::time::Duration::from_secs(2));
let verify_output = StdCommand::new("rc-service")
.args(&[service_name, "status"])
.output()?;
let verify_status = String::from_utf8_lossy(&verify_output.stdout).to_lowercase();
let is_running = verify_status.contains("started") || verify_status.contains("running");
if is_running {
log::info!("Service verified as running after restart");
} else {
log::warn!(
"Service restart command succeeded but service is not running: {}",
verify_status
);
}
Ok(is_running)
} else {
log::error!("Failed to restart OpenRC service, exit code: {}", status);
Ok(false)
}
}
#[cfg(target_os = "macos")]
pub async fn restart_service() -> Result<bool, Box<dyn std::error::Error>> {
const SERVICE_IDENTIFIER: &str = "io.github.openlistteam.openlist.service";
log::info!(
"Attempting to restart macOS service: {}",
SERVICE_IDENTIFIER
);
let stop_result = stop_service().await?;
if !stop_result {
log::warn!("Failed to stop service, but continuing with restart attempt");
}
std::thread::sleep(std::time::Duration::from_millis(500));
start_macos_service(SERVICE_IDENTIFIER).await
}

View File

@@ -34,8 +34,7 @@ use cmd::http_api::{
};
use cmd::service::{
check_service_status, install_service, restart_service, start_service, stop_service,
uninstall_service,
check_service_status, install_service, start_service, stop_service, uninstall_service,
};
use object::structs::*;
@@ -160,7 +159,6 @@ pub fn run() {
check_service_status,
stop_service,
start_service,
restart_service,
check_for_updates,
download_update,
install_update_and_restart,

View File

@@ -15,21 +15,31 @@ pub fn create_tray(app_handle: &AppHandle) -> tauri::Result<()> {
let hide_i = MenuItem::with_id(app_handle, "hide", "隐藏窗口", true, None::<&str>)?;
let restart_i = MenuItem::with_id(app_handle, "restart", "重启应用", true, None::<&str>)?;
let start_service_i =
MenuItem::with_id(app_handle, "start_service", "启动服务", true, None::<&str>)?;
let stop_service_i =
MenuItem::with_id(app_handle, "stop_service", "停止服务", true, None::<&str>)?;
let start_service_i = MenuItem::with_id(
app_handle,
"start_service",
"启动OpenList",
true,
None::<&str>,
)?;
let stop_service_i = MenuItem::with_id(
app_handle,
"stop_service",
"停止OpenList",
true,
None::<&str>,
)?;
let restart_service_i = MenuItem::with_id(
app_handle,
"restart_service",
"重启服务",
"重启OpenList",
true,
None::<&str>,
)?;
let service_submenu = Submenu::with_id_and_items(
app_handle,
"service",
"服务控制",
"核心控制",
true,
&[&start_service_i, &stop_service_i, &restart_service_i],
)?;
@@ -113,15 +123,15 @@ fn handle_menu_event(app_handle: &AppHandle, event: tauri::menu::MenuEvent) {
}
"start_service" => {
log::info!("Start service menu item clicked");
handle_service_action(app_handle, "start");
handle_core_action(app_handle, "start");
}
"stop_service" => {
log::info!("Stop service menu item clicked");
handle_service_action(app_handle, "stop");
handle_core_action(app_handle, "stop");
}
"restart_service" => {
log::info!("Restart service menu item clicked");
handle_service_action(app_handle, "restart");
handle_core_action(app_handle, "restart");
}
_ => {
log::debug!("Unknown menu item clicked: {:?}", event.id());
@@ -144,21 +154,21 @@ pub fn update_tray_menu(app_handle: &AppHandle, service_running: bool) -> tauri:
let start_service_i = MenuItem::with_id(
app_handle,
"start_service",
"启动服务",
"启动OpenList",
!service_running,
None::<&str>,
)?;
let stop_service_i = MenuItem::with_id(
app_handle,
"stop_service",
"停止服务",
"停止OpenList",
service_running,
None::<&str>,
)?;
let restart_service_i = MenuItem::with_id(
app_handle,
"restart_service",
"重启服务",
"重启OpenList",
service_running,
None::<&str>,
)?;
@@ -166,7 +176,7 @@ pub fn update_tray_menu(app_handle: &AppHandle, service_running: bool) -> tauri:
let service_submenu = Submenu::with_id_and_items(
app_handle,
"service",
"服务控制",
"核心控制",
true,
&[&start_service_i, &stop_service_i, &restart_service_i],
)?;
@@ -212,14 +222,14 @@ pub fn update_tray_menu_delayed(
Ok(())
}
fn handle_service_action(app_handle: &AppHandle, action: &str) {
log::info!("Handling service action from tray: {}", action);
fn handle_core_action(app_handle: &AppHandle, action: &str) {
log::info!("Handling core action from tray: {}", action);
if let Err(e) = app_handle.emit("tray-service-action", action) {
log::error!("Failed to emit tray service action event: {}", e);
if let Err(e) = app_handle.emit("tray-core-action", action) {
log::error!("Failed to emit tray core action event: {}", e);
}
log::debug!("Service action '{}' dispatched to frontend", action);
log::debug!("Core action '{}' dispatched to frontend", action);
}
pub fn force_update_tray_menu(app_handle: &AppHandle, service_running: bool) -> tauri::Result<()> {
@@ -227,21 +237,21 @@ pub fn force_update_tray_menu(app_handle: &AppHandle, service_running: bool) ->
let start_service_i = MenuItem::with_id(
app_handle,
"start_service",
"启动服务",
"启动OpenList",
!service_running,
None::<&str>,
)?;
let stop_service_i = MenuItem::with_id(
app_handle,
"stop_service",
"停止服务",
"停止OpenList",
service_running,
None::<&str>,
)?;
let restart_service_i = MenuItem::with_id(
app_handle,
"restart_service",
"重启服务",
"重启OpenList",
service_running,
None::<&str>,
)?;
@@ -249,7 +259,7 @@ pub fn force_update_tray_menu(app_handle: &AppHandle, service_running: bool) ->
let service_submenu = Submenu::with_id_and_items(
app_handle,
"service",
"服务控制",
"核心控制",
true,
&[&start_service_i, &stop_service_i, &restart_service_i],
)?;

View File

@@ -51,7 +51,7 @@ onMounted(async () => {
try {
store.init()
store.applyTheme(store.settings.app.theme || 'light')
await updateTrayMenu(store.serviceStatus.running)
await updateTrayMenu(store.openlistCoreStatus.running)
try {
updateUnlisten = await TauriAPI.listenToBackgroundUpdateAvailable(updateInfo => {

View File

@@ -6,18 +6,18 @@ import {
DownloadProgress,
FileItem,
MergedSettings,
OpenListCoreStatus,
ProcessConfig,
ProcessStatus,
RcloneMountInfo,
RcloneWebdavConfig,
ServiceStatus,
TauriResponse,
UpdateCheck
} from '../types'
export class TauriAPI {
// openlist desktop service management
static async checkServiceStatus(): Promise<boolean> {
static async checkServiceStatus(): Promise<string> {
return await invoke('check_service_status')
}
@@ -37,10 +37,6 @@ export class TauriAPI {
return await invoke('stop_service')
}
static async restartOpenListService(): Promise<boolean> {
return await invoke('restart_service')
}
// http API management
static async getProcessList(): Promise<ProcessStatus[]> {
return await invoke('get_process_list')
@@ -68,7 +64,7 @@ export class TauriAPI {
return await invoke('create_openlist_core_process', { autoStart })
}
static async getOpenListCoreStatus(): Promise<ServiceStatus> {
static async getOpenListCoreStatus(): Promise<OpenListCoreStatus> {
return await invoke('get_openlist_core_status')
}
@@ -229,7 +225,7 @@ export class TauriAPI {
// Tray event listeners
static async listenToTrayServiceActions(callback: (action: string) => void) {
return await listen('tray-service-action', event => {
return await listen('tray-core-action', event => {
callback(event.payload as string)
})
}

View File

@@ -13,7 +13,7 @@
<div class="metrics" v-if="isCoreRunning">
<span class="metric info">
<Globe :size="14" />
Port: {{ serviceStatus.port || 5244 }}
Port: {{ openlistCoreStatus.port || 5244 }}
</span>
<span class="metric info">
<Activity :size="14" />
@@ -98,7 +98,7 @@ const tooltip = ref({
})
const isCoreRunning = computed(() => store.isCoreRunning)
const serviceStatus = computed(() => store.serviceStatus)
const openlistCoreStatus = computed(() => store.openlistCoreStatus)
const avgResponseTime = computed(() => {
if (dataPoints.value.length === 0) return 0
@@ -228,7 +228,7 @@ onMounted(async () => {
startTime.value = Date.now()
}
monitoringInterval.value = window.setInterval(checkServiceHealth, 2000)
monitoringInterval.value = window.setInterval(checkServiceHealth, (store.settings.app.monitor_interval || 5) * 1000)
window.addEventListener('resize', updateChartSize)
})

View File

@@ -75,11 +75,6 @@
<input type="checkbox" v-model="settings.openlist.auto_launch" @change="handleAutoLaunchToggle" />
<span class="toggle-text">{{ t('dashboard.quickActions.autoLaunch') }}</span>
</label>
<label class="toggle-item">
<input type="checkbox" v-model="settings.rclone.auto_mount" @change="saveSettings" />
<span class="toggle-text">{{ t('dashboard.quickActions.autoMount') }}</span>
</label>
</div>
</div>
</div>
@@ -112,24 +107,26 @@ const serviceButtonIcon = computed(() => {
const serviceButtonText = computed(() => {
if (loading.value) return t('dashboard.quickActions.processing')
return isCoreRunning.value ? t('dashboard.quickActions.stopService') : t('dashboard.quickActions.startService')
return isCoreRunning.value
? t('dashboard.quickActions.stopOpenListCore')
: t('dashboard.quickActions.startOpenListCore')
})
const toggleCore = async () => {
if (isCoreRunning.value) {
await store.stopService()
await store.stopOpenListCore()
} else {
await store.startService()
await store.startOpenListCore()
}
}
const restartCore = async () => {
await store.restartService()
await store.restartOpenListCore()
}
const openWebUI = () => {
if (store.serviceUrl) {
window.open(store.serviceUrl, '_blank')
if (store.openListCoreUrl) {
window.open(store.openListCoreUrl, '_blank')
}
}
@@ -260,7 +257,10 @@ const saveSettings = async () => {
onMounted(async () => {
await rcloneStore.checkRcloneBackendStatus()
statusCheckInterval = window.setInterval(rcloneStore.checkRcloneBackendStatus, 30000)
statusCheckInterval = window.setInterval(
rcloneStore.checkRcloneBackendStatus,
(store.settings.app.monitor_interval || 5) * 1000
)
})
onUnmounted(() => {

View File

@@ -22,7 +22,7 @@
@click="installService"
:disabled="actionLoading || serviceStatus === 'installed'"
class="action-btn install-btn"
v-if="serviceStatus !== 'running'"
v-if="serviceStatus !== 'running' && serviceStatus !== 'stopped'"
>
<component :is="actionLoading && currentAction === 'install' ? LoaderIcon : Download" :size="16" />
<span>{{
@@ -31,17 +31,31 @@
: t('dashboard.serviceManagement.install')
}}</span>
</button>
<button
@click="startService"
:disabled="actionLoading || serviceStatus !== 'installed'"
:disabled="actionLoading || (serviceStatus !== 'installed' && serviceStatus !== 'stopped')"
class="action-btn start-btn"
v-if="serviceStatus === 'installed'"
v-if="serviceStatus === 'installed' || serviceStatus === 'stopped'"
>
<component :is="actionLoading && currentAction === 'start' ? LoaderIcon : Play" :size="16" />
<span>{{
actionLoading && currentAction === 'start' ? t('common.loading') : t('dashboard.serviceManagement.start')
}}</span>
</button>
<button
@click="stopService"
:disabled="actionLoading"
class="action-btn stop-btn"
v-if="serviceStatus === 'running'"
>
<component :is="actionLoading && currentAction === 'stop' ? LoaderIcon : Stop" :size="16" />
<span>{{
actionLoading && currentAction === 'stop' ? t('common.loading') : t('dashboard.serviceManagement.stop')
}}</span>
</button>
<button
@click="showUninstallDialog = true"
:disabled="actionLoading"
@@ -75,16 +89,29 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useTranslation } from '../../composables/useI18n'
import { Download, Play, Trash2, Loader2 as LoaderIcon, CheckCircle2, XCircle, Circle, Server } from 'lucide-vue-next'
import {
Download,
Play,
Square as Stop,
Trash2,
Loader2 as LoaderIcon,
CheckCircle2,
XCircle,
Circle,
Server
} from 'lucide-vue-next'
import Card from '../ui/Card.vue'
import ConfirmDialog from '../ui/ConfirmDialog.vue'
import { TauriAPI } from '../../api/tauri'
import { useRcloneStore } from '@/stores/rclone'
import { useAppStore } from '../../stores/app'
const store = useAppStore()
const { t } = useTranslation()
const rcloneStore = useRcloneStore()
const serviceStatus = ref<'not-installed' | 'installed' | 'running' | 'error'>('not-installed')
const serviceStatus = ref<'not-installed' | 'installed' | 'running' | 'error' | 'stopped'>('not-installed')
const actionLoading = ref(false)
const currentAction = ref('')
const showUninstallDialog = ref(false)
@@ -98,6 +125,7 @@ const statusClass = computed(() => {
case 'installed':
return 'status-installed'
case 'error':
case 'stopped':
return 'status-error'
default:
return 'status-not-installed'
@@ -112,6 +140,8 @@ const statusText = computed(() => {
return t('dashboard.serviceManagement.status.installed')
case 'error':
return t('dashboard.serviceManagement.status.error')
case 'stopped':
return t('dashboard.serviceManagement.status.stopped')
default:
return t('dashboard.serviceManagement.status.notInstalled')
}
@@ -125,6 +155,8 @@ const statusIcon = computed(() => {
return Circle
case 'error':
return XCircle
case 'stopped':
return Stop
default:
return Server
}
@@ -133,12 +165,12 @@ const statusIcon = computed(() => {
const checkServiceStatus = async () => {
try {
const status = await TauriAPI.checkServiceStatus()
serviceStatus.value = status ? 'running' : 'not-installed'
serviceStatus.value = status as 'not-installed' | 'installed' | 'running' | 'error' | 'stopped'
return status
} catch (error) {
console.error('Failed to check service status:', error)
serviceStatus.value = 'error'
return false
return 'error'
}
}
@@ -152,8 +184,8 @@ const installService = async () => {
}
await new Promise(resolve => setTimeout(resolve, 5000))
const status = await checkServiceStatus()
if (!status) {
throw new Error('Service installation did not start correctly')
if (status !== 'installed' && status !== 'running' && status !== 'stopped') {
throw new Error('Service installation did not complete successfully')
}
try {
await TauriAPI.createAndStartRcloneBackend()
@@ -178,7 +210,6 @@ const startService = async () => {
throw new Error('Service start failed')
}
serviceStatus.value = 'running'
console.log('Service started successfully')
} catch (error) {
console.error('Failed to start service:', error)
serviceStatus.value = 'error'
@@ -188,6 +219,24 @@ const startService = async () => {
}
}
const stopService = async () => {
actionLoading.value = true
currentAction.value = 'stop'
try {
const result = await TauriAPI.stopOpenListService()
if (!result) {
throw new Error('Service stop failed')
}
await checkServiceStatus()
} catch (error) {
console.error('Failed to stop service:', error)
serviceStatus.value = 'error'
} finally {
actionLoading.value = false
currentAction.value = ''
}
}
const uninstallService = async () => {
actionLoading.value = true
currentAction.value = 'uninstall'
@@ -218,7 +267,7 @@ const cancelUninstall = () => {
onMounted(async () => {
await checkServiceStatus()
statusCheckInterval = window.setInterval(checkServiceStatus, 30000)
statusCheckInterval = window.setInterval(checkServiceStatus, (store.settings.app.monitor_interval || 5) * 1000)
})
onUnmounted(() => {

View File

@@ -1,29 +1,29 @@
import { useAppStore } from '../stores/app'
export const useServiceActions = () => {
export const useCoreActions = () => {
const store = useAppStore()
const startService = async () => {
const startOpenListCore = async () => {
try {
await store.startService()
await store.startOpenListCore()
} catch (error) {
console.error('Failed to start service:', error)
throw error
}
}
const stopService = async () => {
const stopOpenListCore = async () => {
try {
await store.stopService()
await store.startOpenListCore()
} catch (error) {
console.error('Failed to stop service:', error)
throw error
}
}
const restartService = async () => {
const restartOpenListCore = async () => {
try {
await store.restartService()
await store.restartOpenListCore()
} catch (error) {
console.error('Failed to restart service:', error)
throw error
@@ -31,8 +31,8 @@ export const useServiceActions = () => {
}
return {
startService,
stopService,
restartService
startOpenListCore,
stopOpenListCore,
restartOpenListCore
}
}

View File

@@ -2,10 +2,10 @@ import { onMounted, onUnmounted } from 'vue'
import { TauriAPI } from '../api/tauri'
import { useAppStore } from '../stores/app'
import { useServiceActions } from './useServiceActions'
import { useCoreActions } from './useCoreActions'
export const useTray = () => {
const { startService, stopService, restartService } = useServiceActions()
const { startOpenListCore, stopOpenListCore, restartOpenListCore } = useCoreActions()
const store = useAppStore()
let unlistenTrayActions: (() => void) | null = null
@@ -16,27 +16,28 @@ export const useTray = () => {
console.error('Failed to update tray menu:', error)
}
}
const handleTrayServiceAction = async (action: string) => {
console.log('Tray service action:', action)
console.log('Tray core action:', action)
try {
switch (action) {
case 'start':
await startService()
await startOpenListCore()
setTimeout(async () => {
await updateTrayMenu(store.serviceStatus.running)
await updateTrayMenu(store.openlistCoreStatus.running)
}, 5000)
break
case 'stop':
await stopService()
await stopOpenListCore()
setTimeout(async () => {
await updateTrayMenu(store.serviceStatus.running)
await updateTrayMenu(store.openlistCoreStatus.running)
}, 5000)
break
case 'restart':
await restartService()
await restartOpenListCore()
setTimeout(async () => {
await updateTrayMenu(store.serviceStatus.running)
await updateTrayMenu(store.openlistCoreStatus.running)
}, 5000)
break
default:
@@ -45,7 +46,7 @@ export const useTray = () => {
} catch (error) {
console.error(`Failed to execute tray action '${action}':`, error)
setTimeout(async () => {
await updateTrayMenu(store.serviceStatus.running)
await updateTrayMenu(store.openlistCoreStatus.running)
}, 3000)
}
}
@@ -53,7 +54,7 @@ export const useTray = () => {
try {
unlistenTrayActions = await TauriAPI.listenToTrayServiceActions(handleTrayServiceAction)
await TauriAPI.forceUpdateTrayMenu(store.serviceStatus.running)
await TauriAPI.forceUpdateTrayMenu(store.openlistCoreStatus.running)
console.log('Tray listeners initialized and menu updated')
} catch (error) {
console.error('Failed to initialize tray listeners:', error)

View File

@@ -23,8 +23,8 @@
"openlistService": "OpenList Core",
"rclone": "RClone",
"quickSettings": "Quick Settings",
"startService": "Start Core",
"stopService": "Stop Core",
"startOpenListCore": "Start Core",
"stopOpenListCore": "Stop Core",
"restart": "Restart",
"openWeb": "Web UI",
"configRclone": "Configure RClone",
@@ -86,6 +86,7 @@
"running": "Running",
"installed": "Installed",
"notInstalled": "Not Installed",
"stopped": "Stopped",
"error": "Error"
},
"confirmUninstall": {
@@ -255,7 +256,7 @@
}
},
"autoStartApp": {
"title": "Auto-launch on startup",
"title": "Auto-launch on startup(Immediate Effect)",
"subtitle": "Automatically start OpenList Desktop application when the system starts",
"description": "Automatically start OpenList service when the application launches"
},

View File

@@ -23,8 +23,8 @@
"openlistService": "OpenList 核心",
"rclone": "RClone",
"quickSettings": "快速设置",
"startService": "启动核心",
"stopService": "停止核心",
"startOpenListCore": "启动核心",
"stopOpenListCore": "停止核心",
"restart": "重启",
"openWeb": "网页界面",
"configRclone": "配置 RClone",
@@ -86,6 +86,7 @@
"running": "运行中",
"installed": "已安装",
"notInstalled": "未安装",
"stopped": "已停止",
"error": "错误"
},
"confirmUninstall": {
@@ -255,7 +256,7 @@
}
},
"autoStartApp": {
"title": "开机自动启动应用",
"title": "开机自动启动应用(立即生效)",
"subtitle": "在系统启动时自动启动 OpenList 桌面应用",
"description": "在系统启动时自动启动 OpenList 桌面应用"
},

View File

@@ -5,11 +5,11 @@ import { TauriAPI } from '../api/tauri'
import type {
FileItem,
MergedSettings,
OpenListCoreStatus,
ProcessConfig,
RcloneFormConfig,
RcloneMountInfo,
RcloneWebdavConfig,
ServiceStatus,
UpdateCheck
} from '../types'
@@ -22,20 +22,16 @@ export const useAppStore = defineStore('app', () => {
ssl_enabled: false
},
rclone: {
config: {}, // Flexible JSON object for rclone configuration
flags: [],
auto_mount: false
config: {} // Flexible JSON object for rclone configuration
},
app: {
theme: 'light',
monitor_interval: 5000,
service_api_token: 'yeM6PCcZGaCpapyBKAbjTp2YAhcku6cUr',
service_port: 53211,
auto_update_enabled: true
}
})
const serviceStatus = ref<ServiceStatus>({
const openlistCoreStatus = ref<OpenListCoreStatus>({
running: false
})
@@ -305,10 +301,10 @@ export const useAppStore = defineStore('app', () => {
const tutorialStep = ref(0)
const tutorialSkipped = ref(false)
const isCoreRunning = computed(() => serviceStatus.value.running)
const serviceUrl = computed(() => {
const isCoreRunning = computed(() => openlistCoreStatus.value.running)
const openListCoreUrl = computed(() => {
const protocol = settings.value.openlist.ssl_enabled ? 'https' : 'http'
return `${protocol}://localhost:${serviceStatus.value.port}`
return `${protocol}://localhost:${openlistCoreStatus.value.port}`
})
async function loadSettings() {
@@ -366,7 +362,7 @@ export const useAppStore = defineStore('app', () => {
}
}
async function startService() {
async function startOpenListCore() {
try {
loading.value = true
@@ -398,9 +394,9 @@ export const useAppStore = defineStore('app', () => {
openlistProcessId.value = processId
await refreshServiceStatus()
await TauriAPI.updateTrayMenu(serviceStatus.value.running)
await TauriAPI.updateTrayMenu(openlistCoreStatus.value.running)
} catch (err: any) {
serviceStatus.value = { running: false }
openlistCoreStatus.value = { running: false }
let errorMessage = 'Failed to start service'
const formattedError = formatError(err)
if (formattedError) {
@@ -428,12 +424,12 @@ export const useAppStore = defineStore('app', () => {
}
}
async function stopService() {
async function stopOpenListCore() {
try {
loading.value = true
const id = await getOpenListProcessId()
if (!id) {
serviceStatus.value = { running: false }
openlistCoreStatus.value = { running: false }
await TauriAPI.updateTrayMenu(false)
return
}
@@ -443,7 +439,7 @@ export const useAppStore = defineStore('app', () => {
throw new Error('Failed to stop OpenList Core service - service returned false')
}
serviceStatus.value = { running: false }
openlistCoreStatus.value = { running: false }
await TauriAPI.updateTrayMenu(false)
} catch (err: any) {
const errorMessage = `Failed to stop service: ${formatError(err)}`
@@ -478,12 +474,12 @@ export const useAppStore = defineStore('app', () => {
}
}
async function restartService() {
async function restartOpenListCore() {
try {
loading.value = true
const id = await getOpenListProcessId()
if (!id) {
serviceStatus.value = { running: false }
openlistCoreStatus.value = { running: false }
await TauriAPI.updateTrayMenu(false)
return
}
@@ -492,14 +488,14 @@ export const useAppStore = defineStore('app', () => {
throw new Error('Failed to restart OpenList Core service - service returned false')
}
await refreshServiceStatus()
await TauriAPI.updateTrayMenu(serviceStatus.value.running)
await TauriAPI.updateTrayMenu(openlistCoreStatus.value.running)
} catch (err: any) {
const errorMessage = `Failed to restart service: ${formatError(err)}`
error.value = errorMessage
console.error('Failed to restart service:', err)
try {
await refreshServiceStatus()
await safeUpdateTrayMenu(serviceStatus.value.running)
await safeUpdateTrayMenu(openlistCoreStatus.value.running)
} catch (refreshErr) {
console.error('Failed to refresh service status after restart failure:', refreshErr)
}
@@ -512,14 +508,14 @@ export const useAppStore = defineStore('app', () => {
async function refreshServiceStatus() {
try {
const status = await TauriAPI.getOpenListCoreStatus()
const statusChanged = serviceStatus.value.running !== status.running
serviceStatus.value = status
const statusChanged = openlistCoreStatus.value.running !== status.running
openlistCoreStatus.value = status
if (statusChanged) {
await TauriAPI.updateTrayMenuDelayed(status.running)
}
} catch (err) {
const wasRunning = serviceStatus.value.running
serviceStatus.value = { running: false }
const wasRunning = openlistCoreStatus.value.running
openlistCoreStatus.value = { running: false }
if (wasRunning) {
await TauriAPI.updateTrayMenuDelayed(false)
}
@@ -612,7 +608,7 @@ export const useAppStore = defineStore('app', () => {
async function autoStartServiceIfEnabled() {
try {
if (settings.value.openlist.auto_launch) {
await startService()
await startOpenListCore()
}
} catch (err) {
console.warn('Failed to auto-start service:', err)
@@ -747,7 +743,7 @@ export const useAppStore = defineStore('app', () => {
fullRcloneConfigs,
settings,
serviceStatus,
openlistCoreStatus,
logs,
files,
currentPath,
@@ -761,14 +757,15 @@ export const useAppStore = defineStore('app', () => {
tutorialSkipped,
isCoreRunning,
serviceUrl,
openListCoreUrl,
loadSettings,
saveSettings,
resetSettings,
startService,
stopService,
restartService,
startOpenListCore,
stopOpenListCore,
restartOpenListCore,
enableAutoLaunch,
refreshServiceStatus,
loadLogs,

View File

@@ -7,8 +7,6 @@ export interface OpenListCoreConfig {
export interface RcloneConfig {
config?: any // Flexible JSON object for rclone configuration
flags: string[]
auto_mount: boolean
}
export interface RcloneWebdavConfig {
@@ -65,8 +63,6 @@ export interface RcloneMountRequest {
export interface AppConfig {
theme?: 'light' | 'dark' | 'auto'
monitor_interval?: number
service_api_token?: string
service_port?: number
auto_update_enabled?: boolean
}
@@ -77,7 +73,7 @@ export interface MergedSettings {
app: AppConfig
}
export interface ServiceStatus {
export interface OpenListCoreStatus {
running: boolean
pid?: number
port?: number
@@ -98,7 +94,7 @@ export interface FileItem {
}
export interface AppState {
serviceStatus: ServiceStatus
serviceStatus: OpenListCoreStatus
mountStatus: MountStatus
logs: string[]
settings: MergedSettings

View File

@@ -327,7 +327,7 @@ onMounted(async () => {
await scrollToBottom()
}
}
}, 2000)
}, (store.settings.app.monitor_interval || 5) * 1000)
})
onUnmounted(() => {

View File

@@ -197,17 +197,13 @@ const deleteConfig = async (config: RcloneFormConfig) => {
}
}
const refreshMounts = async () => {
try {
await store.loadMountInfos()
} catch (error: any) {
console.error(error.message || t('mount.messages.failedToRefresh'))
}
}
const startBackend = async () => {
try {
await rcloneStore.startRcloneBackend()
await new Promise(resolve => setTimeout(resolve, 1000))
await rcloneStore.checkRcloneBackendStatus()
await store.loadRemoteConfigs()
await store.loadMountInfos()
} catch (error: any) {
console.error(error.message || t('mount.messages.failedToStartService'))
}
@@ -272,7 +268,8 @@ const handleKeydown = (event: KeyboardEvent) => {
addNewConfig()
} else if (ctrl && key === 'r') {
event.preventDefault()
refreshMounts()
store.loadRemoteConfigs()
store.loadMountInfos()
} else if (key === 'Escape') {
event.preventDefault()
if (showAddForm.value) {
@@ -304,11 +301,11 @@ onMounted(async () => {
document.addEventListener('keydown', handleKeydown)
await rcloneStore.checkRcloneBackendStatus()
await store.loadRemoteConfigs()
await refreshMounts()
mountRefreshInterval = setInterval(refreshMounts, 15000)
await store.loadMountInfos()
mountRefreshInterval = setInterval(store.loadMountInfos, (store.settings.app.monitor_interval || 5) * 1000)
backendStatusCheckInterval = setInterval(() => {
rcloneStore.checkRcloneBackendStatus()
}, 5000)
}, (store.settings.app.monitor_interval || 5) * 1000)
await rcloneStore.init()
})
@@ -396,7 +393,7 @@ onUnmounted(() => {
<option value="unmounted">{{ t('mount.status.unmounted') }}</option>
<option value="error">{{ t('mount.status.error') }}</option>
</select>
<button @click="refreshMounts" class="refresh-btn" :disabled="rcloneStore.loading">
<button @click="store.loadMountInfos" class="refresh-btn" :disabled="rcloneStore.loading">
<RefreshCw class="refresh-icon" :class="{ spinning: rcloneStore.loading }" />
</button>
</div>

View File

@@ -3,18 +3,7 @@ import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useAppStore } from '../stores/app'
import { useTranslation } from '../composables/useI18n'
import {
Settings,
Server,
HardDrive,
Save,
RotateCcw,
AlertCircle,
CheckCircle,
Plus,
Trash2,
Play
} from 'lucide-vue-next'
import { Settings, Server, HardDrive, Save, RotateCcw, AlertCircle, CheckCircle, Play } from 'lucide-vue-next'
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
const store = useAppStore()
@@ -31,13 +20,15 @@ const openlistCoreSettings = reactive({ ...store.settings.openlist })
const rcloneSettings = reactive({ ...store.settings.rclone })
const appSettings = reactive({ ...store.settings.app })
const isOpenListPortChanged = computed(() => {
return openlistCoreSettings.port !== store.settings.openlist.port
})
watch(autoStartApp, async newValue => {
if (newValue) {
await enable()
console.log(`registered for autostart? ${await isEnabled()}`)
} else {
await disable()
console.log(`registered for autostart? ${await isEnabled()}`)
}
})
@@ -75,15 +66,11 @@ onMounted(async () => {
if (openlistCoreSettings.ssl_enabled === undefined) openlistCoreSettings.ssl_enabled = false
if (!rcloneSettings.config) rcloneSettings.config = {}
if (!rcloneSettings.flags) rcloneSettings.flags = []
if (rcloneSettings.auto_mount === undefined) rcloneSettings.auto_mount = false
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
if (!appSettings.theme) appSettings.theme = 'light'
if (!appSettings.monitor_interval) appSettings.monitor_interval = 5
if (!appSettings.service_api_token) appSettings.service_api_token = 'yeM6PCcZGaCpapyBKAbjTp2YAhcku6cUr'
if (!appSettings.service_port) appSettings.service_port = 53211
if (appSettings.auto_update_enabled === undefined) appSettings.auto_update_enabled = true
})
@@ -158,14 +145,6 @@ const handleReset = async () => {
messageType.value = 'error'
}
}
const addRcloneFlag = () => {
rcloneSettings.flags.push('')
}
const removeRcloneFlag = (index: number) => {
rcloneSettings.flags.splice(index, 1)
}
</script>
<template>
@@ -270,44 +249,6 @@ const removeRcloneFlag = (index: number) => {
</div>
<div v-if="activeTab === 'rclone'" class="tab-content">
<div class="settings-section">
<h2>{{ t('settings.rclone.config.title') }}</h2>
<p>{{ t('settings.rclone.config.subtitle') }}</p>
<div class="form-group">
<label class="switch-label">
<input v-model="rcloneSettings.auto_mount" type="checkbox" class="switch-input" />
<span class="switch-slider"></span>
<div class="switch-content">
<span class="switch-title">{{ t('settings.rclone.mount.autoMount.title') }}</span>
<span class="switch-description">{{ t('settings.rclone.mount.autoMount.description') }}</span>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h2>{{ t('settings.rclone.flags.title') }}</h2>
<p>{{ t('settings.rclone.flags.subtitle') }}</p>
<div class="flags-container">
<div v-for="(_, index) in rcloneSettings.flags" :key="index" class="flag-item">
<input
v-model="rcloneSettings.flags[index]"
type="text"
class="form-input"
:placeholder="t('settings.rclone.flags.placeholder')"
/>
<button @click="removeRcloneFlag(index)" class="remove-btn">
<Trash2 :size="16" />
</button>
</div>
<button @click="addRcloneFlag" class="add-flag-btn">
<Plus :size="16" />
{{ t('settings.rclone.flags.add') }}
</button>
</div>
</div>
<div class="settings-section">
<h2>{{ t('settings.rclone.config.title') }}</h2>
<p>{{ t('settings.rclone.config.subtitle') }}</p>
@@ -413,37 +354,6 @@ const removeRcloneFlag = (index: number) => {
</div>
</div>
</div>
<div class="settings-section">
<h2>{{ t('settings.app.service.title') }}</h2>
<p>{{ t('settings.app.service.subtitle') }}</p>
<div class="form-grid">
<div class="form-group">
<label>{{ t('settings.app.service.port.label') }}</label>
<input
v-model.number="appSettings.service_port"
type="number"
class="form-input"
:placeholder="t('settings.app.service.port.placeholder')"
min="1"
max="65535"
/>
<small>{{ t('settings.app.service.port.help') }}</small>
</div>
<div class="form-group">
<label>{{ t('settings.app.service.apiToken.label') }}</label>
<input
v-model="appSettings.service_api_token"
type="password"
class="form-input"
:placeholder="t('settings.app.service.apiToken.placeholder')"
/>
<small>{{ t('settings.app.service.apiToken.help') }}</small>
</div>
</div>
</div>
</div>
</div>
</div>