[appimage] Phase 3 komplett: Bridge-Daemon + Unix Socket IPC
Some checks failed
Build AppImage / build (push) Has been cancelled
Some checks failed
Build AppImage / build (push) Has been cancelled
Bridge überlebt jetzt App-Neustarts als eigenständiger Daemon-Prozess. Kommunikation über Unix Domain Socket statt stdio — async, Auto-Reconnect, PID-Tracking. Fallback auf stdio-Modus wenn UDS nicht verfügbar. Neue Commands: get_bridge_status, stop_bridge_daemon. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d29bbd7bfd
commit
e36209690e
7 changed files with 433 additions and 51 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -6,9 +6,18 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [Unreleased] - 2026-04-21
|
## [Unreleased] - 2026-04-22
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
- **Bridge-Daemon (Phase 3)**: Bridge läuft als eigenständiger Daemon-Prozess, überlebt App-Neustarts — kein Cold-Start mehr (`claude-bridge.js --socket`, `claude.rs`)
|
||||||
|
- **Unix Socket IPC (Phase 3)**: Kommunikation über Unix Domain Socket statt stdio — async, kein Block, Auto-Reconnect bei Verbindungsverlust (`claude.rs`, `claude-bridge.js`)
|
||||||
|
- **Bridge-Status API**: `get_bridge_status` Command — zeigt Verbindungsmodus (UDS/stdio), Daemon-PID, Socket-Pfad
|
||||||
|
- **Daemon-Steuerung**: `stop_bridge_daemon` Command zum expliziten Stoppen des Daemon-Prozesses
|
||||||
|
- **Modus-Indikator**: Badge im ChatPanel zeigt aktuellen Agent-Modus (Handlanger/Experten/Auto) mit Verarbeitungsphase
|
||||||
|
- **Plan-Erkennung**: Claude-Antworten mit Plänen werden automatisch als Slides an das Präsentationsfenster gesendet (`planPresentation.ts`)
|
||||||
|
- **Session-Projekt-Filter**: Sessions werden nach aktivem Projekt/Workspace gefiltert (`db.rs`, `session.rs`)
|
||||||
|
- **Weibliche TTS-Stimme**: Kerstin als Standard-Stimme, 5 deutsche Stimmen wählbar (`voice.rs`)
|
||||||
|
- **UTF-8 Crash Fix**: Kein Panic mehr bei Multi-Byte-Zeichen in DB-Abfragen (`db.rs`, `knowledge.rs`)
|
||||||
- **Guard-Rails UI (Live)**: 3-Tab-Ansicht (Live-Feed/Regeln/Blockiert), Risiko-Statistik-Leiste, Ein-Klick-Freigabe bei Bestätigungsbedarf, guard-check Events vom Backend (`GuardRailsPanel.svelte`, `guard.rs`)
|
- **Guard-Rails UI (Live)**: 3-Tab-Ansicht (Live-Feed/Regeln/Blockiert), Risiko-Statistik-Leiste, Ein-Klick-Freigabe bei Bestätigungsbedarf, guard-check Events vom Backend (`GuardRailsPanel.svelte`, `guard.rs`)
|
||||||
- **D-Bus Desktop-Aktionen**: 10 vordefinierte Aktionen (Dolphin, Kate, Konsole, Firefox, Notify, Lock Screen), Aktionen-Grid im ProgramsPanel, CLI/GUI-Unterscheidung (`programs.rs`, `ProgramsPanel.svelte`)
|
- **D-Bus Desktop-Aktionen**: 10 vordefinierte Aktionen (Dolphin, Kate, Konsole, Firefox, Notify, Lock Screen), Aktionen-Grid im ProgramsPanel, CLI/GUI-Unterscheidung (`programs.rs`, `ProgramsPanel.svelte`)
|
||||||
- **Screenshot-Analyse**: Bildschirmbereich oder Vollbild capturen via spectacle/scrot/gnome-screenshot, Vorschau im Panel, "An Claude senden" Button (`programs.rs`, `ProgramsPanel.svelte`)
|
- **Screenshot-Analyse**: Bildschirmbereich oder Vollbild capturen via spectacle/scrot/gnome-screenshot, Vorschau im Panel, "An Claude senden" Button (`programs.rs`, `ProgramsPanel.svelte`)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Claude Desktop — Roadmap
|
# Claude Desktop — Roadmap
|
||||||
|
|
||||||
Stand: 21.04.2026
|
Stand: 22.04.2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -49,8 +49,8 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig:
|
||||||
| ✅ Auto-Retry (Netzwerk) | `claude-bridge.js` | 3x Backoff bei Rate-Limit/5xx |
|
| ✅ Auto-Retry (Netzwerk) | `claude-bridge.js` | 3x Backoff bei Rate-Limit/5xx |
|
||||||
| ✅ Bridge Heartbeat | `claude-bridge.js` | 30s Pulse an Rust |
|
| ✅ Bridge Heartbeat | `claude-bridge.js` | 30s Pulse an Rust |
|
||||||
| ✅ FIFO Message Queue | `ChatPanel.svelte` | Mehrere Nachrichten queuen |
|
| ✅ FIFO Message Queue | `ChatPanel.svelte` | Mehrere Nachrichten queuen |
|
||||||
| ⬜ Bridge-Daemon | `claude.rs`, `claude-bridge.js` | Bridge ueberlebt App-Neustart |
|
| ✅ Bridge-Daemon | `claude.rs`, `claude-bridge.js` | Bridge ueberlebt App-Neustart (--socket Flag) |
|
||||||
| ⬜ Unix Socket IPC | `claude.rs`, `claude-bridge.js` | stdio → UDS (async, kein Block) |
|
| ✅ Unix Socket IPC | `claude.rs`, `claude-bridge.js` | stdio → UDS (async, Reconnect, PID-Tracking) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,32 @@
|
||||||
// Nutzt @anthropic-ai/claude-agent-sdk (query-Funktion)
|
// Nutzt @anthropic-ai/claude-agent-sdk (query-Funktion)
|
||||||
// OAuth-Auth funktioniert automatisch (Claude Max Abo)
|
// OAuth-Auth funktioniert automatisch (Claude Max Abo)
|
||||||
// Kein CLI-Spawn, kein Overhead — direkte SDK-Aufrufe
|
// Kein CLI-Spawn, kein Overhead — direkte SDK-Aufrufe
|
||||||
|
//
|
||||||
|
// Modi:
|
||||||
|
// stdio (Default): node claude-bridge.js
|
||||||
|
// UDS-Daemon: node claude-bridge.js --socket /tmp/claude-bridge.sock
|
||||||
|
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { createInterface } from 'node:readline';
|
import { createInterface } from 'node:readline';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { createServer } from 'node:net';
|
||||||
|
import { writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
||||||
|
|
||||||
// Prozess am Leben halten + Heartbeat an Rust alle 30s
|
// ============ IPC-Modus erkennen ============
|
||||||
|
|
||||||
|
const socketArg = process.argv.indexOf('--socket');
|
||||||
|
const SOCKET_PATH = socketArg !== -1 ? process.argv[socketArg + 1] : null;
|
||||||
|
const PID_PATH = SOCKET_PATH ? SOCKET_PATH.replace('.sock', '.pid') : null;
|
||||||
|
const IS_DAEMON = !!SOCKET_PATH;
|
||||||
|
|
||||||
|
// Aktive UDS-Clients (bei Daemon-Modus)
|
||||||
|
let udsClients = new Set();
|
||||||
|
|
||||||
|
// Prozess am Leben halten + Heartbeat alle 30s
|
||||||
const keepAlive = setInterval(() => {
|
const keepAlive = setInterval(() => {
|
||||||
sendEvent('heartbeat', { ts: Date.now(), uptime: process.uptime() });
|
sendEvent('heartbeat', { ts: Date.now(), uptime: process.uptime() });
|
||||||
}, 30000);
|
}, 30000);
|
||||||
process.stdin.resume();
|
if (!IS_DAEMON) process.stdin.resume();
|
||||||
|
|
||||||
// ============ State ============
|
// ============ State ============
|
||||||
|
|
||||||
|
|
@ -203,7 +219,22 @@ function getSubagentType(toolName, input) {
|
||||||
// ============ Kommunikation mit Tauri ============
|
// ============ Kommunikation mit Tauri ============
|
||||||
|
|
||||||
function sendToTauri(msg) {
|
function sendToTauri(msg) {
|
||||||
process.stdout.write(JSON.stringify(msg) + '\n');
|
const line = JSON.stringify(msg) + '\n';
|
||||||
|
|
||||||
|
if (IS_DAEMON) {
|
||||||
|
// UDS-Modus: An alle verbundenen Clients senden
|
||||||
|
for (const client of udsClients) {
|
||||||
|
try {
|
||||||
|
client.write(line);
|
||||||
|
} catch (err) {
|
||||||
|
process.stderr.write(`UDS-Client Schreibfehler: ${err.message}\n`);
|
||||||
|
udsClients.delete(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// stdio-Modus: An stdout (wie bisher)
|
||||||
|
process.stdout.write(line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendEvent(event, payload = {}) {
|
function sendEvent(event, payload = {}) {
|
||||||
|
|
@ -828,6 +859,74 @@ function handleCommand(msg) {
|
||||||
|
|
||||||
// ============ Main ============
|
// ============ Main ============
|
||||||
|
|
||||||
|
function cleanupDaemon() {
|
||||||
|
if (IS_DAEMON) {
|
||||||
|
try { if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH); } catch {}
|
||||||
|
try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {}
|
||||||
|
process.stderr.write('🔌 Daemon aufgeräumt\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_DAEMON) {
|
||||||
|
// ---- UDS-Daemon-Modus ----
|
||||||
|
// Alte Socket-Datei aufräumen
|
||||||
|
try { if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH); } catch {}
|
||||||
|
|
||||||
|
// PID-File schreiben
|
||||||
|
writeFileSync(PID_PATH, String(process.pid));
|
||||||
|
|
||||||
|
const udsServer = createServer((client) => {
|
||||||
|
process.stderr.write(`🔌 UDS-Client verbunden (${udsClients.size + 1} aktiv)\n`);
|
||||||
|
udsClients.add(client);
|
||||||
|
|
||||||
|
// JSON-Lines Protokoll: jede Zeile = ein Befehl
|
||||||
|
let buffer = '';
|
||||||
|
client.on('data', (data) => {
|
||||||
|
buffer += data.toString();
|
||||||
|
let idx;
|
||||||
|
while ((idx = buffer.indexOf('\n')) !== -1) {
|
||||||
|
const line = buffer.slice(0, idx).trim();
|
||||||
|
buffer = buffer.slice(idx + 1);
|
||||||
|
if (!line) continue;
|
||||||
|
try {
|
||||||
|
handleCommand(JSON.parse(line));
|
||||||
|
} catch (err) {
|
||||||
|
process.stderr.write(`UDS Ungültige Eingabe: ${err.message}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('end', () => {
|
||||||
|
udsClients.delete(client);
|
||||||
|
process.stderr.write(`🔌 UDS-Client getrennt (${udsClients.size} aktiv)\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err) => {
|
||||||
|
udsClients.delete(client);
|
||||||
|
process.stderr.write(`UDS-Client Fehler: ${err.message}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Neuem Client sofort den aktuellen Status senden
|
||||||
|
try {
|
||||||
|
client.write(JSON.stringify({
|
||||||
|
type: 'event', event: 'ready',
|
||||||
|
payload: { version: '1.2.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS, daemon: true }
|
||||||
|
}) + '\n');
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
udsServer.listen(SOCKET_PATH, () => {
|
||||||
|
process.stderr.write(`🔌 Bridge-Daemon lauscht auf ${SOCKET_PATH} (PID: ${process.pid})\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
udsServer.on('error', (err) => {
|
||||||
|
process.stderr.write(`UDS-Server Fehler: ${err.message}\n`);
|
||||||
|
cleanupDaemon();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// ---- stdio-Modus (Kompatibilität) ----
|
||||||
const rl = createInterface({ input: process.stdin });
|
const rl = createInterface({ input: process.stdin });
|
||||||
rl.on('line', (line) => {
|
rl.on('line', (line) => {
|
||||||
if (!line.trim()) return;
|
if (!line.trim()) return;
|
||||||
|
|
@ -841,9 +940,13 @@ rl.on('line', (line) => {
|
||||||
rl.on('close', () => {
|
rl.on('close', () => {
|
||||||
process.stderr.write('stdin geschlossen\n');
|
process.stderr.write('stdin geschlossen\n');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
process.on('SIGTERM', () => { clearInterval(keepAlive); process.exit(0); });
|
process.on('SIGTERM', () => { clearInterval(keepAlive); cleanupDaemon(); process.exit(0); });
|
||||||
process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); });
|
process.on('SIGINT', () => { clearInterval(keepAlive); cleanupDaemon(); process.exit(0); });
|
||||||
|
process.on('exit', () => { cleanupDaemon(); });
|
||||||
|
|
||||||
// Bereit
|
// Bereit-Signal (im stdio-Modus sofort senden, im Daemon-Modus pro Client bei Connect)
|
||||||
sendEvent('ready', { version: '1.1.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS });
|
if (!IS_DAEMON) {
|
||||||
|
sendEvent('ready', { version: '1.2.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS });
|
||||||
|
}
|
||||||
|
|
|
||||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
|
@ -482,6 +482,7 @@ dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"libc",
|
||||||
"mysql_async",
|
"mysql_async",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ futures-util = "0.3"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
tauri-plugin-dialog = "2.7.0"
|
tauri-plugin-dialog = "2.7.0"
|
||||||
tauri-plugin-global-shortcut = "2.3.1"
|
tauri-plugin-global-shortcut = "2.3.1"
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
webkit2gtk = "2.0"
|
webkit2gtk = "2.0"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
// Claude Desktop — Claude SDK Integration
|
// Claude Desktop — Claude SDK Integration
|
||||||
// Kommunikation mit Claude Code via Node.js Child-Process
|
// Kommunikation mit Claude Code via Node.js Child-Process oder Unix Domain Socket
|
||||||
|
//
|
||||||
|
// Modi:
|
||||||
|
// 1. UDS-Daemon: Bridge läuft als eigenständiger Prozess, App verbindet sich über Socket
|
||||||
|
// 2. stdio (Fallback): Bridge als Child-Process mit stdin/stdout (wie bisher)
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
|
@ -7,9 +11,16 @@ use std::process::{Command, Stdio};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::knowledge;
|
use crate::knowledge;
|
||||||
|
|
||||||
|
/// Standard-Pfade für UDS-Daemon
|
||||||
|
const SOCKET_PATH: &str = "/tmp/claude-bridge.sock";
|
||||||
|
const PID_PATH: &str = "/tmp/claude-bridge.pid";
|
||||||
|
|
||||||
/// Status eines Agents
|
/// Status eines Agents
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AgentStatus {
|
pub struct AgentStatus {
|
||||||
|
|
@ -53,10 +64,26 @@ struct BridgeMessage {
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// IPC-Modus der Bridge-Verbindung
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum BridgeConnection {
|
||||||
|
/// stdio: Bridge ist ein Child-Process
|
||||||
|
Stdio {
|
||||||
|
#[allow(dead_code)] // process-Handle muss am Leben bleiben!
|
||||||
|
process: std::process::Child,
|
||||||
|
stdin: std::process::ChildStdin,
|
||||||
|
},
|
||||||
|
/// UDS: Bridge ist ein Daemon, Verbindung über Unix Socket
|
||||||
|
#[cfg(unix)]
|
||||||
|
Uds {
|
||||||
|
stream: UnixStream,
|
||||||
|
daemon_pid: Option<u32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/// Globaler State für die Bridge
|
/// Globaler State für die Bridge
|
||||||
pub struct ClaudeState {
|
pub struct ClaudeState {
|
||||||
pub bridge_process: Option<std::process::Child>,
|
pub connection: Option<BridgeConnection>,
|
||||||
pub bridge_stdin: Option<std::process::ChildStdin>,
|
|
||||||
pub request_counter: u64,
|
pub request_counter: u64,
|
||||||
pub agents: Vec<AgentStatus>,
|
pub agents: Vec<AgentStatus>,
|
||||||
}
|
}
|
||||||
|
|
@ -64,20 +91,40 @@ pub struct ClaudeState {
|
||||||
impl Default for ClaudeState {
|
impl Default for ClaudeState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
bridge_process: None,
|
connection: None,
|
||||||
bridge_stdin: None,
|
|
||||||
request_counter: 0,
|
request_counter: 0,
|
||||||
agents: vec![],
|
agents: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bridge starten
|
impl ClaudeState {
|
||||||
pub fn start_bridge(app: &AppHandle) -> Result<(), String> {
|
/// Prüft ob eine aktive Verbindung besteht
|
||||||
// Smart Hints v2: Session-Topic zurücksetzen bei neuer Bridge/Session
|
pub fn is_connected(&self) -> bool {
|
||||||
knowledge::reset_session_topic();
|
self.connection.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
// Script-Pfad ermitteln
|
/// Schreibt eine Zeile an die Bridge (JSON-Line)
|
||||||
|
pub fn write_line(&mut self, line: &str) -> Result<(), String> {
|
||||||
|
match &mut self.connection {
|
||||||
|
Some(BridgeConnection::Stdio { stdin, .. }) => {
|
||||||
|
writeln!(stdin, "{}", line).map_err(|e| e.to_string())?;
|
||||||
|
stdin.flush().map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
Some(BridgeConnection::Uds { stream, .. }) => {
|
||||||
|
writeln!(stream, "{}", line).map_err(|e| e.to_string())?;
|
||||||
|
stream.flush().map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
None => Err("Bridge nicht verbunden".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Script-Pfad der Bridge ermitteln
|
||||||
|
fn find_bridge_script() -> Result<std::path::PathBuf, String> {
|
||||||
let exe_dir = std::env::current_exe()
|
let exe_dir = std::env::current_exe()
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.parent()
|
.parent()
|
||||||
|
|
@ -97,22 +144,190 @@ pub fn start_bridge(app: &AppHandle) -> Result<(), String> {
|
||||||
std::env::current_dir().unwrap_or_default().join("scripts/claude-bridge.js"),
|
std::env::current_dir().unwrap_or_default().join("scripts/claude-bridge.js"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let script_path = candidates.iter()
|
candidates.iter()
|
||||||
.find(|p| p.exists())
|
.find(|p| p.exists())
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| format!("claude-bridge.js nicht gefunden. Gesucht in: {:?}", candidates))?;
|
.ok_or_else(|| format!("claude-bridge.js nicht gefunden. Gesucht in: {:?}", candidates))
|
||||||
|
}
|
||||||
|
|
||||||
println!("🔌 Starte Claude Bridge: {:?}", script_path);
|
/// Prüft ob ein Daemon-Prozess noch lebt
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn is_daemon_alive() -> Option<u32> {
|
||||||
|
let pid_path = std::path::Path::new(PID_PATH);
|
||||||
|
if !pid_path.exists() { return None; }
|
||||||
|
|
||||||
// Arbeitsverzeichnis = Projektroot (wo node_modules liegt)
|
let pid_str = std::fs::read_to_string(pid_path).ok()?;
|
||||||
|
let pid: u32 = pid_str.trim().parse().ok()?;
|
||||||
|
|
||||||
|
// Signal 0 prüft ob Prozess existiert, ohne ihn zu beeinflussen
|
||||||
|
let alive = unsafe { libc::kill(pid as i32, 0) == 0 };
|
||||||
|
if alive { Some(pid) } else { None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Startet Bridge-Daemon als eigenständigen Prozess (überlebt App-Neustart)
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn start_daemon(script_path: &std::path::Path) -> Result<u32, String> {
|
||||||
let project_dir = script_path.parent()
|
let project_dir = script_path.parent()
|
||||||
.and_then(|p| p.parent())
|
.and_then(|p| p.parent())
|
||||||
.unwrap_or_else(|| std::path::Path::new("."));
|
.unwrap_or_else(|| std::path::Path::new("."));
|
||||||
|
|
||||||
|
println!("🔌 Starte Bridge-Daemon: {:?} --socket {}", script_path, SOCKET_PATH);
|
||||||
|
|
||||||
|
let child = Command::new("node")
|
||||||
|
.arg(script_path)
|
||||||
|
.arg("--socket")
|
||||||
|
.arg(SOCKET_PATH)
|
||||||
|
.current_dir(project_dir)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Bridge-Daemon konnte nicht gestartet werden: {}", e))?;
|
||||||
|
|
||||||
|
let pid = child.id();
|
||||||
|
|
||||||
|
// Stderr in separatem Thread lesen (Daemon-Logs)
|
||||||
|
if let Some(stderr) = child.stderr {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let reader = BufReader::new(stderr);
|
||||||
|
for line in reader.lines().map_while(Result::ok) {
|
||||||
|
println!("🔌 Daemon: {}", line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kurz warten bis Socket-Datei erstellt wird
|
||||||
|
for _ in 0..20 {
|
||||||
|
if std::path::Path::new(SOCKET_PATH).exists() {
|
||||||
|
println!("✅ Bridge-Daemon gestartet (PID: {})", pid);
|
||||||
|
return Ok(pid);
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!("Bridge-Daemon gestartet (PID: {}), aber Socket {} nicht erstellt", pid, SOCKET_PATH))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verbindet sich mit dem UDS-Daemon und startet Reader-Thread
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn connect_uds(app: &AppHandle, daemon_pid: Option<u32>) -> Result<(), String> {
|
||||||
|
let stream = UnixStream::connect(SOCKET_PATH)
|
||||||
|
.map_err(|e| format!("UDS-Verbindung fehlgeschlagen: {}", e))?;
|
||||||
|
|
||||||
|
// Reader-Stream klonen für den Lese-Thread
|
||||||
|
let reader_stream = stream.try_clone()
|
||||||
|
.map_err(|e| format!("UDS-Stream klonen fehlgeschlagen: {}", e))?;
|
||||||
|
|
||||||
|
// Verbindung speichern
|
||||||
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||||
|
{
|
||||||
|
let mut state = state.lock().unwrap();
|
||||||
|
state.connection = Some(BridgeConnection::Uds {
|
||||||
|
stream,
|
||||||
|
daemon_pid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader-Thread: JSON-Lines vom Socket lesen
|
||||||
|
let app_handle = app.clone();
|
||||||
|
let state_for_reconnect = app.state::<Arc<Mutex<ClaudeState>>>().inner().clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let reader = BufReader::new(reader_stream);
|
||||||
|
for line in reader.lines().map_while(Result::ok) {
|
||||||
|
if let Ok(msg) = serde_json::from_str::<BridgeMessage>(&line) {
|
||||||
|
handle_bridge_message(&app_handle, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("⚠️ UDS-Verbindung getrennt — versuche Reconnect...");
|
||||||
|
|
||||||
|
// Verbindung als geschlossen markieren
|
||||||
|
{
|
||||||
|
let mut state = state_for_reconnect.lock().unwrap();
|
||||||
|
state.connection = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatischer Reconnect (3 Versuche)
|
||||||
|
for attempt in 1..=3 {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(attempt));
|
||||||
|
if std::path::Path::new(SOCKET_PATH).exists() {
|
||||||
|
println!("🔄 UDS Reconnect Versuch {}/3...", attempt);
|
||||||
|
match connect_uds(&app_handle, daemon_pid) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("✅ UDS Reconnect erfolgreich");
|
||||||
|
let _ = app_handle.emit("bridge-ready", ());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => println!("⚠️ UDS Reconnect fehlgeschlagen: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("❌ UDS Reconnect endgültig fehlgeschlagen");
|
||||||
|
let _ = app_handle.emit("bridge-disconnected", ());
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("✅ UDS-Verbindung hergestellt");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bridge starten — versucht erst UDS-Daemon, dann stdio-Fallback
|
||||||
|
pub fn start_bridge(app: &AppHandle) -> Result<(), String> {
|
||||||
|
// Smart Hints v2: Session-Topic zurücksetzen bei neuer Bridge/Session
|
||||||
|
knowledge::reset_session_topic();
|
||||||
|
|
||||||
|
// Bereits verbunden?
|
||||||
|
{
|
||||||
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||||
|
let state_guard = state.lock().unwrap();
|
||||||
|
if state_guard.is_connected() {
|
||||||
|
println!("🔌 Bridge bereits verbunden");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let script_path = find_bridge_script()?;
|
||||||
|
|
||||||
|
// ---- UDS-Daemon-Modus (bevorzugt) ----
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
// 1. Läuft schon ein Daemon?
|
||||||
|
if let Some(pid) = is_daemon_alive() {
|
||||||
|
println!("🔌 Existierender Bridge-Daemon gefunden (PID: {})", pid);
|
||||||
|
if let Ok(()) = connect_uds(app, Some(pid)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!("⚠️ Verbindung zu bestehendem Daemon fehlgeschlagen, starte neu...");
|
||||||
|
// Alten Daemon killen
|
||||||
|
unsafe { libc::kill(pid as i32, libc::SIGTERM); }
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Neuen Daemon starten
|
||||||
|
match start_daemon(&script_path) {
|
||||||
|
Ok(pid) => {
|
||||||
|
match connect_uds(app, Some(pid)) {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(e) => println!("⚠️ UDS-Verbindung nach Daemon-Start fehlgeschlagen: {} — Fallback auf stdio", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => println!("⚠️ Daemon-Start fehlgeschlagen: {} — Fallback auf stdio", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- stdio-Fallback (Kompatibilität) ----
|
||||||
|
start_bridge_stdio(app, &script_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bridge im stdio-Modus starten (Child-Process, wie bisher)
|
||||||
|
fn start_bridge_stdio(app: &AppHandle, script_path: &std::path::Path) -> Result<(), String> {
|
||||||
|
let project_dir = script_path.parent()
|
||||||
|
.and_then(|p| p.parent())
|
||||||
|
.unwrap_or_else(|| std::path::Path::new("."));
|
||||||
|
|
||||||
|
println!("🔌 Starte Claude Bridge (stdio): {:?}", script_path);
|
||||||
println!("📂 Bridge Arbeitsverzeichnis: {:?}", project_dir);
|
println!("📂 Bridge Arbeitsverzeichnis: {:?}", project_dir);
|
||||||
|
|
||||||
let mut child = Command::new("node")
|
let mut child = Command::new("node")
|
||||||
.arg(&script_path)
|
.arg(script_path)
|
||||||
.current_dir(project_dir)
|
.current_dir(project_dir)
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
|
|
@ -128,8 +343,10 @@ pub fn start_bridge(app: &AppHandle) -> Result<(), String> {
|
||||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||||
{
|
{
|
||||||
let mut state = state.lock().unwrap();
|
let mut state = state.lock().unwrap();
|
||||||
state.bridge_process = Some(child);
|
state.connection = Some(BridgeConnection::Stdio {
|
||||||
state.bridge_stdin = Some(stdin);
|
process: child,
|
||||||
|
stdin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stderr in separatem Thread lesen und loggen
|
// Stderr in separatem Thread lesen und loggen
|
||||||
|
|
@ -368,13 +585,8 @@ fn send_to_bridge_full(
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(stdin) = &mut state.bridge_stdin {
|
state.write_line(&msg.to_string())?;
|
||||||
writeln!(stdin, "{}", msg.to_string()).map_err(|e| e.to_string())?;
|
|
||||||
stdin.flush().map_err(|e| e.to_string())?;
|
|
||||||
Ok(request_id)
|
Ok(request_id)
|
||||||
} else {
|
|
||||||
Err("Bridge nicht gestartet".to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Tauri Commands ============
|
// ============ Tauri Commands ============
|
||||||
|
|
@ -388,7 +600,7 @@ pub async fn send_message(app: AppHandle, message: String) -> Result<String, Str
|
||||||
let needs_start = {
|
let needs_start = {
|
||||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||||
let state_guard = state.lock().unwrap();
|
let state_guard = state.lock().unwrap();
|
||||||
state_guard.bridge_stdin.is_none()
|
!state_guard.is_connected()
|
||||||
};
|
};
|
||||||
|
|
||||||
if needs_start {
|
if needs_start {
|
||||||
|
|
@ -549,7 +761,7 @@ pub async fn set_model(app: AppHandle, model: String) -> Result<String, String>
|
||||||
let needs_start = {
|
let needs_start = {
|
||||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||||
let state_guard = state.lock().unwrap();
|
let state_guard = state.lock().unwrap();
|
||||||
state_guard.bridge_stdin.is_none()
|
!state_guard.is_connected()
|
||||||
};
|
};
|
||||||
|
|
||||||
if needs_start {
|
if needs_start {
|
||||||
|
|
@ -618,7 +830,7 @@ pub async fn set_agent_mode(app: AppHandle, mode: String) -> Result<String, Stri
|
||||||
let needs_start = {
|
let needs_start = {
|
||||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||||
let state_guard = state.lock().unwrap();
|
let state_guard = state.lock().unwrap();
|
||||||
state_guard.bridge_stdin.is_none()
|
!state_guard.is_connected()
|
||||||
};
|
};
|
||||||
|
|
||||||
if needs_start {
|
if needs_start {
|
||||||
|
|
@ -743,7 +955,7 @@ pub async fn init_sticky_context(app: AppHandle) -> Result<StickyContextInfo, St
|
||||||
let needs_start = {
|
let needs_start = {
|
||||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||||
let state_guard = state.lock().unwrap();
|
let state_guard = state.lock().unwrap();
|
||||||
state_guard.bridge_stdin.is_none()
|
!state_guard.is_connected()
|
||||||
};
|
};
|
||||||
|
|
||||||
if needs_start {
|
if needs_start {
|
||||||
|
|
@ -762,3 +974,55 @@ pub async fn init_sticky_context(app: AppHandle) -> Result<StickyContextInfo, St
|
||||||
|
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bridge-Verbindungsstatus abfragen
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct BridgeStatus {
|
||||||
|
pub connected: bool,
|
||||||
|
pub mode: String, // "uds" | "stdio" | "disconnected"
|
||||||
|
pub daemon_pid: Option<u32>,
|
||||||
|
pub socket_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
|
||||||
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||||
|
let state = state.lock().unwrap();
|
||||||
|
|
||||||
|
let (connected, mode, daemon_pid) = match &state.connection {
|
||||||
|
Some(BridgeConnection::Stdio { .. }) => (true, "stdio".to_string(), None),
|
||||||
|
#[cfg(unix)]
|
||||||
|
Some(BridgeConnection::Uds { daemon_pid, .. }) => (true, "uds".to_string(), *daemon_pid),
|
||||||
|
None => (false, "disconnected".to_string(), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(BridgeStatus {
|
||||||
|
connected,
|
||||||
|
mode,
|
||||||
|
daemon_pid,
|
||||||
|
socket_path: SOCKET_PATH.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bridge-Daemon explizit stoppen (z.B. für Neustart oder Debugging)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn stop_bridge_daemon(app: AppHandle) -> Result<String, String> {
|
||||||
|
// Verbindung trennen
|
||||||
|
{
|
||||||
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||||
|
let mut state = state.lock().unwrap();
|
||||||
|
state.connection = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daemon-Prozess killen
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
if let Some(pid) = is_daemon_alive() {
|
||||||
|
unsafe { libc::kill(pid as i32, libc::SIGTERM); }
|
||||||
|
println!("🔌 Bridge-Daemon (PID: {}) wird gestoppt", pid);
|
||||||
|
return Ok(format!("Daemon PID {} gestoppt", pid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok("Kein aktiver Daemon gefunden".to_string())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ pub fn run() {
|
||||||
claude::set_agent_mode,
|
claude::set_agent_mode,
|
||||||
claude::get_agent_mode,
|
claude::get_agent_mode,
|
||||||
claude::init_sticky_context,
|
claude::init_sticky_context,
|
||||||
|
claude::get_bridge_status,
|
||||||
|
claude::stop_bridge_daemon,
|
||||||
// Gedächtnis-System
|
// Gedächtnis-System
|
||||||
memory::load_memory,
|
memory::load_memory,
|
||||||
memory::get_sticky_memory_entries,
|
memory::get_sticky_memory_entries,
|
||||||
|
|
@ -325,7 +327,9 @@ pub fn run() {
|
||||||
// Lock-Datei bei App-Beendigung aufräumen
|
// Lock-Datei bei App-Beendigung aufräumen
|
||||||
if let tauri::RunEvent::Exit = event {
|
if let tauri::RunEvent::Exit = event {
|
||||||
update::remove_lock_file();
|
update::remove_lock_file();
|
||||||
println!("🔒 Lock-Datei aufgeräumt, App beendet.");
|
// Bridge-Daemon am Leben lassen! Er überlebt App-Neustarts.
|
||||||
|
// Nur die UDS-Verbindung wird geschlossen (Drop).
|
||||||
|
println!("🔒 Lock-Datei aufgeräumt, App beendet. Bridge-Daemon läuft weiter.");
|
||||||
}
|
}
|
||||||
// Bei Fenster-Schließen: Lock entfernen falls App komplett beendet wird
|
// Bei Fenster-Schließen: Lock entfernen falls App komplett beendet wird
|
||||||
if let tauri::RunEvent::WindowEvent {
|
if let tauri::RunEvent::WindowEvent {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue