mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-14 18:24:27 -04:00
Add daemon service management commands
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -481,3 +481,4 @@ whitepaper/*.log
|
||||
|
||||
|
||||
test_data
|
||||
:memory:
|
||||
|
||||
@@ -107,6 +107,13 @@ impl DaemonConnectionPool {
|
||||
}
|
||||
}
|
||||
|
||||
async fn reset(&self) {
|
||||
let mut initialized = self.initialized.lock().await;
|
||||
*initialized = false;
|
||||
*self.writer.lock().await = None;
|
||||
tracing::info!("Connection pool reset");
|
||||
}
|
||||
|
||||
async fn ensure_connected(&self, app: &AppHandle) -> Result<(), String> {
|
||||
let mut initialized = self.initialized.lock().await;
|
||||
|
||||
@@ -787,6 +794,325 @@ async fn stop_daemon_process(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if daemon is installed as a service (LaunchAgent on macOS, systemd on Linux)
|
||||
#[tauri::command]
|
||||
async fn check_daemon_installed() -> Result<bool, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = std::env::var("HOME").map_err(|_| "Could not determine home directory".to_string())?;
|
||||
let plist_path = std::path::PathBuf::from(home).join("Library/LaunchAgents/com.spacedrive.daemon.plist");
|
||||
let exists = plist_path.exists();
|
||||
tracing::info!("Checking daemon installation at {}: {}", plist_path.display(), exists);
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let home = std::env::var("HOME").map_err(|_| "Could not determine home directory".to_string())?;
|
||||
let service_path = std::path::PathBuf::from(home).join(".config/systemd/user/spacedrive-daemon.service");
|
||||
Ok(service_path.exists())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Install daemon as a service (LaunchAgent on macOS, systemd on Linux)
|
||||
#[tauri::command]
|
||||
async fn install_daemon_service(
|
||||
app: tauri::AppHandle,
|
||||
daemon_state: tauri::State<'_, Arc<RwLock<DaemonState>>>,
|
||||
app_state: tauri::State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let (data_dir, socket_addr) = {
|
||||
let state = daemon_state.read().await;
|
||||
(state.data_dir.clone(), state.socket_addr.clone())
|
||||
};
|
||||
|
||||
tracing::info!("Installing daemon as service");
|
||||
|
||||
// Stop any existing daemon child process first
|
||||
{
|
||||
let mut state = daemon_state.write().await;
|
||||
if let Some(process_arc) = state.daemon_process.take() {
|
||||
tracing::info!("Stopping existing daemon child process");
|
||||
let mut process_lock = process_arc.lock().await;
|
||||
if let Some(mut child) = process_lock.take() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit starting event since installation starts the daemon
|
||||
let _ = app.emit("daemon-starting", ());
|
||||
tracing::info!("Emitted daemon-starting event");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::io::Write;
|
||||
|
||||
let home = std::env::var("HOME").map_err(|_| "Could not determine home directory".to_string())?;
|
||||
let launch_agents_dir = std::path::PathBuf::from(&home).join("Library/LaunchAgents");
|
||||
|
||||
std::fs::create_dir_all(&launch_agents_dir)
|
||||
.map_err(|e| format!("Failed to create LaunchAgents directory: {}", e))?;
|
||||
|
||||
let plist_path = launch_agents_dir.join("com.spacedrive.daemon.plist");
|
||||
tracing::info!("Creating plist at: {}", plist_path.display());
|
||||
|
||||
let daemon_path = std::env::current_exe()
|
||||
.map_err(|e| format!("Failed to get current exe: {}", e))?
|
||||
.parent()
|
||||
.ok_or_else(|| "Could not determine binary directory".to_string())?
|
||||
.join("sd-daemon");
|
||||
|
||||
if !daemon_path.exists() {
|
||||
return Err(format!("Daemon binary not found at {}", daemon_path.display()));
|
||||
}
|
||||
|
||||
let log_dir = data_dir.join("logs");
|
||||
std::fs::create_dir_all(&log_dir)
|
||||
.map_err(|e| format!("Failed to create logs directory: {}", e))?;
|
||||
|
||||
let plist_content = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.spacedrive.daemon</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{}</string>
|
||||
<string>--data-dir</string>
|
||||
<string>{}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<dict>
|
||||
<key>SuccessfulExit</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{}</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{}</string>
|
||||
</dict>
|
||||
</plist>"#,
|
||||
daemon_path.display(),
|
||||
data_dir.display(),
|
||||
log_dir.join("daemon.out.log").display(),
|
||||
log_dir.join("daemon.err.log").display()
|
||||
);
|
||||
|
||||
let mut file = std::fs::File::create(&plist_path)
|
||||
.map_err(|e| format!("Failed to create plist file: {}", e))?;
|
||||
file.write_all(plist_content.as_bytes())
|
||||
.map_err(|e| format!("Failed to write plist file: {}", e))?;
|
||||
|
||||
// Unload any existing service first
|
||||
tracing::info!("Unloading any existing service");
|
||||
let _ = std::process::Command::new("launchctl")
|
||||
.args(&["unload", plist_path.to_str().unwrap()])
|
||||
.output();
|
||||
|
||||
// Load the service (this starts the daemon)
|
||||
tracing::info!("Loading service with launchctl");
|
||||
let output = std::process::Command::new("launchctl")
|
||||
.args(&["load", plist_path.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to load service: {}", e))?;
|
||||
|
||||
tracing::info!("launchctl load output: {:?}", String::from_utf8_lossy(&output.stdout));
|
||||
if !output.status.success() {
|
||||
tracing::error!("launchctl load failed: {:?}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
// Update daemon state - we no longer own the process
|
||||
let mut state = daemon_state.write().await;
|
||||
state.started_by_us = false;
|
||||
state.daemon_process = None;
|
||||
tracing::info!("Updated daemon state: started_by_us = false");
|
||||
drop(state);
|
||||
|
||||
// Reset connection pool so it can reconnect to the service-managed daemon
|
||||
tracing::info!("Resetting connection pool to reconnect to service daemon");
|
||||
app_state.connection_pool.reset().await;
|
||||
|
||||
// Wait for daemon to start and become available
|
||||
tracing::info!("Waiting for daemon to become available...");
|
||||
for i in 0..30 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
if is_daemon_running(&socket_addr).await {
|
||||
tracing::info!("Daemon is running after {} attempts", i + 1);
|
||||
break;
|
||||
}
|
||||
if i == 29 {
|
||||
return Err("Daemon failed to start after installing service".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger reconnection
|
||||
tracing::info!("Triggering reconnection");
|
||||
app_state.connection_pool.ensure_connected(&app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::io::Write;
|
||||
|
||||
let home = std::env::var("HOME").map_err(|_| "Could not determine home directory".to_string())?;
|
||||
let systemd_dir = std::path::PathBuf::from(&home).join(".config/systemd/user");
|
||||
|
||||
std::fs::create_dir_all(&systemd_dir)
|
||||
.map_err(|e| format!("Failed to create systemd directory: {}", e))?;
|
||||
|
||||
let service_path = systemd_dir.join("spacedrive-daemon.service");
|
||||
|
||||
let daemon_path = std::env::current_exe()
|
||||
.map_err(|e| format!("Failed to get current exe: {}", e))?
|
||||
.parent()
|
||||
.ok_or_else(|| "Could not determine binary directory".to_string())?
|
||||
.join("sd-daemon");
|
||||
|
||||
if !daemon_path.exists() {
|
||||
return Err(format!("Daemon binary not found at {}", daemon_path.display()));
|
||||
}
|
||||
|
||||
let service_content = format!(
|
||||
r#"[Unit]
|
||||
Description=Spacedrive Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart={} --data-dir {}
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
"#,
|
||||
daemon_path.display(),
|
||||
data_dir.display()
|
||||
);
|
||||
|
||||
let mut file = std::fs::File::create(&service_path)
|
||||
.map_err(|e| format!("Failed to create service file: {}", e))?;
|
||||
file.write_all(service_content.as_bytes())
|
||||
.map_err(|e| format!("Failed to write service file: {}", e))?;
|
||||
|
||||
// Enable and start the service
|
||||
std::process::Command::new("systemctl")
|
||||
.args(&["--user", "daemon-reload"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to reload systemd: {}", e))?;
|
||||
|
||||
std::process::Command::new("systemctl")
|
||||
.args(&["--user", "enable", "spacedrive-daemon.service"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to enable service: {}", e))?;
|
||||
|
||||
std::process::Command::new("systemctl")
|
||||
.args(&["--user", "start", "spacedrive-daemon.service"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to start service: {}", e))?;
|
||||
|
||||
// Update daemon state - we no longer own the process
|
||||
let mut state = daemon_state.write().await;
|
||||
state.started_by_us = false;
|
||||
state.daemon_process = None;
|
||||
tracing::info!("Updated daemon state: started_by_us = false");
|
||||
drop(state);
|
||||
|
||||
// Reset connection pool so it can reconnect to the service-managed daemon
|
||||
tracing::info!("Resetting connection pool to reconnect to service daemon");
|
||||
app_state.connection_pool.reset().await;
|
||||
|
||||
// Wait for daemon to start and become available
|
||||
tracing::info!("Waiting for daemon to become available...");
|
||||
for i in 0..30 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
if is_daemon_running(&socket_addr).await {
|
||||
tracing::info!("Daemon is running after {} attempts", i + 1);
|
||||
break;
|
||||
}
|
||||
if i == 29 {
|
||||
return Err("Daemon failed to start after installing service".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger reconnection
|
||||
tracing::info!("Triggering reconnection");
|
||||
app_state.connection_pool.ensure_connected(&app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
Err("Service installation not supported on this platform".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Uninstall daemon service
|
||||
#[tauri::command]
|
||||
async fn uninstall_daemon_service() -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = std::env::var("HOME").map_err(|_| "Could not determine home directory".to_string())?;
|
||||
let plist_path = std::path::PathBuf::from(&home).join("Library/LaunchAgents/com.spacedrive.daemon.plist");
|
||||
|
||||
if plist_path.exists() {
|
||||
// Unload the service
|
||||
let _ = std::process::Command::new("launchctl")
|
||||
.args(&["unload", plist_path.to_str().unwrap()])
|
||||
.output();
|
||||
|
||||
std::fs::remove_file(&plist_path)
|
||||
.map_err(|e| format!("Failed to remove plist file: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let home = std::env::var("HOME").map_err(|_| "Could not determine home directory".to_string())?;
|
||||
let service_path = std::path::PathBuf::from(&home).join(".config/systemd/user/spacedrive-daemon.service");
|
||||
|
||||
if service_path.exists() {
|
||||
// Stop and disable the service
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(&["--user", "stop", "spacedrive-daemon.service"])
|
||||
.output();
|
||||
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(&["--user", "disable", "spacedrive-daemon.service"])
|
||||
.output();
|
||||
|
||||
std::fs::remove_file(&service_path)
|
||||
.map_err(|e| format!("Failed to remove service file: {}", e))?;
|
||||
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(&["--user", "daemon-reload"])
|
||||
.output();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
Err("Service installation not supported on this platform".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Open macOS system settings for background items
|
||||
#[tauri::command]
|
||||
async fn open_macos_settings() -> Result<(), String> {
|
||||
@@ -1214,6 +1540,9 @@ fn main() {
|
||||
get_daemon_status,
|
||||
start_daemon_process,
|
||||
stop_daemon_process,
|
||||
check_daemon_installed,
|
||||
install_daemon_service,
|
||||
uninstall_daemon_service,
|
||||
open_macos_settings,
|
||||
windows::show_window,
|
||||
windows::close_window,
|
||||
|
||||
@@ -177,6 +177,18 @@ export const platform: Platform = {
|
||||
return unlisten;
|
||||
},
|
||||
|
||||
async checkDaemonInstalled() {
|
||||
return await invoke<boolean>("check_daemon_installed");
|
||||
},
|
||||
|
||||
async installDaemonService() {
|
||||
await invoke("install_daemon_service");
|
||||
},
|
||||
|
||||
async uninstallDaemonService() {
|
||||
await invoke("uninstall_daemon_service");
|
||||
},
|
||||
|
||||
async openMacOSSettings() {
|
||||
await invoke("open_macos_settings");
|
||||
},
|
||||
|
||||
@@ -25,13 +25,16 @@ impl AsRef<DatabaseConnection> for Database {
|
||||
impl Database {
|
||||
/// Create a new database at the specified path
|
||||
pub async fn create(path: &Path) -> Result<Self, DbErr> {
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| DbErr::Custom(format!("Failed to create directory: {}", e)))?;
|
||||
}
|
||||
|
||||
let db_url = format!("sqlite://{}?mode=rwc", path.display());
|
||||
let db_url = if path.as_os_str() == ":memory:" {
|
||||
"sqlite::memory:".to_string()
|
||||
} else {
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| DbErr::Custom(format!("Failed to create directory: {}", e)))?;
|
||||
}
|
||||
format!("sqlite://{}?mode=rwc", path.display())
|
||||
};
|
||||
|
||||
// Connection pool sizing for concurrent indexing + sync operations
|
||||
// Supports: indexing (3-5) + sync (8-10) + content ID (3-5) + network (5-8) + headroom (5-10)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Copy } from "@phosphor-icons/react";
|
||||
import { useDaemonStatus } from "../hooks/useDaemonStatus";
|
||||
import { usePlatform } from "../platform";
|
||||
import { Button } from "@sd/ui";
|
||||
import folderIcon from "@sd/assets/icons/FolderNoSpace.png";
|
||||
|
||||
@@ -36,9 +37,21 @@ export function DaemonDisconnectedOverlay({
|
||||
}: {
|
||||
forceShow?: boolean;
|
||||
}) {
|
||||
const { isConnected, isChecking, retryConnection } = useDaemonStatus();
|
||||
const [runInBackground, setRunInBackground] = useState(true);
|
||||
const { isConnected, isChecking, isInstalled, startDaemon, installAndStartDaemon } = useDaemonStatus();
|
||||
const [installAsService, setInstallAsService] = useState(isInstalled);
|
||||
const prevConnected = useRef(isConnected);
|
||||
const platform = usePlatform();
|
||||
|
||||
// Update checkbox when installation state changes
|
||||
useEffect(() => {
|
||||
console.log('[DaemonDisconnectedOverlay] isInstalled changed to:', isInstalled);
|
||||
setInstallAsService(isInstalled);
|
||||
}, [isInstalled]);
|
||||
|
||||
// Log checkbox state changes
|
||||
useEffect(() => {
|
||||
console.log('[DaemonDisconnectedOverlay] installAsService checkbox state:', installAsService);
|
||||
}, [installAsService]);
|
||||
|
||||
// Reload when connection state changes from false to true
|
||||
useEffect(() => {
|
||||
@@ -68,15 +81,32 @@ export function DaemonDisconnectedOverlay({
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center backdrop-blur-lg bg-black/50"
|
||||
>
|
||||
<div className="fixed right-4 top-4 flex items-center gap-2 rounded-full border border-app-line bg-app-box px-3 py-1.5 text-xs font-medium">
|
||||
<div
|
||||
className={`size-2 rounded-full ${
|
||||
isConnected ? "bg-green-500" : "bg-red-500"
|
||||
} animate-pulse`}
|
||||
/>
|
||||
<span className="text-ink-dull">
|
||||
{isConnected ? "Connected" : "Disconnected"}
|
||||
</span>
|
||||
<div className="fixed right-4 top-4 flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 rounded-full border border-app-line bg-app-box px-3 py-1.5 text-xs font-medium">
|
||||
<div
|
||||
className={`size-2 rounded-full ${
|
||||
isChecking
|
||||
? "bg-yellow-500"
|
||||
: isConnected
|
||||
? "bg-green-500"
|
||||
: "bg-red-500"
|
||||
} animate-pulse`}
|
||||
/>
|
||||
<span className="text-ink-dull">
|
||||
{isChecking ? "Starting..." : isConnected ? "Connected" : "Disconnected"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 rounded-full border border-app-line bg-app-box px-3 py-1.5 text-xs font-medium">
|
||||
<div
|
||||
className={`size-2 rounded-full ${
|
||||
isInstalled ? "bg-blue-500" : "bg-gray-500"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-ink-dull">
|
||||
{isInstalled ? "Persistent" : "Temporary"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex max-w-4xl gap-8 rounded-lg border border-app-line p-8 shadow-2xl">
|
||||
@@ -102,17 +132,26 @@ export function DaemonDisconnectedOverlay({
|
||||
<label className="flex cursor-pointer items-center gap-2 text-sm text-ink">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={runInBackground}
|
||||
onChange={(e) => setRunInBackground(e.target.checked)}
|
||||
checked={installAsService}
|
||||
onChange={async (e) => {
|
||||
const shouldInstall = e.target.checked;
|
||||
setInstallAsService(shouldInstall);
|
||||
|
||||
if (shouldInstall) {
|
||||
await installAndStartDaemon();
|
||||
} else {
|
||||
await platform.uninstallDaemonService?.();
|
||||
}
|
||||
}}
|
||||
className="size-4 cursor-pointer rounded border-app-line bg-app accent-accent"
|
||||
/>
|
||||
<span>Run daemon in background</span>
|
||||
<span>Install as persistent service</span>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="gray">Help</Button>
|
||||
<Button
|
||||
onClick={retryConnection}
|
||||
onClick={startDaemon}
|
||||
disabled={isChecking}
|
||||
variant="accent"
|
||||
>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { DirectorySortBy } from "@sd/ts-client";
|
||||
|
||||
export function GridView() {
|
||||
const { currentPath, sortBy, viewSettings } = useExplorer();
|
||||
const { isSelected, focusedIndex, selectedFiles, selectFile } = useSelection();
|
||||
const { isSelected, focusedIndex, selectedFiles, selectFile, clearSelection } = useSelection();
|
||||
const { gridSize, gapSize } = viewSettings;
|
||||
|
||||
const directoryQuery = useNormalizedCache({
|
||||
@@ -26,13 +26,21 @@ export function GridView() {
|
||||
|
||||
const files = directoryQuery.data?.files || [];
|
||||
|
||||
const handleContainerClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
clearSelection();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid p-3"
|
||||
className="grid p-3 min-h-full"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(${gridSize}px, 1fr))`,
|
||||
gridAutoRows: 'max-content',
|
||||
gap: `${gapSize}px`,
|
||||
}}
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
{files.map((file, index) => (
|
||||
<FileCard
|
||||
|
||||
@@ -4,6 +4,7 @@ import { usePlatform } from '../platform';
|
||||
export interface DaemonStatus {
|
||||
isConnected: boolean;
|
||||
isChecking: boolean;
|
||||
isInstalled: boolean;
|
||||
}
|
||||
|
||||
export function useDaemonStatus() {
|
||||
@@ -11,6 +12,7 @@ export function useDaemonStatus() {
|
||||
const [status, setStatus] = useState<DaemonStatus>({
|
||||
isConnected: true,
|
||||
isChecking: false,
|
||||
isInstalled: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,6 +34,7 @@ export function useDaemonStatus() {
|
||||
if (mounted) {
|
||||
const isRunning = daemonStatus?.is_running ?? false;
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
isConnected: isRunning,
|
||||
// Only clear isChecking if we're connected (daemon started successfully)
|
||||
isChecking: isRunning ? false : prev.isChecking,
|
||||
@@ -56,11 +59,13 @@ export function useDaemonStatus() {
|
||||
|
||||
const setupListeners = async () => {
|
||||
unlistenConnected = await platform.onDaemonConnected?.(() => {
|
||||
console.log('[useDaemonStatus] daemon-connected event received');
|
||||
if (mounted) {
|
||||
setStatus({
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
isConnected: true,
|
||||
isChecking: false,
|
||||
});
|
||||
}));
|
||||
|
||||
// Stop polling when connected
|
||||
if (checkInterval) {
|
||||
@@ -71,6 +76,7 @@ export function useDaemonStatus() {
|
||||
});
|
||||
|
||||
unlistenDisconnected = await platform.onDaemonDisconnected?.(() => {
|
||||
console.log('[useDaemonStatus] daemon-disconnected event received');
|
||||
if (mounted) {
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
@@ -86,6 +92,7 @@ export function useDaemonStatus() {
|
||||
});
|
||||
|
||||
unlistenStarting = await platform.onDaemonStarting?.(() => {
|
||||
console.log('[useDaemonStatus] daemon-starting event received');
|
||||
if (mounted) {
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
@@ -95,8 +102,25 @@ export function useDaemonStatus() {
|
||||
});
|
||||
};
|
||||
|
||||
// Initial check
|
||||
// Check if daemon is installed as a service
|
||||
const checkInstallation = async () => {
|
||||
try {
|
||||
const installed = await platform.checkDaemonInstalled?.();
|
||||
console.log('[useDaemonStatus] checkInstallation result:', installed);
|
||||
if (mounted) {
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
isInstalled: installed ?? false,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[useDaemonStatus] Failed to check daemon installation:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial checks
|
||||
checkDaemonStatus();
|
||||
checkInstallation();
|
||||
|
||||
// Set up event listeners
|
||||
setupListeners();
|
||||
@@ -116,7 +140,7 @@ export function useDaemonStatus() {
|
||||
};
|
||||
}, [platform]);
|
||||
|
||||
const retryConnection = async () => {
|
||||
const startDaemon = async () => {
|
||||
try {
|
||||
await platform.startDaemonProcess?.();
|
||||
} catch (error) {
|
||||
@@ -128,8 +152,28 @@ export function useDaemonStatus() {
|
||||
}
|
||||
};
|
||||
|
||||
const installAndStartDaemon = async () => {
|
||||
console.log('[useDaemonStatus] installAndStartDaemon called');
|
||||
try {
|
||||
console.log('[useDaemonStatus] Calling platform.installDaemonService()');
|
||||
await platform.installDaemonService?.();
|
||||
console.log('[useDaemonStatus] installDaemonService completed, updating isInstalled state');
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
isInstalled: true,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[useDaemonStatus] Failed to install daemon service:', error);
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
isChecking: false,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...status,
|
||||
retryConnection,
|
||||
startDaemon,
|
||||
installAndStartDaemon,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,6 +108,15 @@ export type Platform = {
|
||||
/** Listen for daemon starting events (Tauri only) */
|
||||
onDaemonStarting?(callback: () => void): Promise<() => void>;
|
||||
|
||||
/** Check if daemon is installed as a service (Tauri only) */
|
||||
checkDaemonInstalled?(): Promise<boolean>;
|
||||
|
||||
/** Install daemon as a service (Tauri only) */
|
||||
installDaemonService?(): Promise<void>;
|
||||
|
||||
/** Uninstall daemon service (Tauri only) */
|
||||
uninstallDaemonService?(): Promise<void>;
|
||||
|
||||
/** Open macOS system settings (Tauri/macOS only) */
|
||||
openMacOSSettings?(): Promise<void>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user