mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-13 12:35:54 +00:00
feat: Add Rust formatter and linter configuration, fix warnings
- Add rustfmt.toml with comprehensive formatting rules - Add clippy.toml with linting configuration - Add .cargo/config.toml with clippy warning flags - Fix unused imports and variables warnings - Connect unused functions in terminal.rs, server.rs, tray_menu.rs, and session_monitor.rs - Add proper error handling and cleanup on app quit - Apply formatting to all Rust code files - Ensure feature parity with Mac app implementation This improves code quality, consistency, and maintainability of the Tauri app.
This commit is contained in:
parent
0d9232574c
commit
5d3576a205
31 changed files with 2870 additions and 2246 deletions
|
|
@ -1,3 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// API test method
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
|
@ -36,11 +36,22 @@ impl HttpMethod {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AssertionType {
|
||||
StatusCode(u16),
|
||||
StatusRange { min: u16, max: u16 },
|
||||
ResponseTime { max_ms: u64 },
|
||||
StatusRange {
|
||||
min: u16,
|
||||
max: u16,
|
||||
},
|
||||
ResponseTime {
|
||||
max_ms: u64,
|
||||
},
|
||||
HeaderExists(String),
|
||||
HeaderEquals { key: String, value: String },
|
||||
JsonPath { path: String, expected: serde_json::Value },
|
||||
HeaderEquals {
|
||||
key: String,
|
||||
value: String,
|
||||
},
|
||||
JsonPath {
|
||||
path: String,
|
||||
expected: serde_json::Value,
|
||||
},
|
||||
BodyContains(String),
|
||||
BodyMatches(String), // Regex
|
||||
ContentType(String),
|
||||
|
|
@ -78,9 +89,16 @@ pub enum APITestBody {
|
|||
/// API test authentication
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum APITestAuth {
|
||||
Basic { username: String, password: String },
|
||||
Basic {
|
||||
username: String,
|
||||
password: String,
|
||||
},
|
||||
Bearer(String),
|
||||
ApiKey { key: String, value: String, in_header: bool },
|
||||
ApiKey {
|
||||
key: String,
|
||||
value: String,
|
||||
in_header: bool,
|
||||
},
|
||||
Custom(HashMap<String, String>),
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +223,10 @@ impl APITestingManager {
|
|||
}
|
||||
|
||||
/// Set the notification manager
|
||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
||||
pub fn set_notification_manager(
|
||||
&mut self,
|
||||
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||
) {
|
||||
self.notification_manager = Some(notification_manager);
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +242,10 @@ impl APITestingManager {
|
|||
|
||||
/// Add test suite
|
||||
pub async fn add_test_suite(&self, suite: APITestSuite) {
|
||||
self.test_suites.write().await.insert(suite.id.clone(), suite);
|
||||
self.test_suites
|
||||
.write()
|
||||
.await
|
||||
.insert(suite.id.clone(), suite);
|
||||
}
|
||||
|
||||
/// Get test suite
|
||||
|
|
@ -235,7 +259,11 @@ impl APITestingManager {
|
|||
}
|
||||
|
||||
/// Run single test
|
||||
pub async fn run_test(&self, test: &APITest, variables: &HashMap<String, String>) -> APITestResult {
|
||||
pub async fn run_test(
|
||||
&self,
|
||||
test: &APITest,
|
||||
variables: &HashMap<String, String>,
|
||||
) -> APITestResult {
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut result = APITestResult {
|
||||
test_id: test.id.clone(),
|
||||
|
|
@ -274,9 +302,11 @@ impl APITestingManager {
|
|||
result.retries_used = retry;
|
||||
|
||||
// Run assertions
|
||||
result.assertion_results = self.run_assertions(&test.assertions, status, &result.response_headers, &body).await;
|
||||
result.assertion_results = self
|
||||
.run_assertions(&test.assertions, status, &result.response_headers, &body)
|
||||
.await;
|
||||
result.success = result.assertion_results.iter().all(|a| a.passed);
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -298,7 +328,7 @@ impl APITestingManager {
|
|||
let suite = self.get_test_suite(suite_id).await?;
|
||||
let run_id = uuid::Uuid::new_v4().to_string();
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
|
||||
// Merge variables
|
||||
let mut variables = self.shared_variables.read().await.clone();
|
||||
variables.extend(suite.variables.clone());
|
||||
|
|
@ -323,10 +353,10 @@ impl APITestingManager {
|
|||
let test = test.clone();
|
||||
let vars = variables.clone();
|
||||
let manager = self.clone_for_parallel();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
manager.run_test(&test, &vars).await
|
||||
}));
|
||||
|
||||
tasks.push(tokio::spawn(
|
||||
async move { manager.run_test(&test, &vars).await },
|
||||
));
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
|
|
@ -371,11 +401,10 @@ impl APITestingManager {
|
|||
|
||||
// Send notification
|
||||
if let Some(notification_manager) = &self.notification_manager {
|
||||
let message = format!(
|
||||
"Test suite completed: {} passed, {} failed",
|
||||
passed, failed
|
||||
);
|
||||
let _ = notification_manager.notify_success("API Tests", &message).await;
|
||||
let message = format!("Test suite completed: {} passed, {} failed", passed, failed);
|
||||
let _ = notification_manager
|
||||
.notify_success("API Tests", &message)
|
||||
.await;
|
||||
}
|
||||
|
||||
Some(history_entry)
|
||||
|
|
@ -403,9 +432,11 @@ impl APITestingManager {
|
|||
|
||||
/// Export test suite
|
||||
pub async fn export_test_suite(&self, suite_id: &str) -> Result<String, String> {
|
||||
let suite = self.get_test_suite(suite_id).await
|
||||
let suite = self
|
||||
.get_test_suite(suite_id)
|
||||
.await
|
||||
.ok_or_else(|| "Test suite not found".to_string())?;
|
||||
|
||||
|
||||
serde_json::to_string_pretty(&suite)
|
||||
.map_err(|e| format!("Failed to serialize test suite: {}", e))
|
||||
}
|
||||
|
|
@ -469,7 +500,7 @@ impl APITestingManager {
|
|||
// Execute request
|
||||
let response = request.send().await.map_err(|e| e.to_string())?;
|
||||
let status = response.status().as_u16();
|
||||
|
||||
|
||||
let mut headers = HashMap::new();
|
||||
for (key, value) in response.headers() {
|
||||
if let Ok(value_str) = value.to_str() {
|
||||
|
|
@ -493,42 +524,39 @@ impl APITestingManager {
|
|||
|
||||
for assertion in assertions {
|
||||
let result = match assertion {
|
||||
AssertionType::StatusCode(expected) => {
|
||||
AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: status == *expected,
|
||||
actual_value: Some(status.to_string()),
|
||||
error_message: if status != *expected {
|
||||
Some(format!("Expected status {}, got {}", expected, status))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
AssertionType::StatusRange { min, max } => {
|
||||
AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: status >= *min && status <= *max,
|
||||
actual_value: Some(status.to_string()),
|
||||
error_message: if status < *min || status > *max {
|
||||
Some(format!("Expected status between {} and {}, got {}", min, max, status))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
AssertionType::HeaderExists(key) => {
|
||||
AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: headers.contains_key(key),
|
||||
actual_value: None,
|
||||
error_message: if !headers.contains_key(key) {
|
||||
Some(format!("Header '{}' not found", key))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
AssertionType::StatusCode(expected) => AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: status == *expected,
|
||||
actual_value: Some(status.to_string()),
|
||||
error_message: if status != *expected {
|
||||
Some(format!("Expected status {}, got {}", expected, status))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
AssertionType::StatusRange { min, max } => AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: status >= *min && status <= *max,
|
||||
actual_value: Some(status.to_string()),
|
||||
error_message: if status < *min || status > *max {
|
||||
Some(format!(
|
||||
"Expected status between {} and {}, got {}",
|
||||
min, max, status
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
AssertionType::HeaderExists(key) => AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: headers.contains_key(key),
|
||||
actual_value: None,
|
||||
error_message: if !headers.contains_key(key) {
|
||||
Some(format!("Header '{}' not found", key))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
AssertionType::HeaderEquals { key, value } => {
|
||||
let actual = headers.get(key);
|
||||
AssertionResult {
|
||||
|
|
@ -536,25 +564,29 @@ impl APITestingManager {
|
|||
passed: actual == Some(value),
|
||||
actual_value: actual.cloned(),
|
||||
error_message: if actual != Some(value) {
|
||||
Some(format!("Header '{}' expected '{}', got '{:?}'", key, value, actual))
|
||||
Some(format!(
|
||||
"Header '{}' expected '{}', got '{:?}'",
|
||||
key, value, actual
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
AssertionType::BodyContains(text) => {
|
||||
AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: body.contains(text),
|
||||
actual_value: None,
|
||||
error_message: if !body.contains(text) {
|
||||
Some(format!("Body does not contain '{}'", text))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
AssertionType::JsonPath { path: _, expected: _ } => {
|
||||
AssertionType::BodyContains(text) => AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: body.contains(text),
|
||||
actual_value: None,
|
||||
error_message: if !body.contains(text) {
|
||||
Some(format!("Body does not contain '{}'", text))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
AssertionType::JsonPath {
|
||||
path: _,
|
||||
expected: _,
|
||||
} => {
|
||||
// TODO: Implement JSON path assertion
|
||||
AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
|
|
@ -563,14 +595,12 @@ impl APITestingManager {
|
|||
error_message: Some("JSON path assertions not yet implemented".to_string()),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: false,
|
||||
actual_value: None,
|
||||
error_message: Some("Assertion type not implemented".to_string()),
|
||||
}
|
||||
}
|
||||
_ => AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed: false,
|
||||
actual_value: None,
|
||||
error_message: Some("Assertion type not implemented".to_string()),
|
||||
},
|
||||
};
|
||||
results.push(result);
|
||||
}
|
||||
|
|
@ -602,7 +632,11 @@ impl APITestingManager {
|
|||
let token = self.replace_variables(token, variables);
|
||||
request.bearer_auth(token)
|
||||
}
|
||||
APITestAuth::ApiKey { key, value, in_header } => {
|
||||
APITestAuth::ApiKey {
|
||||
key,
|
||||
value,
|
||||
in_header,
|
||||
} => {
|
||||
let key = self.replace_variables(key, variables);
|
||||
let value = self.replace_variables(value, variables);
|
||||
if *in_header {
|
||||
|
|
@ -645,4 +679,4 @@ pub struct APITestStatistics {
|
|||
pub average_duration_ms: f64,
|
||||
pub most_failed_tests: Vec<(String, usize)>,
|
||||
pub slowest_tests: Vec<(String, u64)>,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
use tauri::AppHandle;
|
||||
use std::path::PathBuf;
|
||||
use tauri::AppHandle;
|
||||
|
||||
/// Check if the app should be moved to Applications folder
|
||||
/// This is a macOS-specific feature
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn check_and_prompt_move(app_handle: AppHandle) -> Result<(), String> {
|
||||
|
||||
pub async fn check_and_prompt_move(_app_handle: AppHandle) -> Result<(), String> {
|
||||
// Get current app bundle path
|
||||
let bundle_path = get_app_bundle_path()?;
|
||||
|
||||
|
||||
// Check if already in Applications folder
|
||||
if is_in_applications_folder(&bundle_path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
// Check if we've already asked this question
|
||||
let settings = crate::settings::Settings::load().unwrap_or_default();
|
||||
if let Some(asked) = settings.general.show_welcome_on_startup {
|
||||
|
|
@ -22,23 +21,24 @@ pub async fn check_and_prompt_move(app_handle: AppHandle) -> Result<(), String>
|
|||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// For now, just log and return
|
||||
// TODO: Implement dialog using tauri-plugin-dialog
|
||||
tracing::info!("App should be moved to Applications folder");
|
||||
|
||||
if false { // Temporarily disabled until dialog is implemented
|
||||
|
||||
if false {
|
||||
// Temporarily disabled until dialog is implemented
|
||||
move_to_applications_folder(bundle_path)?;
|
||||
|
||||
|
||||
// Restart the app from the new location
|
||||
restart_from_applications()?;
|
||||
}
|
||||
|
||||
|
||||
// Update settings to not ask again
|
||||
let mut settings = crate::settings::Settings::load().unwrap_or_default();
|
||||
settings.general.show_welcome_on_startup = Some(false);
|
||||
settings.save().ok();
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -51,27 +51,28 @@ pub async fn check_and_prompt_move(_app_handle: AppHandle) -> Result<(), String>
|
|||
#[cfg(target_os = "macos")]
|
||||
fn get_app_bundle_path() -> Result<PathBuf, String> {
|
||||
use std::env;
|
||||
|
||||
|
||||
// Get the executable path
|
||||
let exe_path = env::current_exe()
|
||||
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||
|
||||
let exe_path =
|
||||
env::current_exe().map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||
|
||||
// Navigate up to the .app bundle
|
||||
// Typical structure: /path/to/VibeTunnel.app/Contents/MacOS/VibeTunnel
|
||||
let mut bundle_path = exe_path;
|
||||
|
||||
|
||||
// Go up three levels to reach the .app bundle
|
||||
for _ in 0..3 {
|
||||
bundle_path = bundle_path.parent()
|
||||
bundle_path = bundle_path
|
||||
.parent()
|
||||
.ok_or("Failed to find app bundle")?
|
||||
.to_path_buf();
|
||||
}
|
||||
|
||||
|
||||
// Verify this is an .app bundle
|
||||
if !bundle_path.to_string_lossy().ends_with(".app") {
|
||||
return Err("Not running from an app bundle".to_string());
|
||||
}
|
||||
|
||||
|
||||
Ok(bundle_path)
|
||||
}
|
||||
|
||||
|
|
@ -83,25 +84,26 @@ fn is_in_applications_folder(bundle_path: &PathBuf) -> bool {
|
|||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn move_to_applications_folder(bundle_path: PathBuf) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
use std::fs;
|
||||
|
||||
let app_name = bundle_path.file_name()
|
||||
use std::process::Command;
|
||||
|
||||
let app_name = bundle_path
|
||||
.file_name()
|
||||
.ok_or("Failed to get app name")?
|
||||
.to_string_lossy();
|
||||
|
||||
|
||||
let dest_path = PathBuf::from("/Applications").join(app_name.as_ref());
|
||||
|
||||
|
||||
// Check if destination already exists
|
||||
if dest_path.exists() {
|
||||
// For now, just remove the existing app
|
||||
// TODO: Implement dialog using tauri-plugin-dialog
|
||||
|
||||
|
||||
// Remove existing app
|
||||
fs::remove_dir_all(&dest_path)
|
||||
.map_err(|e| format!("Failed to remove existing app: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
// Use AppleScript to move the app with proper permissions
|
||||
let script = format!(
|
||||
r#"tell application "Finder"
|
||||
|
|
@ -109,32 +111,32 @@ fn move_to_applications_folder(bundle_path: PathBuf) -> Result<(), String> {
|
|||
end tell"#,
|
||||
bundle_path.to_string_lossy()
|
||||
);
|
||||
|
||||
|
||||
let output = Command::new("osascript")
|
||||
.arg("-e")
|
||||
.arg(script)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute move command: {}", e))?;
|
||||
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("Failed to move app: {}", error));
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn restart_from_applications() -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
// Launch the app from the Applications folder
|
||||
let _output = Command::new("open")
|
||||
.arg("-n")
|
||||
.arg("/Applications/VibeTunnel.app")
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to restart app: {}", e))?;
|
||||
|
||||
|
||||
// Exit the current instance
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
|
@ -151,10 +153,10 @@ pub async fn is_in_applications_folder_command() -> Result<bool, String> {
|
|||
let bundle_path = get_app_bundle_path()?;
|
||||
Ok(is_in_applications_folder(&bundle_path))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
// Always return true on non-macOS platforms
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use axum::{
|
|||
response::Response,
|
||||
Json,
|
||||
};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -108,4 +108,4 @@ pub async fn login(
|
|||
message: "No password configured".to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
/// Authentication token type
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
|
|
@ -104,7 +104,7 @@ impl Default for AuthCacheConfig {
|
|||
Self {
|
||||
enabled: true,
|
||||
max_entries: 1000,
|
||||
default_ttl_seconds: 3600, // 1 hour
|
||||
default_ttl_seconds: 3600, // 1 hour
|
||||
refresh_threshold_seconds: 300, // 5 minutes
|
||||
persist_to_disk: false,
|
||||
encryption_enabled: true,
|
||||
|
|
@ -126,7 +126,11 @@ pub struct AuthCacheStats {
|
|||
}
|
||||
|
||||
/// Token refresh callback
|
||||
pub type TokenRefreshCallback = Arc<dyn Fn(CachedToken) -> futures::future::BoxFuture<'static, Result<CachedToken, String>> + Send + Sync>;
|
||||
pub type TokenRefreshCallback = Arc<
|
||||
dyn Fn(CachedToken) -> futures::future::BoxFuture<'static, Result<CachedToken, String>>
|
||||
+ Send
|
||||
+ Sync,
|
||||
>;
|
||||
|
||||
/// Authentication cache manager
|
||||
pub struct AuthCacheManager {
|
||||
|
|
@ -168,7 +172,10 @@ impl AuthCacheManager {
|
|||
}
|
||||
|
||||
/// Set the notification manager
|
||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
||||
pub fn set_notification_manager(
|
||||
&mut self,
|
||||
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||
) {
|
||||
self.notification_manager = Some(notification_manager);
|
||||
}
|
||||
|
||||
|
|
@ -244,12 +251,13 @@ impl AuthCacheManager {
|
|||
// Check if needs refresh
|
||||
if token.needs_refresh(config.refresh_threshold_seconds) {
|
||||
// Trigger refresh in background
|
||||
if let Some(refresh_callback) = self.refresh_callbacks.read().await.get(key) {
|
||||
if let Some(refresh_callback) = self.refresh_callbacks.read().await.get(key)
|
||||
{
|
||||
let token_clone = token.clone();
|
||||
let callback = refresh_callback.clone();
|
||||
let key_clone = key.to_string();
|
||||
let manager = self.clone_for_refresh();
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Ok(refreshed_token) = callback(token_clone).await {
|
||||
let _ = manager.store_token(&key_clone, refreshed_token).await;
|
||||
|
|
@ -269,7 +277,11 @@ impl AuthCacheManager {
|
|||
}
|
||||
|
||||
/// Store credential in cache
|
||||
pub async fn store_credential(&self, key: &str, credential: AuthCredential) -> Result<(), String> {
|
||||
pub async fn store_credential(
|
||||
&self,
|
||||
key: &str,
|
||||
credential: AuthCredential,
|
||||
) -> Result<(), String> {
|
||||
let config = self.config.read().await;
|
||||
if !config.enabled {
|
||||
return Ok(());
|
||||
|
|
@ -303,7 +315,7 @@ impl AuthCacheManager {
|
|||
}
|
||||
|
||||
let mut cache = self.cache.write().await;
|
||||
|
||||
|
||||
if let Some(entry) = cache.get_mut(key) {
|
||||
entry.last_accessed = Utc::now();
|
||||
entry.access_count += 1;
|
||||
|
|
@ -315,7 +327,10 @@ impl AuthCacheManager {
|
|||
|
||||
/// Register token refresh callback
|
||||
pub async fn register_refresh_callback(&self, key: &str, callback: TokenRefreshCallback) {
|
||||
self.refresh_callbacks.write().await.insert(key.to_string(), callback);
|
||||
self.refresh_callbacks
|
||||
.write()
|
||||
.await
|
||||
.insert(key.to_string(), callback);
|
||||
}
|
||||
|
||||
/// Clear specific cache entry
|
||||
|
|
@ -330,7 +345,7 @@ impl AuthCacheManager {
|
|||
pub async fn clear_all(&self) {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.clear();
|
||||
|
||||
|
||||
let mut stats = self.stats.write().await;
|
||||
stats.total_entries = 0;
|
||||
stats.total_tokens = 0;
|
||||
|
|
@ -344,7 +359,9 @@ impl AuthCacheManager {
|
|||
|
||||
/// List all cache entries
|
||||
pub async fn list_entries(&self) -> Vec<(String, DateTime<Utc>, u64)> {
|
||||
self.cache.read().await
|
||||
self.cache
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.map(|entry| (entry.key.clone(), entry.last_accessed, entry.access_count))
|
||||
.collect()
|
||||
|
|
@ -354,7 +371,7 @@ impl AuthCacheManager {
|
|||
pub async fn export_cache(&self) -> Result<String, String> {
|
||||
let cache = self.cache.read().await;
|
||||
let entries: Vec<_> = cache.values().cloned().collect();
|
||||
|
||||
|
||||
serde_json::to_string_pretty(&entries)
|
||||
.map_err(|e| format!("Failed to serialize cache: {}", e))
|
||||
}
|
||||
|
|
@ -363,19 +380,17 @@ impl AuthCacheManager {
|
|||
pub async fn import_cache(&self, json_data: &str) -> Result<(), String> {
|
||||
let entries: Vec<AuthCacheEntry> = serde_json::from_str(json_data)
|
||||
.map_err(|e| format!("Failed to deserialize cache: {}", e))?;
|
||||
|
||||
|
||||
let mut cache = self.cache.write().await;
|
||||
let mut stats = self.stats.write().await;
|
||||
|
||||
|
||||
for entry in entries {
|
||||
cache.insert(entry.key.clone(), entry);
|
||||
}
|
||||
|
||||
|
||||
stats.total_entries = cache.len();
|
||||
stats.total_tokens = cache.values()
|
||||
.map(|e| e.tokens.len())
|
||||
.sum();
|
||||
|
||||
stats.total_tokens = cache.values().map(|e| e.tokens.len()).sum();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -388,14 +403,20 @@ impl AuthCacheManager {
|
|||
|
||||
// Helper methods
|
||||
fn token_matches_scope(&self, token: &CachedToken, scope: &AuthScope) -> bool {
|
||||
token.scope.service == scope.service &&
|
||||
token.scope.resource == scope.resource &&
|
||||
scope.permissions.iter().all(|p| token.scope.permissions.contains(p))
|
||||
token.scope.service == scope.service
|
||||
&& token.scope.resource == scope.resource
|
||||
&& scope
|
||||
.permissions
|
||||
.iter()
|
||||
.all(|p| token.scope.permissions.contains(p))
|
||||
}
|
||||
|
||||
fn evict_oldest_entry(&self, cache: &mut HashMap<String, AuthCacheEntry>, stats: &mut AuthCacheStats) {
|
||||
if let Some((key, _)) = cache.iter()
|
||||
.min_by_key(|(_, entry)| entry.last_accessed) {
|
||||
fn evict_oldest_entry(
|
||||
&self,
|
||||
cache: &mut HashMap<String, AuthCacheEntry>,
|
||||
stats: &mut AuthCacheStats,
|
||||
) {
|
||||
if let Some((key, _)) = cache.iter().min_by_key(|(_, entry)| entry.last_accessed) {
|
||||
let key = key.clone();
|
||||
cache.remove(&key);
|
||||
stats.eviction_count += 1;
|
||||
|
|
@ -410,7 +431,7 @@ impl AuthCacheManager {
|
|||
|
||||
loop {
|
||||
tokio::time::sleep(cleanup_interval.to_std().unwrap()).await;
|
||||
|
||||
|
||||
let config = self.config.read().await;
|
||||
if !config.enabled {
|
||||
continue;
|
||||
|
|
@ -429,9 +450,7 @@ impl AuthCacheManager {
|
|||
}
|
||||
|
||||
stats.expired_tokens += total_expired;
|
||||
stats.total_tokens = cache.values()
|
||||
.map(|e| e.tokens.len())
|
||||
.sum();
|
||||
stats.total_tokens = cache.values().map(|e| e.tokens.len()).sum();
|
||||
|
||||
// Remove empty entries
|
||||
cache.retain(|_, entry| !entry.tokens.is_empty() || entry.credential.is_some());
|
||||
|
|
@ -480,4 +499,4 @@ pub struct AuthCacheError {
|
|||
pub code: String,
|
||||
pub message: String,
|
||||
pub details: Option<HashMap<String, String>>,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use crate::state::AppState;
|
||||
use auto_launch::AutoLaunchBuilder;
|
||||
use tauri::State;
|
||||
use crate::state::AppState;
|
||||
|
||||
fn get_app_path() -> String {
|
||||
let exe_path = std::env::current_exe().unwrap();
|
||||
|
||||
|
||||
// On macOS, we need to use the .app bundle path, not the executable inside it
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
|
@ -20,7 +20,7 @@ fn get_app_path() -> String {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// For other platforms or if we couldn't find the .app bundle, use the executable path
|
||||
exe_path.to_string_lossy().to_string()
|
||||
}
|
||||
|
|
@ -32,10 +32,10 @@ pub fn enable_auto_launch() -> Result<(), String> {
|
|||
.set_args(&["--auto-launch"])
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build auto-launch: {}", e))?;
|
||||
|
||||
|
||||
auto.enable()
|
||||
.map_err(|e| format!("Failed to enable auto-launch: {}", e))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -45,10 +45,10 @@ pub fn disable_auto_launch() -> Result<(), String> {
|
|||
.set_app_path(&get_app_path())
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build auto-launch: {}", e))?;
|
||||
|
||||
|
||||
auto.disable()
|
||||
.map_err(|e| format!("Failed to disable auto-launch: {}", e))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -58,16 +58,13 @@ pub fn is_auto_launch_enabled() -> Result<bool, String> {
|
|||
.set_app_path(&get_app_path())
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build auto-launch: {}", e))?;
|
||||
|
||||
|
||||
auto.is_enabled()
|
||||
.map_err(|e| format!("Failed to check auto-launch status: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_auto_launch(
|
||||
enabled: bool,
|
||||
_state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
pub async fn set_auto_launch(enabled: bool, _state: State<'_, AppState>) -> Result<(), String> {
|
||||
if enabled {
|
||||
enable_auto_launch()
|
||||
} else {
|
||||
|
|
@ -76,8 +73,6 @@ pub async fn set_auto_launch(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_launch(
|
||||
_state: State<'_, AppState>,
|
||||
) -> Result<bool, String> {
|
||||
pub async fn get_auto_launch(_state: State<'_, AppState>) -> Result<bool, String> {
|
||||
is_auto_launch_enabled()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::Arc;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Backend type enumeration
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
|
|
@ -26,7 +26,7 @@ impl BackendType {
|
|||
BackendType::Custom => "custom",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"rust" => BackendType::Rust,
|
||||
|
|
@ -141,7 +141,7 @@ impl BackendManager {
|
|||
active_backend: Arc::new(RwLock::new(Some(BackendType::Rust))),
|
||||
notification_manager: None,
|
||||
};
|
||||
|
||||
|
||||
// Initialize default backend configurations
|
||||
tokio::spawn({
|
||||
let configs = manager.configs.clone();
|
||||
|
|
@ -150,118 +150,130 @@ impl BackendManager {
|
|||
*configs.write().await = default_configs;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
/// Set the notification manager
|
||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
||||
pub fn set_notification_manager(
|
||||
&mut self,
|
||||
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||
) {
|
||||
self.notification_manager = Some(notification_manager);
|
||||
}
|
||||
|
||||
/// Initialize default backend configurations
|
||||
fn initialize_default_configs() -> HashMap<BackendType, BackendConfig> {
|
||||
let mut configs = HashMap::new();
|
||||
|
||||
|
||||
// Rust backend (built-in)
|
||||
configs.insert(BackendType::Rust, BackendConfig {
|
||||
backend_type: BackendType::Rust,
|
||||
name: "Rust (Built-in)".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
executable_path: None,
|
||||
working_directory: None,
|
||||
environment_variables: HashMap::new(),
|
||||
arguments: vec![],
|
||||
port: Some(4020),
|
||||
features: BackendFeatures {
|
||||
terminal_sessions: true,
|
||||
file_browser: true,
|
||||
port_forwarding: true,
|
||||
authentication: true,
|
||||
websocket_support: true,
|
||||
rest_api: true,
|
||||
graphql_api: false,
|
||||
metrics: true,
|
||||
configs.insert(
|
||||
BackendType::Rust,
|
||||
BackendConfig {
|
||||
backend_type: BackendType::Rust,
|
||||
name: "Rust (Built-in)".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
executable_path: None,
|
||||
working_directory: None,
|
||||
environment_variables: HashMap::new(),
|
||||
arguments: vec![],
|
||||
port: Some(4020),
|
||||
features: BackendFeatures {
|
||||
terminal_sessions: true,
|
||||
file_browser: true,
|
||||
port_forwarding: true,
|
||||
authentication: true,
|
||||
websocket_support: true,
|
||||
rest_api: true,
|
||||
graphql_api: false,
|
||||
metrics: true,
|
||||
},
|
||||
requirements: BackendRequirements {
|
||||
runtime: None,
|
||||
runtime_version: None,
|
||||
dependencies: vec![],
|
||||
system_packages: vec![],
|
||||
min_memory_mb: Some(64),
|
||||
min_disk_space_mb: Some(10),
|
||||
},
|
||||
},
|
||||
requirements: BackendRequirements {
|
||||
runtime: None,
|
||||
runtime_version: None,
|
||||
dependencies: vec![],
|
||||
system_packages: vec![],
|
||||
min_memory_mb: Some(64),
|
||||
min_disk_space_mb: Some(10),
|
||||
},
|
||||
});
|
||||
|
||||
);
|
||||
|
||||
// Node.js backend
|
||||
configs.insert(BackendType::NodeJS, BackendConfig {
|
||||
backend_type: BackendType::NodeJS,
|
||||
name: "Node.js Server".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
executable_path: Some(PathBuf::from("node")),
|
||||
working_directory: None,
|
||||
environment_variables: HashMap::new(),
|
||||
arguments: vec!["server.js".to_string()],
|
||||
port: Some(4021),
|
||||
features: BackendFeatures {
|
||||
terminal_sessions: true,
|
||||
file_browser: true,
|
||||
port_forwarding: false,
|
||||
authentication: true,
|
||||
websocket_support: true,
|
||||
rest_api: true,
|
||||
graphql_api: true,
|
||||
metrics: false,
|
||||
configs.insert(
|
||||
BackendType::NodeJS,
|
||||
BackendConfig {
|
||||
backend_type: BackendType::NodeJS,
|
||||
name: "Node.js Server".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
executable_path: Some(PathBuf::from("node")),
|
||||
working_directory: None,
|
||||
environment_variables: HashMap::new(),
|
||||
arguments: vec!["server.js".to_string()],
|
||||
port: Some(4021),
|
||||
features: BackendFeatures {
|
||||
terminal_sessions: true,
|
||||
file_browser: true,
|
||||
port_forwarding: false,
|
||||
authentication: true,
|
||||
websocket_support: true,
|
||||
rest_api: true,
|
||||
graphql_api: true,
|
||||
metrics: false,
|
||||
},
|
||||
requirements: BackendRequirements {
|
||||
runtime: Some("node".to_string()),
|
||||
runtime_version: Some(">=16.0.0".to_string()),
|
||||
dependencies: vec![
|
||||
"express".to_string(),
|
||||
"socket.io".to_string(),
|
||||
"node-pty".to_string(),
|
||||
],
|
||||
system_packages: vec![],
|
||||
min_memory_mb: Some(128),
|
||||
min_disk_space_mb: Some(50),
|
||||
},
|
||||
},
|
||||
requirements: BackendRequirements {
|
||||
runtime: Some("node".to_string()),
|
||||
runtime_version: Some(">=16.0.0".to_string()),
|
||||
dependencies: vec![
|
||||
"express".to_string(),
|
||||
"socket.io".to_string(),
|
||||
"node-pty".to_string(),
|
||||
],
|
||||
system_packages: vec![],
|
||||
min_memory_mb: Some(128),
|
||||
min_disk_space_mb: Some(50),
|
||||
},
|
||||
});
|
||||
|
||||
);
|
||||
|
||||
// Python backend
|
||||
configs.insert(BackendType::Python, BackendConfig {
|
||||
backend_type: BackendType::Python,
|
||||
name: "Python Server".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
executable_path: Some(PathBuf::from("python3")),
|
||||
working_directory: None,
|
||||
environment_variables: HashMap::new(),
|
||||
arguments: vec!["-m".to_string(), "vibetunnel_server".to_string()],
|
||||
port: Some(4022),
|
||||
features: BackendFeatures {
|
||||
terminal_sessions: true,
|
||||
file_browser: true,
|
||||
port_forwarding: false,
|
||||
authentication: true,
|
||||
websocket_support: true,
|
||||
rest_api: true,
|
||||
graphql_api: false,
|
||||
metrics: true,
|
||||
configs.insert(
|
||||
BackendType::Python,
|
||||
BackendConfig {
|
||||
backend_type: BackendType::Python,
|
||||
name: "Python Server".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
executable_path: Some(PathBuf::from("python3")),
|
||||
working_directory: None,
|
||||
environment_variables: HashMap::new(),
|
||||
arguments: vec!["-m".to_string(), "vibetunnel_server".to_string()],
|
||||
port: Some(4022),
|
||||
features: BackendFeatures {
|
||||
terminal_sessions: true,
|
||||
file_browser: true,
|
||||
port_forwarding: false,
|
||||
authentication: true,
|
||||
websocket_support: true,
|
||||
rest_api: true,
|
||||
graphql_api: false,
|
||||
metrics: true,
|
||||
},
|
||||
requirements: BackendRequirements {
|
||||
runtime: Some("python3".to_string()),
|
||||
runtime_version: Some(">=3.8".to_string()),
|
||||
dependencies: vec![
|
||||
"fastapi".to_string(),
|
||||
"uvicorn".to_string(),
|
||||
"websockets".to_string(),
|
||||
"ptyprocess".to_string(),
|
||||
],
|
||||
system_packages: vec![],
|
||||
min_memory_mb: Some(96),
|
||||
min_disk_space_mb: Some(30),
|
||||
},
|
||||
},
|
||||
requirements: BackendRequirements {
|
||||
runtime: Some("python3".to_string()),
|
||||
runtime_version: Some(">=3.8".to_string()),
|
||||
dependencies: vec![
|
||||
"fastapi".to_string(),
|
||||
"uvicorn".to_string(),
|
||||
"websockets".to_string(),
|
||||
"ptyprocess".to_string(),
|
||||
],
|
||||
system_packages: vec![],
|
||||
min_memory_mb: Some(96),
|
||||
min_disk_space_mb: Some(30),
|
||||
},
|
||||
});
|
||||
|
||||
);
|
||||
|
||||
configs
|
||||
}
|
||||
|
||||
|
|
@ -303,14 +315,16 @@ impl BackendManager {
|
|||
if !self.is_backend_installed(backend_type).await {
|
||||
return Err(format!("{:?} backend is not installed", backend_type));
|
||||
}
|
||||
|
||||
|
||||
// Get backend configuration
|
||||
let config = self.get_backend_config(backend_type).await
|
||||
let config = self
|
||||
.get_backend_config(backend_type)
|
||||
.await
|
||||
.ok_or_else(|| "Backend configuration not found".to_string())?;
|
||||
|
||||
|
||||
// Generate instance ID
|
||||
let instance_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
|
||||
// Create backend instance
|
||||
let instance = BackendInstance {
|
||||
id: instance_id.clone(),
|
||||
|
|
@ -330,15 +344,19 @@ impl BackendManager {
|
|||
active_connections: 0,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Store instance
|
||||
self.instances.write().await.insert(instance_id.clone(), instance);
|
||||
|
||||
self.instances
|
||||
.write()
|
||||
.await
|
||||
.insert(instance_id.clone(), instance);
|
||||
|
||||
// Start backend process
|
||||
match backend_type {
|
||||
BackendType::Rust => {
|
||||
// Rust backend is handled internally
|
||||
self.update_instance_status(&instance_id, BackendStatus::Running).await;
|
||||
self.update_instance_status(&instance_id, BackendStatus::Running)
|
||||
.await;
|
||||
*self.active_backend.write().await = Some(BackendType::Rust);
|
||||
Ok(instance_id)
|
||||
}
|
||||
|
|
@ -351,15 +369,19 @@ impl BackendManager {
|
|||
|
||||
/// Stop backend
|
||||
pub async fn stop_backend(&self, instance_id: &str) -> Result<(), String> {
|
||||
let instance = self.instances.read().await
|
||||
let instance = self
|
||||
.instances
|
||||
.read()
|
||||
.await
|
||||
.get(instance_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| "Backend instance not found".to_string())?;
|
||||
|
||||
|
||||
match instance.backend_type {
|
||||
BackendType::Rust => {
|
||||
// Rust backend is handled internally
|
||||
self.update_instance_status(instance_id, BackendStatus::Stopped).await;
|
||||
self.update_instance_status(instance_id, BackendStatus::Stopped)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
|
|
@ -378,8 +400,12 @@ impl BackendManager {
|
|||
// Find and stop current backend instances
|
||||
let instance_id = {
|
||||
let instances = self.instances.read().await;
|
||||
instances.iter()
|
||||
.find(|(_, instance)| instance.backend_type == current && instance.status == BackendStatus::Running)
|
||||
instances
|
||||
.iter()
|
||||
.find(|(_, instance)| {
|
||||
instance.backend_type == current
|
||||
&& instance.status == BackendStatus::Running
|
||||
})
|
||||
.map(|(id, _)| id.clone())
|
||||
};
|
||||
if let Some(id) = instance_id {
|
||||
|
|
@ -387,21 +413,23 @@ impl BackendManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Start new backend
|
||||
self.start_backend(backend_type).await?;
|
||||
|
||||
|
||||
// Update active backend
|
||||
*self.active_backend.write().await = Some(backend_type);
|
||||
|
||||
|
||||
// Notify about backend switch
|
||||
if let Some(notification_manager) = &self.notification_manager {
|
||||
let _ = notification_manager.notify_success(
|
||||
"Backend Switched",
|
||||
&format!("Switched to {:?} backend", backend_type)
|
||||
).await;
|
||||
let _ = notification_manager
|
||||
.notify_success(
|
||||
"Backend Switched",
|
||||
&format!("Switched to {:?} backend", backend_type),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -417,27 +445,30 @@ impl BackendManager {
|
|||
|
||||
/// Get backend health
|
||||
pub async fn check_backend_health(&self, instance_id: &str) -> Result<HealthStatus, String> {
|
||||
let instance = self.instances.read().await
|
||||
let instance = self
|
||||
.instances
|
||||
.read()
|
||||
.await
|
||||
.get(instance_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| "Backend instance not found".to_string())?;
|
||||
|
||||
|
||||
if instance.status != BackendStatus::Running {
|
||||
return Ok(HealthStatus::Unknown);
|
||||
}
|
||||
|
||||
|
||||
// Perform health check based on backend type
|
||||
let health_status = match instance.backend_type {
|
||||
BackendType::Rust => HealthStatus::Healthy, // Always healthy for built-in
|
||||
_ => self.check_external_backend_health(&instance).await?,
|
||||
};
|
||||
|
||||
|
||||
// Update instance health status
|
||||
if let Some(instance) = self.instances.write().await.get_mut(instance_id) {
|
||||
instance.health_status = health_status;
|
||||
instance.last_health_check = Some(Utc::now());
|
||||
}
|
||||
|
||||
|
||||
Ok(health_status)
|
||||
}
|
||||
|
||||
|
|
@ -487,7 +518,11 @@ impl BackendManager {
|
|||
Err("Python backend installation not yet implemented".to_string())
|
||||
}
|
||||
|
||||
async fn start_external_backend(&self, _instance_id: &str, _config: BackendConfig) -> Result<String, String> {
|
||||
async fn start_external_backend(
|
||||
&self,
|
||||
_instance_id: &str,
|
||||
_config: BackendConfig,
|
||||
) -> Result<String, String> {
|
||||
// TODO: Implement external backend startup
|
||||
Err("External backend startup not yet implemented".to_string())
|
||||
}
|
||||
|
|
@ -497,7 +532,10 @@ impl BackendManager {
|
|||
Err("External backend shutdown not yet implemented".to_string())
|
||||
}
|
||||
|
||||
async fn check_external_backend_health(&self, _instance: &BackendInstance) -> Result<HealthStatus, String> {
|
||||
async fn check_external_backend_health(
|
||||
&self,
|
||||
_instance: &BackendInstance,
|
||||
) -> Result<HealthStatus, String> {
|
||||
// TODO: Implement health check for external backends
|
||||
Ok(HealthStatus::Unknown)
|
||||
}
|
||||
|
|
@ -520,4 +558,4 @@ pub struct BackendStats {
|
|||
pub running_instances: usize,
|
||||
pub active_backend: Option<BackendType>,
|
||||
pub health_summary: HashMap<HealthStatus, usize>,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
|
|
@ -5,7 +6,6 @@ use std::io::{BufWriter, Write};
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Asciinema cast v2 format header
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -62,12 +62,7 @@ pub struct CastRecorder {
|
|||
|
||||
impl CastRecorder {
|
||||
/// Create a new cast recorder
|
||||
pub fn new(
|
||||
width: u16,
|
||||
height: u16,
|
||||
title: Option<String>,
|
||||
command: Option<String>,
|
||||
) -> Self {
|
||||
pub fn new(width: u16, height: u16, title: Option<String>, command: Option<String>) -> Self {
|
||||
let now = Utc::now();
|
||||
let header = CastHeader {
|
||||
version: 2,
|
||||
|
|
@ -114,7 +109,8 @@ impl CastRecorder {
|
|||
self.write_event_to_file(&mut writer, event)?;
|
||||
}
|
||||
|
||||
writer.flush()
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|e| format!("Failed to flush writer: {}", e))?;
|
||||
|
||||
self.file_writer = Some(Arc::new(Mutex::new(writer)));
|
||||
|
|
@ -131,7 +127,8 @@ impl CastRecorder {
|
|||
|
||||
if let Some(writer_arc) = self.file_writer.take() {
|
||||
let mut writer = writer_arc.lock().await;
|
||||
writer.flush()
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|e| format!("Failed to flush final data: {}", e))?;
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +150,8 @@ impl CastRecorder {
|
|||
async fn add_event(&self, event_type: EventType, data: &[u8]) -> Result<(), String> {
|
||||
let timestamp = Utc::now()
|
||||
.signed_duration_since(self.start_time)
|
||||
.num_milliseconds() as f64 / 1000.0;
|
||||
.num_milliseconds() as f64
|
||||
/ 1000.0;
|
||||
|
||||
// Convert data to string (handling potential UTF-8 errors)
|
||||
let data_string = String::from_utf8_lossy(data).to_string();
|
||||
|
|
@ -168,7 +166,8 @@ impl CastRecorder {
|
|||
if let Some(writer_arc) = &self.file_writer {
|
||||
let mut writer = writer_arc.lock().await;
|
||||
self.write_event_to_file(&mut writer, &event)?;
|
||||
writer.flush()
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|e| format!("Failed to flush event: {}", e))?;
|
||||
}
|
||||
|
||||
|
|
@ -186,14 +185,10 @@ impl CastRecorder {
|
|||
event: &CastEvent,
|
||||
) -> Result<(), String> {
|
||||
// Format: [timestamp, event_type, data]
|
||||
let event_array = serde_json::json!([
|
||||
event.timestamp,
|
||||
event.event_type.as_str(),
|
||||
event.data
|
||||
]);
|
||||
let event_array =
|
||||
serde_json::json!([event.timestamp, event.event_type.as_str(), event.data]);
|
||||
|
||||
writeln!(writer, "{}", event_array)
|
||||
.map_err(|e| format!("Failed to write event: {}", e))?;
|
||||
writeln!(writer, "{}", event_array).map_err(|e| format!("Failed to write event: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -207,7 +202,7 @@ impl CastRecorder {
|
|||
// Calculate duration
|
||||
let events = self.events.lock().await;
|
||||
let duration = events.last().map(|e| e.timestamp);
|
||||
|
||||
|
||||
// Update header with duration
|
||||
let mut header = self.header.clone();
|
||||
header.duration = duration;
|
||||
|
|
@ -223,7 +218,8 @@ impl CastRecorder {
|
|||
self.write_event_to_file(&mut writer, event)?;
|
||||
}
|
||||
|
||||
writer.flush()
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|e| format!("Failed to flush file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -361,4 +357,4 @@ impl CastManager {
|
|||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use serde::Serialize;
|
||||
use std::fs;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use serde::Serialize;
|
||||
|
||||
const CLI_SCRIPT: &str = r#"#!/bin/bash
|
||||
# VibeTunnel CLI wrapper
|
||||
|
|
@ -21,6 +21,7 @@ fi
|
|||
"$APP_PATH/Contents/MacOS/VibeTunnel" --cli "$@"
|
||||
"#;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const WINDOWS_CLI_SCRIPT: &str = r#"@echo off
|
||||
:: VibeTunnel CLI wrapper for Windows
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ if not exist "%APP_PATH%" (
|
|||
"%APP_PATH%" --cli %*
|
||||
"#;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const LINUX_CLI_SCRIPT: &str = r#"#!/bin/bash
|
||||
# VibeTunnel CLI wrapper for Linux
|
||||
|
||||
|
|
@ -67,12 +69,12 @@ pub fn install_cli_tool() -> Result<CliInstallResult, String> {
|
|||
{
|
||||
install_cli_macos()
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
install_cli_windows()
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
install_cli_linux()
|
||||
|
|
@ -82,18 +84,22 @@ pub fn install_cli_tool() -> Result<CliInstallResult, String> {
|
|||
#[cfg(target_os = "macos")]
|
||||
fn install_cli_macos() -> Result<CliInstallResult, String> {
|
||||
let cli_path = PathBuf::from("/usr/local/bin/vt");
|
||||
|
||||
|
||||
// Check if /usr/local/bin exists, create if not
|
||||
let bin_dir = cli_path.parent().unwrap();
|
||||
if !bin_dir.exists() {
|
||||
fs::create_dir_all(bin_dir)
|
||||
.map_err(|e| format!("Failed to create /usr/local/bin: {}. Try running with sudo.", e))?;
|
||||
fs::create_dir_all(bin_dir).map_err(|e| {
|
||||
format!(
|
||||
"Failed to create /usr/local/bin: {}. Try running with sudo.",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
// Write the CLI script
|
||||
fs::write(&cli_path, CLI_SCRIPT)
|
||||
.map_err(|e| format!("Failed to write CLI script: {}. Try running with sudo.", e))?;
|
||||
|
||||
|
||||
// Make it executable
|
||||
#[cfg(unix)]
|
||||
{
|
||||
|
|
@ -104,7 +110,7 @@ fn install_cli_macos() -> Result<CliInstallResult, String> {
|
|||
fs::set_permissions(&cli_path, perms)
|
||||
.map_err(|e| format!("Failed to set permissions: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
Ok(CliInstallResult {
|
||||
installed: true,
|
||||
path: cli_path.to_string_lossy().to_string(),
|
||||
|
|
@ -114,50 +120,51 @@ fn install_cli_macos() -> Result<CliInstallResult, String> {
|
|||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn install_cli_windows() -> Result<CliInstallResult, String> {
|
||||
let user_path = std::env::var("USERPROFILE")
|
||||
.map_err(|_| "Failed to get user profile path")?;
|
||||
|
||||
let user_path = std::env::var("USERPROFILE").map_err(|_| "Failed to get user profile path")?;
|
||||
|
||||
let cli_dir = PathBuf::from(&user_path).join(".vibetunnel");
|
||||
let cli_path = cli_dir.join("vt.cmd");
|
||||
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if !cli_dir.exists() {
|
||||
fs::create_dir_all(&cli_dir)
|
||||
.map_err(|e| format!("Failed to create CLI directory: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
// Write the CLI script
|
||||
fs::write(&cli_path, WINDOWS_CLI_SCRIPT)
|
||||
.map_err(|e| format!("Failed to write CLI script: {}", e))?;
|
||||
|
||||
|
||||
// Add to PATH if not already there
|
||||
add_to_windows_path(&cli_dir)?;
|
||||
|
||||
|
||||
Ok(CliInstallResult {
|
||||
installed: true,
|
||||
path: cli_path.to_string_lossy().to_string(),
|
||||
message: format!("CLI tool installed successfully at {}. Restart your terminal to use 'vt' command.", cli_path.display()),
|
||||
message: format!(
|
||||
"CLI tool installed successfully at {}. Restart your terminal to use 'vt' command.",
|
||||
cli_path.display()
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn install_cli_linux() -> Result<CliInstallResult, String> {
|
||||
let home_dir = std::env::var("HOME")
|
||||
.map_err(|_| "Failed to get home directory")?;
|
||||
|
||||
let home_dir = std::env::var("HOME").map_err(|_| "Failed to get home directory")?;
|
||||
|
||||
let local_bin = PathBuf::from(&home_dir).join(".local").join("bin");
|
||||
let cli_path = local_bin.join("vt");
|
||||
|
||||
|
||||
// Create ~/.local/bin if it doesn't exist
|
||||
if !local_bin.exists() {
|
||||
fs::create_dir_all(&local_bin)
|
||||
.map_err(|e| format!("Failed to create ~/.local/bin: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
// Write the CLI script
|
||||
fs::write(&cli_path, LINUX_CLI_SCRIPT)
|
||||
.map_err(|e| format!("Failed to write CLI script: {}", e))?;
|
||||
|
||||
|
||||
// Make it executable
|
||||
#[cfg(unix)]
|
||||
{
|
||||
|
|
@ -168,11 +175,14 @@ fn install_cli_linux() -> Result<CliInstallResult, String> {
|
|||
fs::set_permissions(&cli_path, perms)
|
||||
.map_err(|e| format!("Failed to set permissions: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
Ok(CliInstallResult {
|
||||
installed: true,
|
||||
path: cli_path.to_string_lossy().to_string(),
|
||||
message: format!("CLI tool installed successfully at {}. Make sure ~/.local/bin is in your PATH.", cli_path.display()),
|
||||
message: format!(
|
||||
"CLI tool installed successfully at {}. Make sure ~/.local/bin is in your PATH.",
|
||||
cli_path.display()
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -183,26 +193,27 @@ fn add_to_windows_path(dir: &Path) -> Result<(), String> {
|
|||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let env = hkcu.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
|
||||
let env = hkcu
|
||||
.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
|
||||
.map_err(|e| format!("Failed to open registry key: {}", e))?;
|
||||
|
||||
|
||||
let path: String = env.get_value("Path").unwrap_or_default();
|
||||
let dir_str = dir.to_string_lossy();
|
||||
|
||||
|
||||
if !path.contains(&*dir_str) {
|
||||
let new_path = if path.is_empty() {
|
||||
dir_str.to_string()
|
||||
} else {
|
||||
format!("{};{}", path, dir_str)
|
||||
};
|
||||
|
||||
|
||||
env.set_value("Path", &new_path)
|
||||
.map_err(|e| format!("Failed to update PATH: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Ok(())
|
||||
|
|
@ -217,43 +228,43 @@ pub fn uninstall_cli_tool() -> Result<CliInstallResult, String> {
|
|||
fs::remove_file(&cli_path)
|
||||
.map_err(|e| format!("Failed to remove CLI tool: {}. Try running with sudo.", e))?;
|
||||
}
|
||||
|
||||
|
||||
Ok(CliInstallResult {
|
||||
installed: false,
|
||||
path: cli_path.to_string_lossy().to_string(),
|
||||
message: "CLI tool uninstalled successfully".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let user_path = std::env::var("USERPROFILE")
|
||||
.map_err(|_| "Failed to get user profile path")?;
|
||||
let user_path =
|
||||
std::env::var("USERPROFILE").map_err(|_| "Failed to get user profile path")?;
|
||||
let cli_path = PathBuf::from(&user_path).join(".vibetunnel").join("vt.cmd");
|
||||
|
||||
|
||||
if cli_path.exists() {
|
||||
fs::remove_file(&cli_path)
|
||||
.map_err(|e| format!("Failed to remove CLI tool: {}", e))?;
|
||||
fs::remove_file(&cli_path).map_err(|e| format!("Failed to remove CLI tool: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
Ok(CliInstallResult {
|
||||
installed: false,
|
||||
path: cli_path.to_string_lossy().to_string(),
|
||||
message: "CLI tool uninstalled successfully".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let home_dir = std::env::var("HOME")
|
||||
.map_err(|_| "Failed to get home directory")?;
|
||||
let cli_path = PathBuf::from(&home_dir).join(".local").join("bin").join("vt");
|
||||
|
||||
let home_dir = std::env::var("HOME").map_err(|_| "Failed to get home directory")?;
|
||||
let cli_path = PathBuf::from(&home_dir)
|
||||
.join(".local")
|
||||
.join("bin")
|
||||
.join("vt");
|
||||
|
||||
if cli_path.exists() {
|
||||
fs::remove_file(&cli_path)
|
||||
.map_err(|e| format!("Failed to remove CLI tool: {}", e))?;
|
||||
fs::remove_file(&cli_path).map_err(|e| format!("Failed to remove CLI tool: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
Ok(CliInstallResult {
|
||||
installed: false,
|
||||
path: cli_path.to_string_lossy().to_string(),
|
||||
|
|
@ -267,20 +278,27 @@ pub fn is_cli_installed() -> bool {
|
|||
{
|
||||
PathBuf::from("/usr/local/bin/vt").exists()
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(user_path) = std::env::var("USERPROFILE") {
|
||||
PathBuf::from(&user_path).join(".vibetunnel").join("vt.cmd").exists()
|
||||
PathBuf::from(&user_path)
|
||||
.join(".vibetunnel")
|
||||
.join("vt.cmd")
|
||||
.exists()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Ok(home_dir) = std::env::var("HOME") {
|
||||
PathBuf::from(&home_dir).join(".local").join("bin").join("vt").exists()
|
||||
PathBuf::from(&home_dir)
|
||||
.join(".local")
|
||||
.join("bin")
|
||||
.join("vt")
|
||||
.exists()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
@ -300,4 +318,4 @@ pub fn uninstall_cli() -> Result<CliInstallResult, String> {
|
|||
#[tauri::command]
|
||||
pub fn check_cli_installed() -> Result<bool, String> {
|
||||
Ok(is_cli_installed())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,9 +1,9 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Debug feature types
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
|
|
@ -245,7 +245,10 @@ impl DebugFeaturesManager {
|
|||
}
|
||||
|
||||
/// Set the notification manager
|
||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
||||
pub fn set_notification_manager(
|
||||
&mut self,
|
||||
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||
) {
|
||||
self.notification_manager = Some(notification_manager);
|
||||
}
|
||||
|
||||
|
|
@ -260,14 +263,20 @@ impl DebugFeaturesManager {
|
|||
}
|
||||
|
||||
/// Log a message
|
||||
pub async fn log(&self, level: LogLevel, component: &str, message: &str, metadata: HashMap<String, serde_json::Value>) {
|
||||
pub async fn log(
|
||||
&self,
|
||||
level: LogLevel,
|
||||
component: &str,
|
||||
message: &str,
|
||||
metadata: HashMap<String, serde_json::Value>,
|
||||
) {
|
||||
let settings = self.settings.read().await;
|
||||
|
||||
|
||||
// Check if logging is enabled and level is appropriate
|
||||
if !settings.enabled || level < settings.log_level {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let entry = LogEntry {
|
||||
timestamp: Utc::now(),
|
||||
level,
|
||||
|
|
@ -275,16 +284,16 @@ impl DebugFeaturesManager {
|
|||
message: message.to_string(),
|
||||
metadata,
|
||||
};
|
||||
|
||||
|
||||
// Add to in-memory log
|
||||
let mut logs = self.logs.write().await;
|
||||
logs.push_back(entry.clone());
|
||||
|
||||
|
||||
// Limit log size
|
||||
while logs.len() > settings.max_log_entries {
|
||||
logs.pop_front();
|
||||
}
|
||||
|
||||
|
||||
// Log to file if enabled
|
||||
if settings.log_to_file {
|
||||
if let Some(path) = &settings.log_file_path {
|
||||
|
|
@ -294,13 +303,19 @@ impl DebugFeaturesManager {
|
|||
}
|
||||
|
||||
/// Record a performance metric
|
||||
pub async fn record_metric(&self, name: &str, value: f64, unit: &str, tags: HashMap<String, String>) {
|
||||
pub async fn record_metric(
|
||||
&self,
|
||||
name: &str,
|
||||
value: f64,
|
||||
unit: &str,
|
||||
tags: HashMap<String, String>,
|
||||
) {
|
||||
let settings = self.settings.read().await;
|
||||
|
||||
|
||||
if !settings.enabled || !settings.enable_performance_monitoring {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let metric = PerformanceMetric {
|
||||
name: name.to_string(),
|
||||
value,
|
||||
|
|
@ -308,10 +323,10 @@ impl DebugFeaturesManager {
|
|||
timestamp: Utc::now(),
|
||||
tags,
|
||||
};
|
||||
|
||||
|
||||
let mut metrics = self.performance_metrics.write().await;
|
||||
metrics.push_back(metric);
|
||||
|
||||
|
||||
// Keep only last 1000 metrics
|
||||
while metrics.len() > 1000 {
|
||||
metrics.pop_front();
|
||||
|
|
@ -321,11 +336,11 @@ impl DebugFeaturesManager {
|
|||
/// Take a memory snapshot
|
||||
pub async fn take_memory_snapshot(&self) -> Result<MemorySnapshot, String> {
|
||||
let settings = self.settings.read().await;
|
||||
|
||||
|
||||
if !settings.enabled || !settings.enable_memory_profiling {
|
||||
return Err("Memory profiling is disabled".to_string());
|
||||
}
|
||||
|
||||
|
||||
// TODO: Implement actual memory profiling
|
||||
let snapshot = MemorySnapshot {
|
||||
timestamp: Utc::now(),
|
||||
|
|
@ -335,29 +350,29 @@ impl DebugFeaturesManager {
|
|||
process_rss_mb: 0.0,
|
||||
details: HashMap::new(),
|
||||
};
|
||||
|
||||
|
||||
let mut snapshots = self.memory_snapshots.write().await;
|
||||
snapshots.push_back(snapshot.clone());
|
||||
|
||||
|
||||
// Keep only last 100 snapshots
|
||||
while snapshots.len() > 100 {
|
||||
snapshots.pop_front();
|
||||
}
|
||||
|
||||
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
/// Log a network request
|
||||
pub async fn log_network_request(&self, request: NetworkRequest) {
|
||||
let settings = self.settings.read().await;
|
||||
|
||||
|
||||
if !settings.enabled || !settings.enable_network_inspector {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let mut requests = self.network_requests.write().await;
|
||||
requests.insert(request.id.clone(), request);
|
||||
|
||||
|
||||
// Keep only last 500 requests
|
||||
if requests.len() > 500 {
|
||||
// Remove oldest entries
|
||||
|
|
@ -372,28 +387,29 @@ impl DebugFeaturesManager {
|
|||
/// Run API tests
|
||||
pub async fn run_api_tests(&self, tests: Vec<APITestCase>) -> Vec<APITestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
|
||||
for test in tests {
|
||||
let result = self.run_single_api_test(&test).await;
|
||||
results.push(result.clone());
|
||||
|
||||
|
||||
// Store result
|
||||
let mut test_results = self.api_test_results.write().await;
|
||||
test_results.entry(test.id.clone())
|
||||
test_results
|
||||
.entry(test.id.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(result);
|
||||
}
|
||||
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Run a single API test
|
||||
async fn run_single_api_test(&self, test: &APITestCase) -> APITestResult {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
|
||||
// TODO: Implement actual API testing
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
|
||||
APITestResult {
|
||||
test_id: test.id.clone(),
|
||||
success: false,
|
||||
|
|
@ -408,15 +424,15 @@ impl DebugFeaturesManager {
|
|||
/// Run benchmarks
|
||||
pub async fn run_benchmarks(&self, configs: Vec<BenchmarkConfig>) -> Vec<BenchmarkResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
|
||||
for config in configs {
|
||||
let result = self.run_single_benchmark(&config).await;
|
||||
results.push(result.clone());
|
||||
|
||||
|
||||
// Store result
|
||||
self.benchmark_results.write().await.push(result);
|
||||
}
|
||||
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
|
|
@ -443,8 +459,13 @@ impl DebugFeaturesManager {
|
|||
let app_info = self.get_app_info().await;
|
||||
let performance_summary = self.get_performance_summary().await;
|
||||
let error_summary = self.get_error_summary().await;
|
||||
let recommendations = self.generate_recommendations(&system_info, &app_info, &performance_summary, &error_summary);
|
||||
|
||||
let recommendations = self.generate_recommendations(
|
||||
&system_info,
|
||||
&app_info,
|
||||
&performance_summary,
|
||||
&error_summary,
|
||||
);
|
||||
|
||||
DiagnosticReport {
|
||||
timestamp: Utc::now(),
|
||||
system_info,
|
||||
|
|
@ -459,13 +480,13 @@ impl DebugFeaturesManager {
|
|||
pub async fn get_logs(&self, limit: Option<usize>, level: Option<LogLevel>) -> Vec<LogEntry> {
|
||||
let logs = self.logs.read().await;
|
||||
let iter = logs.iter().rev();
|
||||
|
||||
|
||||
let filtered: Vec<_> = if let Some(min_level) = level {
|
||||
iter.filter(|log| log.level >= min_level).cloned().collect()
|
||||
} else {
|
||||
iter.cloned().collect()
|
||||
};
|
||||
|
||||
|
||||
match limit {
|
||||
Some(n) => filtered.into_iter().take(n).collect(),
|
||||
None => filtered,
|
||||
|
|
@ -495,7 +516,7 @@ impl DebugFeaturesManager {
|
|||
let requests = self.network_requests.read().await;
|
||||
let mut sorted: Vec<_> = requests.values().cloned().collect();
|
||||
sorted.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
||||
|
||||
|
||||
match limit {
|
||||
Some(n) => sorted.into_iter().take(n).collect(),
|
||||
None => sorted,
|
||||
|
|
@ -515,21 +536,23 @@ impl DebugFeaturesManager {
|
|||
/// Enable/disable debug mode
|
||||
pub async fn set_debug_mode(&self, enabled: bool) {
|
||||
self.settings.write().await.enabled = enabled;
|
||||
|
||||
|
||||
if let Some(notification_manager) = &self.notification_manager {
|
||||
let message = if enabled {
|
||||
"Debug mode enabled"
|
||||
} else {
|
||||
"Debug mode disabled"
|
||||
};
|
||||
let _ = notification_manager.notify_success("Debug Mode", message).await;
|
||||
let _ = notification_manager
|
||||
.notify_success("Debug Mode", message)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
async fn write_log_to_file(&self, entry: &LogEntry, path: &PathBuf) -> Result<(), String> {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
|
||||
let log_line = format!(
|
||||
"[{}] [{}] [{}] {}\n",
|
||||
entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
|
||||
|
|
@ -537,17 +560,18 @@ impl DebugFeaturesManager {
|
|||
entry.component,
|
||||
entry.message
|
||||
);
|
||||
|
||||
|
||||
let mut file = tokio::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
file.write_all(log_line.as_bytes()).await
|
||||
|
||||
file.write_all(log_line.as_bytes())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -568,7 +592,7 @@ impl DebugFeaturesManager {
|
|||
AppInfo {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
build_date: chrono::Utc::now().to_rfc3339(), // TODO: Get actual build date
|
||||
uptime_seconds: 0, // TODO: Track uptime
|
||||
uptime_seconds: 0, // TODO: Track uptime
|
||||
active_sessions: 0,
|
||||
total_requests: 0,
|
||||
error_count: 0,
|
||||
|
|
@ -588,20 +612,23 @@ impl DebugFeaturesManager {
|
|||
|
||||
async fn get_error_summary(&self) -> ErrorSummary {
|
||||
let logs = self.logs.read().await;
|
||||
let errors: Vec<_> = logs.iter()
|
||||
let errors: Vec<_> = logs
|
||||
.iter()
|
||||
.filter(|log| log.level == LogLevel::Error)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
|
||||
let mut errors_by_type = HashMap::new();
|
||||
for error in &errors {
|
||||
let error_type = error.metadata.get("type")
|
||||
let error_type = error
|
||||
.metadata
|
||||
.get("type")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
*errors_by_type.entry(error_type).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
|
||||
ErrorSummary {
|
||||
total_errors: errors.len() as u64,
|
||||
errors_by_type,
|
||||
|
|
@ -609,25 +636,37 @@ impl DebugFeaturesManager {
|
|||
}
|
||||
}
|
||||
|
||||
fn generate_recommendations(&self, system: &SystemInfo, _app: &AppInfo, perf: &PerformanceSummary, errors: &ErrorSummary) -> Vec<String> {
|
||||
fn generate_recommendations(
|
||||
&self,
|
||||
system: &SystemInfo,
|
||||
_app: &AppInfo,
|
||||
perf: &PerformanceSummary,
|
||||
errors: &ErrorSummary,
|
||||
) -> Vec<String> {
|
||||
let mut recommendations = Vec::new();
|
||||
|
||||
|
||||
if perf.cpu_usage_percent > 80.0 {
|
||||
recommendations.push("High CPU usage detected. Consider optimizing performance-critical code.".to_string());
|
||||
recommendations.push(
|
||||
"High CPU usage detected. Consider optimizing performance-critical code."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if perf.memory_usage_mb > (system.total_memory_mb as f64 * 0.8) {
|
||||
recommendations.push("High memory usage detected. Check for memory leaks.".to_string());
|
||||
}
|
||||
|
||||
|
||||
if errors.total_errors > 100 {
|
||||
recommendations.push("High error rate detected. Review error logs for patterns.".to_string());
|
||||
recommendations
|
||||
.push("High error rate detected. Review error logs for patterns.".to_string());
|
||||
}
|
||||
|
||||
|
||||
if perf.avg_response_time_ms > 1000.0 {
|
||||
recommendations.push("Slow response times detected. Consider caching or query optimization.".to_string());
|
||||
recommendations.push(
|
||||
"Slow response times detected. Consider caching or query optimization.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
recommendations
|
||||
}
|
||||
}
|
||||
|
|
@ -645,4 +684,4 @@ pub struct DebugStats {
|
|||
}
|
||||
|
||||
// Re-export num_cpus if needed
|
||||
extern crate num_cpus;
|
||||
extern crate num_cpus;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use axum::{
|
||||
extract::Query,
|
||||
http::{StatusCode, header},
|
||||
http::{header, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
|
|
@ -62,8 +62,7 @@ pub struct OperationResult {
|
|||
/// Expand tilde to home directory
|
||||
fn expand_path(path: &str) -> Result<PathBuf, StatusCode> {
|
||||
if path.starts_with('~') {
|
||||
let home = dirs::home_dir()
|
||||
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let home = dirs::home_dir().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(home.join(path.strip_prefix("~/").unwrap_or("")))
|
||||
} else {
|
||||
Ok(PathBuf::from(path))
|
||||
|
|
@ -75,70 +74,77 @@ pub async fn get_file_info(
|
|||
Query(params): Query<FileQuery>,
|
||||
) -> Result<Json<FileMetadata>, StatusCode> {
|
||||
let path = expand_path(¶ms.path)?;
|
||||
|
||||
let metadata = fs::metadata(&path).await
|
||||
|
||||
let metadata = fs::metadata(&path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
|
||||
let name = path.file_name()
|
||||
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.to_string_lossy().to_string());
|
||||
|
||||
let is_symlink = fs::symlink_metadata(&path).await
|
||||
|
||||
let is_symlink = fs::symlink_metadata(&path)
|
||||
.await
|
||||
.map(|m| m.file_type().is_symlink())
|
||||
.unwrap_or(false);
|
||||
|
||||
|
||||
let hidden = name.starts_with('.');
|
||||
|
||||
let created = metadata.created()
|
||||
|
||||
let created = metadata
|
||||
.created()
|
||||
.map(|t| {
|
||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||
datetime.to_rfc3339()
|
||||
})
|
||||
.ok();
|
||||
|
||||
let modified = metadata.modified()
|
||||
|
||||
let modified = metadata
|
||||
.modified()
|
||||
.map(|t| {
|
||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||
datetime.to_rfc3339()
|
||||
})
|
||||
.ok();
|
||||
|
||||
let accessed = metadata.accessed()
|
||||
|
||||
let accessed = metadata
|
||||
.accessed()
|
||||
.map(|t| {
|
||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||
datetime.to_rfc3339()
|
||||
})
|
||||
.ok();
|
||||
|
||||
|
||||
#[cfg(unix)]
|
||||
let permissions = {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
Some(format!("{:o}", metadata.permissions().mode() & 0o777))
|
||||
};
|
||||
|
||||
|
||||
let mime_type = if metadata.is_file() {
|
||||
// Simple MIME type detection based on extension
|
||||
let ext = path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
Some(match ext {
|
||||
"txt" => "text/plain",
|
||||
"html" | "htm" => "text/html",
|
||||
"css" => "text/css",
|
||||
"js" => "application/javascript",
|
||||
"json" => "application/json",
|
||||
"png" => "image/png",
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"gif" => "image/gif",
|
||||
"pdf" => "application/pdf",
|
||||
"zip" => "application/zip",
|
||||
_ => "application/octet-stream",
|
||||
}.to_string())
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
|
||||
Some(
|
||||
match ext {
|
||||
"txt" => "text/plain",
|
||||
"html" | "htm" => "text/html",
|
||||
"css" => "text/css",
|
||||
"js" => "application/javascript",
|
||||
"json" => "application/json",
|
||||
"png" => "image/png",
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"gif" => "image/gif",
|
||||
"pdf" => "application/pdf",
|
||||
"zip" => "application/zip",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
|
||||
Ok(Json(FileMetadata {
|
||||
name,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
|
|
@ -160,29 +166,31 @@ pub async fn get_file_info(
|
|||
}
|
||||
|
||||
/// Read file contents
|
||||
pub async fn read_file(
|
||||
Query(params): Query<FileQuery>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
pub async fn read_file(Query(params): Query<FileQuery>) -> Result<Response, StatusCode> {
|
||||
let path = expand_path(¶ms.path)?;
|
||||
|
||||
|
||||
// Check if file exists and is a file
|
||||
let metadata = fs::metadata(&path).await
|
||||
let metadata = fs::metadata(&path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
|
||||
|
||||
if !metadata.is_file() {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
// Read file contents
|
||||
let mut file = fs::File::open(&path).await
|
||||
let mut file = fs::File::open(&path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
let mut contents = Vec::new();
|
||||
file.read_to_end(&mut contents).await
|
||||
file.read_to_end(&mut contents)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
// Determine content type
|
||||
let content_type = path.extension()
|
||||
let content_type = path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.and_then(|ext| match ext {
|
||||
"txt" => Some("text/plain"),
|
||||
|
|
@ -197,11 +205,8 @@ pub async fn read_file(
|
|||
_ => None,
|
||||
})
|
||||
.unwrap_or("application/octet-stream");
|
||||
|
||||
Ok((
|
||||
[(header::CONTENT_TYPE, content_type)],
|
||||
contents,
|
||||
).into_response())
|
||||
|
||||
Ok(([(header::CONTENT_TYPE, content_type)], contents).into_response())
|
||||
}
|
||||
|
||||
/// Write file contents
|
||||
|
|
@ -209,24 +214,27 @@ pub async fn write_file(
|
|||
Json(req): Json<WriteFileRequest>,
|
||||
) -> Result<Json<OperationResult>, StatusCode> {
|
||||
let path = expand_path(&req.path)?;
|
||||
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).await
|
||||
fs::create_dir_all(parent)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
|
||||
|
||||
// Write file
|
||||
let content = if req.encoding.as_deref() == Some("base64") {
|
||||
base64::engine::general_purpose::STANDARD.decode(&req.content)
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(&req.content)
|
||||
.map_err(|_| StatusCode::BAD_REQUEST)?
|
||||
} else {
|
||||
req.content.into_bytes()
|
||||
};
|
||||
|
||||
fs::write(&path, content).await
|
||||
|
||||
fs::write(&path, content)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
Ok(Json(OperationResult {
|
||||
success: true,
|
||||
message: format!("File written successfully: {}", path.display()),
|
||||
|
|
@ -238,20 +246,23 @@ pub async fn delete_file(
|
|||
Query(params): Query<FileQuery>,
|
||||
) -> Result<Json<OperationResult>, StatusCode> {
|
||||
let path = expand_path(¶ms.path)?;
|
||||
|
||||
|
||||
// Check if path exists
|
||||
let metadata = fs::metadata(&path).await
|
||||
let metadata = fs::metadata(&path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
|
||||
|
||||
// Delete based on type
|
||||
if metadata.is_dir() {
|
||||
fs::remove_dir_all(&path).await
|
||||
fs::remove_dir_all(&path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
} else {
|
||||
fs::remove_file(&path).await
|
||||
fs::remove_file(&path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
|
||||
|
||||
Ok(Json(OperationResult {
|
||||
success: true,
|
||||
message: format!("Deleted: {}", path.display()),
|
||||
|
|
@ -259,100 +270,116 @@ pub async fn delete_file(
|
|||
}
|
||||
|
||||
/// Move/rename file or directory
|
||||
pub async fn move_file(
|
||||
Json(req): Json<MoveRequest>,
|
||||
) -> Result<Json<OperationResult>, StatusCode> {
|
||||
pub async fn move_file(Json(req): Json<MoveRequest>) -> Result<Json<OperationResult>, StatusCode> {
|
||||
let from_path = expand_path(&req.from)?;
|
||||
let to_path = expand_path(&req.to)?;
|
||||
|
||||
|
||||
// Check if source exists
|
||||
if !from_path.exists() {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
// Check if destination already exists
|
||||
if to_path.exists() {
|
||||
return Err(StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
|
||||
// Ensure destination parent directory exists
|
||||
if let Some(parent) = to_path.parent() {
|
||||
fs::create_dir_all(parent).await
|
||||
fs::create_dir_all(parent)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
|
||||
|
||||
// Move the file/directory
|
||||
fs::rename(&from_path, &to_path).await
|
||||
fs::rename(&from_path, &to_path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
Ok(Json(OperationResult {
|
||||
success: true,
|
||||
message: format!("Moved from {} to {}", from_path.display(), to_path.display()),
|
||||
message: format!(
|
||||
"Moved from {} to {}",
|
||||
from_path.display(),
|
||||
to_path.display()
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Copy file or directory
|
||||
pub async fn copy_file(
|
||||
Json(req): Json<CopyRequest>,
|
||||
) -> Result<Json<OperationResult>, StatusCode> {
|
||||
pub async fn copy_file(Json(req): Json<CopyRequest>) -> Result<Json<OperationResult>, StatusCode> {
|
||||
let from_path = expand_path(&req.from)?;
|
||||
let to_path = expand_path(&req.to)?;
|
||||
|
||||
|
||||
// Check if source exists
|
||||
let metadata = fs::metadata(&from_path).await
|
||||
let metadata = fs::metadata(&from_path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
|
||||
|
||||
// Check if destination already exists
|
||||
if to_path.exists() && !req.overwrite.unwrap_or(false) {
|
||||
return Err(StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
|
||||
// Ensure destination parent directory exists
|
||||
if let Some(parent) = to_path.parent() {
|
||||
fs::create_dir_all(parent).await
|
||||
fs::create_dir_all(parent)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
|
||||
|
||||
// Copy based on type
|
||||
if metadata.is_file() {
|
||||
fs::copy(&from_path, &to_path).await
|
||||
fs::copy(&from_path, &to_path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
} else if metadata.is_dir() {
|
||||
// Recursive directory copy
|
||||
copy_dir_recursive(&from_path, &to_path).await?;
|
||||
}
|
||||
|
||||
|
||||
Ok(Json(OperationResult {
|
||||
success: true,
|
||||
message: format!("Copied from {} to {}", from_path.display(), to_path.display()),
|
||||
message: format!(
|
||||
"Copied from {} to {}",
|
||||
from_path.display(),
|
||||
to_path.display()
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Recursively copy a directory
|
||||
async fn copy_dir_recursive(from: &PathBuf, to: &PathBuf) -> Result<(), StatusCode> {
|
||||
fs::create_dir_all(to).await
|
||||
fs::create_dir_all(to)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let mut entries = fs::read_dir(from).await
|
||||
|
||||
let mut entries = fs::read_dir(from)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
while let Some(entry) = entries.next_entry().await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
||||
|
||||
|
||||
while let Some(entry) = entries
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
{
|
||||
let from_path = entry.path();
|
||||
let to_path = to.join(entry.file_name());
|
||||
|
||||
let metadata = entry.metadata().await
|
||||
|
||||
let metadata = entry
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
if metadata.is_file() {
|
||||
fs::copy(&from_path, &to_path).await
|
||||
fs::copy(&from_path, &to_path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
} else if metadata.is_dir() {
|
||||
Box::pin(copy_dir_recursive(&from_path, &to_path)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -378,10 +405,10 @@ pub async fn search_files(
|
|||
let base_path = expand_path(¶ms.path)?;
|
||||
let pattern = params.pattern.to_lowercase();
|
||||
let max_depth = params.max_depth.unwrap_or(5);
|
||||
|
||||
|
||||
let mut results = Vec::new();
|
||||
search_recursive(&base_path, &pattern, 0, max_depth, &mut results).await?;
|
||||
|
||||
|
||||
Ok(Json(results))
|
||||
}
|
||||
|
||||
|
|
@ -395,20 +422,25 @@ async fn search_recursive(
|
|||
if depth > max_depth {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut entries = fs::read_dir(path).await
|
||||
|
||||
let mut entries = fs::read_dir(path)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
while let Some(entry) = entries.next_entry().await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
||||
|
||||
|
||||
while let Some(entry) = entries
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
{
|
||||
let entry_path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
|
||||
if file_name.to_lowercase().contains(pattern) {
|
||||
let metadata = entry.metadata().await
|
||||
let metadata = entry
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
results.push(SearchResult {
|
||||
path: entry_path.to_string_lossy().to_string(),
|
||||
name: file_name,
|
||||
|
|
@ -416,14 +448,19 @@ async fn search_recursive(
|
|||
size: metadata.len(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Recurse into directories
|
||||
if entry.file_type().await
|
||||
.map(|t| t.is_dir())
|
||||
.unwrap_or(false) {
|
||||
Box::pin(search_recursive(&entry_path, pattern, depth + 1, max_depth, results)).await?;
|
||||
if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
|
||||
Box::pin(search_recursive(
|
||||
&entry_path,
|
||||
pattern,
|
||||
depth + 1,
|
||||
max_depth,
|
||||
results,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
pub mod commands;
|
||||
pub mod terminal;
|
||||
pub mod server;
|
||||
pub mod state;
|
||||
pub mod settings;
|
||||
pub mod auto_launch;
|
||||
pub mod ngrok;
|
||||
pub mod auth;
|
||||
pub mod terminal_detector;
|
||||
pub mod cli_installer;
|
||||
pub mod tray_menu;
|
||||
pub mod cast;
|
||||
pub mod tty_forward;
|
||||
pub mod session_monitor;
|
||||
pub mod port_conflict;
|
||||
pub mod network_utils;
|
||||
pub mod notification_manager;
|
||||
pub mod welcome;
|
||||
pub mod permissions;
|
||||
pub mod updater;
|
||||
pub mod backend_manager;
|
||||
pub mod debug_features;
|
||||
pub mod api_testing;
|
||||
pub mod auth_cache;
|
||||
pub mod terminal_integrations;
|
||||
pub mod app_mover;
|
||||
pub mod terminal_spawn_service;
|
||||
pub mod auth;
|
||||
pub mod auth_cache;
|
||||
pub mod auto_launch;
|
||||
pub mod backend_manager;
|
||||
pub mod cast;
|
||||
pub mod cli_installer;
|
||||
pub mod commands;
|
||||
pub mod debug_features;
|
||||
pub mod fs_api;
|
||||
pub mod network_utils;
|
||||
pub mod ngrok;
|
||||
pub mod notification_manager;
|
||||
pub mod permissions;
|
||||
pub mod port_conflict;
|
||||
pub mod server;
|
||||
pub mod session_monitor;
|
||||
pub mod settings;
|
||||
pub mod state;
|
||||
pub mod terminal;
|
||||
pub mod terminal_detector;
|
||||
pub mod terminal_integrations;
|
||||
pub mod terminal_spawn_service;
|
||||
pub mod tray_menu;
|
||||
pub mod tty_forward;
|
||||
pub mod updater;
|
||||
pub mod welcome;
|
||||
|
||||
#[cfg(mobile)]
|
||||
pub fn init() {
|
||||
// Mobile-specific initialization
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,44 +3,44 @@
|
|||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use tauri::{AppHandle, Manager, Emitter, WindowEvent};
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::menu::Menu;
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::{AppHandle, Emitter, Manager, WindowEvent};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
mod commands;
|
||||
mod terminal;
|
||||
mod server;
|
||||
mod state;
|
||||
mod settings;
|
||||
mod auto_launch;
|
||||
mod ngrok;
|
||||
mod terminal_detector;
|
||||
mod cli_installer;
|
||||
mod auth;
|
||||
mod tray_menu;
|
||||
mod cast;
|
||||
mod tty_forward;
|
||||
mod session_monitor;
|
||||
mod port_conflict;
|
||||
mod network_utils;
|
||||
mod notification_manager;
|
||||
mod welcome;
|
||||
mod permissions;
|
||||
mod updater;
|
||||
mod backend_manager;
|
||||
mod debug_features;
|
||||
mod api_testing;
|
||||
mod auth_cache;
|
||||
mod terminal_integrations;
|
||||
mod app_mover;
|
||||
mod terminal_spawn_service;
|
||||
mod auth;
|
||||
mod auth_cache;
|
||||
mod auto_launch;
|
||||
mod backend_manager;
|
||||
mod cast;
|
||||
mod cli_installer;
|
||||
mod commands;
|
||||
mod debug_features;
|
||||
mod fs_api;
|
||||
mod network_utils;
|
||||
mod ngrok;
|
||||
mod notification_manager;
|
||||
mod permissions;
|
||||
mod port_conflict;
|
||||
mod server;
|
||||
mod session_monitor;
|
||||
mod settings;
|
||||
mod state;
|
||||
mod terminal;
|
||||
mod terminal_detector;
|
||||
mod terminal_integrations;
|
||||
mod terminal_spawn_service;
|
||||
mod tray_menu;
|
||||
mod tty_forward;
|
||||
mod updater;
|
||||
mod welcome;
|
||||
|
||||
use commands::*;
|
||||
use state::AppState;
|
||||
use server::HttpServer;
|
||||
use commands::ServerStatus;
|
||||
use commands::*;
|
||||
use server::HttpServer;
|
||||
use state::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
fn open_settings_window(app: AppHandle) -> Result<(), String> {
|
||||
|
|
@ -53,7 +53,7 @@ fn open_settings_window(app: AppHandle) -> Result<(), String> {
|
|||
tauri::WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"settings",
|
||||
tauri::WebviewUrl::App("settings.html".into())
|
||||
tauri::WebviewUrl::App("settings.html".into()),
|
||||
)
|
||||
.title("VibeTunnel Settings")
|
||||
.inner_size(800.0, 600.0)
|
||||
|
|
@ -66,10 +66,13 @@ fn open_settings_window(app: AppHandle) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn update_tray_menu_status(_app: &AppHandle, port: u16, _session_count: usize) {
|
||||
// For now, just log the status update
|
||||
// TODO: In Tauri v2, dynamic menu updates require rebuilding the menu
|
||||
tracing::info!("Server status updated: port {}", port);
|
||||
fn update_tray_menu_status(app: &AppHandle, port: u16, session_count: usize) {
|
||||
// Update tray menu status using the tray menu manager
|
||||
let app_handle = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tray_menu::TrayMenuManager::update_server_status(&app_handle, port, true).await;
|
||||
tray_menu::TrayMenuManager::update_session_count(&app_handle, session_count).await;
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
|
@ -290,16 +293,16 @@ fn main() {
|
|||
welcome_manager.set_app_handle(app_handle2).await;
|
||||
permissions_manager.set_app_handle(app_handle3).await;
|
||||
update_manager.set_app_handle(app_handle4).await;
|
||||
|
||||
|
||||
// Load welcome state and check if should show welcome
|
||||
let _ = welcome_manager.load_state().await;
|
||||
if welcome_manager.should_show_welcome().await {
|
||||
let _ = welcome_manager.show_welcome_window().await;
|
||||
}
|
||||
|
||||
|
||||
// Check permissions on startup
|
||||
let _ = permissions_manager.check_all_permissions().await;
|
||||
|
||||
|
||||
// Check if app should be moved to Applications folder (macOS only)
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
|
@ -310,14 +313,18 @@ fn main() {
|
|||
let _ = app_mover::check_and_prompt_move(app_handle_move).await;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Load updater settings and start auto-check
|
||||
let _ = update_manager.load_settings().await;
|
||||
update_manager.clone().start_auto_check().await;
|
||||
});
|
||||
|
||||
// Create system tray icon using menu-bar-icon.png with template mode
|
||||
let icon_path = app.path().resource_dir().unwrap().join("icons/menu-bar-icon.png");
|
||||
let icon_path = app
|
||||
.path()
|
||||
.resource_dir()
|
||||
.unwrap()
|
||||
.join("icons/menu-bar-icon.png");
|
||||
let tray_icon = if let Ok(icon_data) = std::fs::read(&icon_path) {
|
||||
tauri::image::Image::from_bytes(&icon_data).ok()
|
||||
} else {
|
||||
|
|
@ -360,16 +367,17 @@ fn main() {
|
|||
|
||||
// Load settings to determine initial dock icon visibility
|
||||
let settings = settings::Settings::load().unwrap_or_default();
|
||||
|
||||
|
||||
// Check if launched at startup (auto-launch)
|
||||
let is_auto_launched = std::env::args().any(|arg| arg == "--auto-launch" || arg == "--minimized");
|
||||
|
||||
let is_auto_launched =
|
||||
std::env::args().any(|arg| arg == "--auto-launch" || arg == "--minimized");
|
||||
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
|
||||
|
||||
// Hide window if auto-launched
|
||||
if is_auto_launched {
|
||||
window.hide()?;
|
||||
|
||||
|
||||
// On macOS, apply dock icon visibility based on settings
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
|
@ -393,13 +401,15 @@ fn main() {
|
|||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = window_clone.hide();
|
||||
|
||||
|
||||
// Hide dock icon on macOS when window is hidden (only if settings say so)
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Ok(settings) = settings::Settings::load() {
|
||||
if !settings.general.show_dock_icon {
|
||||
let _ = window_clone.app_handle().set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
let _ = window_clone
|
||||
.app_handle()
|
||||
.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -420,10 +430,11 @@ fn main() {
|
|||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(dead_code)]
|
||||
fn create_app_menu(app: &tauri::App) -> Result<Menu<tauri::Wry>, tauri::Error> {
|
||||
// Create the menu using the builder pattern
|
||||
let menu = Menu::new(app)?;
|
||||
|
||||
|
||||
// For now, return a basic menu
|
||||
// TODO: Once we understand the correct Tauri v2 menu API, implement full menu
|
||||
Ok(menu)
|
||||
|
|
@ -546,7 +557,7 @@ fn show_main_window(app: AppHandle) -> Result<(), String> {
|
|||
if let Some(window) = app.get_webview_window("main") {
|
||||
window.show().map_err(|e| e.to_string())?;
|
||||
window.set_focus().map_err(|e| e.to_string())?;
|
||||
|
||||
|
||||
// Show dock icon on macOS when window is shown
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
|
@ -560,87 +571,111 @@ fn show_main_window(app: AppHandle) -> Result<(), String> {
|
|||
fn quit_app(app: AppHandle) {
|
||||
// Stop monitoring before exit
|
||||
let state = app.state::<AppState>();
|
||||
state.server_monitoring.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
state
|
||||
.server_monitoring
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Close all terminal sessions
|
||||
let terminal_manager = state.terminal_manager.clone();
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let _ = terminal_manager.close_all_sessions().await;
|
||||
});
|
||||
|
||||
app.exit(0);
|
||||
}
|
||||
|
||||
async fn start_server_with_monitoring(app_handle: AppHandle) {
|
||||
let state = app_handle.state::<AppState>();
|
||||
let state_clone = state.inner().clone();
|
||||
|
||||
|
||||
// Start initial server
|
||||
match start_server_internal(&*state).await {
|
||||
Ok(status) => {
|
||||
tracing::info!("Server started on port {}", status.port);
|
||||
*state.server_target_port.write().await = Some(status.port);
|
||||
|
||||
|
||||
// Update tray menu with server status
|
||||
update_tray_menu_status(&app_handle, status.port, 0);
|
||||
|
||||
|
||||
// Show notification
|
||||
let _ = state.notification_manager.notify_server_status(true, status.port).await;
|
||||
let _ = state
|
||||
.notification_manager
|
||||
.notify_server_status(true, status.port)
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to start server: {}", e);
|
||||
let _ = state.notification_manager.notify_error(
|
||||
"Server Start Failed",
|
||||
&format!("Failed to start server: {}", e)
|
||||
).await;
|
||||
let _ = state
|
||||
.notification_manager
|
||||
.notify_error(
|
||||
"Server Start Failed",
|
||||
&format!("Failed to start server: {}", e),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Monitor server health
|
||||
let monitoring_state = state_clone.clone();
|
||||
let monitoring_app = app_handle.clone();
|
||||
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut check_interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
|
||||
|
||||
while monitoring_state.server_monitoring.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
|
||||
while monitoring_state
|
||||
.server_monitoring
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
{
|
||||
check_interval.tick().await;
|
||||
|
||||
|
||||
// Check if server is still running
|
||||
let server_running = {
|
||||
let server = monitoring_state.http_server.read().await;
|
||||
server.is_some()
|
||||
};
|
||||
|
||||
|
||||
if server_running {
|
||||
// Perform health check
|
||||
let health_check_result = perform_server_health_check(&monitoring_state).await;
|
||||
|
||||
|
||||
if !health_check_result {
|
||||
tracing::warn!("Server health check failed, attempting restart...");
|
||||
|
||||
|
||||
// Stop current server
|
||||
let _ = stop_server_internal(&monitoring_state).await;
|
||||
|
||||
|
||||
// Wait a bit before restart
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
|
||||
// Restart server
|
||||
match start_server_internal(&monitoring_state).await {
|
||||
Ok(status) => {
|
||||
tracing::info!("Server restarted on port {}", status.port);
|
||||
*monitoring_state.server_target_port.write().await = Some(status.port);
|
||||
|
||||
|
||||
// Update tray menu with server status
|
||||
update_tray_menu_status(&monitoring_app, status.port, 0);
|
||||
|
||||
|
||||
// Notify frontend of server restart
|
||||
if let Some(window) = monitoring_app.get_webview_window("main") {
|
||||
let _ = window.emit("server:restarted", &status);
|
||||
}
|
||||
|
||||
|
||||
// Show notification
|
||||
let _ = monitoring_state.notification_manager.notify_server_status(true, status.port).await;
|
||||
let _ = monitoring_state
|
||||
.notification_manager
|
||||
.notify_server_status(true, status.port)
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to restart server: {}", e);
|
||||
let _ = monitoring_state.notification_manager.notify_error(
|
||||
"Server Restart Failed",
|
||||
&format!("Failed to restart server: {}", e)
|
||||
).await;
|
||||
let _ = monitoring_state
|
||||
.notification_manager
|
||||
.notify_error(
|
||||
"Server Restart Failed",
|
||||
&format!("Failed to restart server: {}", e),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -649,31 +684,37 @@ async fn start_server_with_monitoring(app_handle: AppHandle) {
|
|||
let target_port = *monitoring_state.server_target_port.read().await;
|
||||
if target_port.is_some() {
|
||||
tracing::info!("Server not running, attempting to start...");
|
||||
|
||||
|
||||
match start_server_internal(&monitoring_state).await {
|
||||
Ok(status) => {
|
||||
tracing::info!("Server started on port {}", status.port);
|
||||
|
||||
|
||||
// Notify frontend of server restart
|
||||
if let Some(window) = monitoring_app.get_webview_window("main") {
|
||||
let _ = window.emit("server:restarted", &status);
|
||||
}
|
||||
|
||||
|
||||
// Show notification
|
||||
let _ = monitoring_state.notification_manager.notify_server_status(true, status.port).await;
|
||||
let _ = monitoring_state
|
||||
.notification_manager
|
||||
.notify_server_status(true, status.port)
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to start server: {}", e);
|
||||
let _ = monitoring_state.notification_manager.notify_error(
|
||||
"Server Start Failed",
|
||||
&format!("Failed to start server: {}", e)
|
||||
).await;
|
||||
let _ = monitoring_state
|
||||
.notification_manager
|
||||
.notify_error(
|
||||
"Server Start Failed",
|
||||
&format!("Failed to start server: {}", e),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tracing::info!("Server monitoring stopped");
|
||||
});
|
||||
}
|
||||
|
|
@ -685,7 +726,7 @@ async fn perform_server_health_check(state: &AppState) -> bool {
|
|||
// Server reports as running, perform additional check
|
||||
// by trying to access the API endpoint
|
||||
let url = format!("http://localhost:{}/api/sessions", status.port);
|
||||
|
||||
|
||||
match reqwest::Client::new()
|
||||
.get(&url)
|
||||
.timeout(std::time::Duration::from_secs(2))
|
||||
|
|
@ -703,50 +744,62 @@ async fn perform_server_health_check(state: &AppState) -> bool {
|
|||
// Internal server management functions that work directly with AppState
|
||||
async fn start_server_internal(state: &AppState) -> Result<ServerStatus, String> {
|
||||
let mut server = state.http_server.write().await;
|
||||
|
||||
|
||||
if let Some(http_server) = server.as_ref() {
|
||||
// Get actual port from running server
|
||||
let port = http_server.port();
|
||||
|
||||
|
||||
// Check if ngrok is active
|
||||
let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() {
|
||||
ngrok_tunnel.url
|
||||
} else {
|
||||
format!("http://localhost:{}", port)
|
||||
};
|
||||
|
||||
|
||||
return Ok(ServerStatus {
|
||||
running: true,
|
||||
port,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Load settings to check if password is enabled
|
||||
let settings = crate::settings::Settings::load().unwrap_or_default();
|
||||
|
||||
|
||||
// Start HTTP server with auth if configured
|
||||
let mut http_server = if settings.dashboard.enable_password && !settings.dashboard.password.is_empty() {
|
||||
let auth_config = crate::auth::AuthConfig::new(true, Some(settings.dashboard.password));
|
||||
HttpServer::with_auth(state.terminal_manager.clone(), state.session_monitor.clone(), auth_config)
|
||||
} else {
|
||||
HttpServer::new(state.terminal_manager.clone(), state.session_monitor.clone())
|
||||
};
|
||||
|
||||
let mut http_server =
|
||||
if settings.dashboard.enable_password && !settings.dashboard.password.is_empty() {
|
||||
let auth_config = crate::auth::AuthConfig::new(true, Some(settings.dashboard.password));
|
||||
HttpServer::with_auth(
|
||||
state.terminal_manager.clone(),
|
||||
state.session_monitor.clone(),
|
||||
auth_config,
|
||||
)
|
||||
} else {
|
||||
HttpServer::new(
|
||||
state.terminal_manager.clone(),
|
||||
state.session_monitor.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
// Start server with appropriate access mode
|
||||
let (port, url) = match settings.dashboard.access_mode.as_str() {
|
||||
"network" => {
|
||||
let port = http_server.start_with_mode("network").await?;
|
||||
(port, format!("http://0.0.0.0:{}", port))
|
||||
},
|
||||
}
|
||||
"ngrok" => {
|
||||
// For ngrok mode, start in localhost and let ngrok handle the tunneling
|
||||
let port = http_server.start_with_mode("localhost").await?;
|
||||
|
||||
|
||||
// Try to start ngrok tunnel if auth token is configured
|
||||
let url = if let Some(auth_token) = settings.advanced.ngrok_auth_token {
|
||||
if !auth_token.is_empty() {
|
||||
match state.ngrok_manager.start_tunnel(port, Some(auth_token)).await {
|
||||
match state
|
||||
.ngrok_manager
|
||||
.start_tunnel(port, Some(auth_token))
|
||||
.await
|
||||
{
|
||||
Ok(tunnel) => tunnel.url,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to start ngrok tunnel: {}", e);
|
||||
|
|
@ -759,17 +812,17 @@ async fn start_server_internal(state: &AppState) -> Result<ServerStatus, String>
|
|||
} else {
|
||||
return Err("Ngrok auth token is required for ngrok access mode".to_string());
|
||||
};
|
||||
|
||||
|
||||
(port, url)
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
let port = http_server.start_with_mode("localhost").await?;
|
||||
(port, format!("http://localhost:{}", port))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
*server = Some(http_server);
|
||||
|
||||
|
||||
Ok(ServerStatus {
|
||||
running: true,
|
||||
port,
|
||||
|
|
@ -779,23 +832,23 @@ async fn start_server_internal(state: &AppState) -> Result<ServerStatus, String>
|
|||
|
||||
async fn stop_server_internal(state: &AppState) -> Result<(), String> {
|
||||
let mut server = state.http_server.write().await;
|
||||
|
||||
|
||||
if let Some(mut http_server) = server.take() {
|
||||
http_server.stop().await?;
|
||||
}
|
||||
|
||||
|
||||
// Also stop ngrok tunnel if active
|
||||
let _ = state.ngrok_manager.stop_tunnel().await;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_server_status_internal(state: &AppState) -> Result<ServerStatus, String> {
|
||||
let server = state.http_server.read().await;
|
||||
|
||||
|
||||
if let Some(http_server) = server.as_ref() {
|
||||
let port = http_server.port();
|
||||
|
||||
|
||||
// Check if ngrok is active and return its URL
|
||||
let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() {
|
||||
ngrok_tunnel.url
|
||||
|
|
@ -807,7 +860,7 @@ async fn get_server_status_internal(state: &AppState) -> Result<ServerStatus, St
|
|||
_ => format!("http://localhost:{}", port),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Ok(ServerStatus {
|
||||
running: true,
|
||||
port,
|
||||
|
|
@ -820,4 +873,4 @@ async fn get_server_status_internal(state: &AppState) -> Result<ServerStatus, St
|
|||
url: String::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use tracing::error;
|
||||
|
||||
|
|
@ -28,91 +28,95 @@ impl NetworkUtils {
|
|||
pub fn get_local_ip_address() -> Option<String> {
|
||||
// Try to get network interfaces
|
||||
let interfaces = Self::get_all_interfaces();
|
||||
|
||||
|
||||
// First, try to find a private network address (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
|
||||
for interface in &interfaces {
|
||||
if interface.is_loopback || !interface.is_up {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
for addr in &interface.addresses {
|
||||
if addr.is_ipv4 && addr.is_private {
|
||||
return Some(addr.address.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If no private address found, return any non-loopback IPv4
|
||||
for interface in &interfaces {
|
||||
if interface.is_loopback || !interface.is_up {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
for addr in &interface.addresses {
|
||||
if addr.is_ipv4 {
|
||||
return Some(addr.address.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
/// Get all IP addresses
|
||||
pub fn get_all_ip_addresses() -> Vec<String> {
|
||||
let interfaces = Self::get_all_interfaces();
|
||||
let mut addresses = Vec::new();
|
||||
|
||||
|
||||
for interface in interfaces {
|
||||
if interface.is_loopback {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
for addr in interface.addresses {
|
||||
addresses.push(addr.address);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
addresses
|
||||
}
|
||||
|
||||
|
||||
/// Get all network interfaces
|
||||
pub fn get_all_interfaces() -> Vec<NetworkInterface> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
Self::get_interfaces_unix()
|
||||
}
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Self::get_interfaces_windows()
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(unix)]
|
||||
fn get_interfaces_unix() -> Vec<NetworkInterface> {
|
||||
use nix::ifaddrs::getifaddrs;
|
||||
|
||||
|
||||
let mut interfaces = std::collections::HashMap::new();
|
||||
|
||||
|
||||
match getifaddrs() {
|
||||
Ok(addrs) => {
|
||||
for ifaddr in addrs {
|
||||
let name = ifaddr.interface_name.clone();
|
||||
let flags = ifaddr.flags;
|
||||
|
||||
let interface = interfaces.entry(name.clone()).or_insert_with(|| NetworkInterface {
|
||||
name,
|
||||
addresses: Vec::new(),
|
||||
is_up: flags.contains(nix::net::if_::InterfaceFlags::IFF_UP),
|
||||
is_loopback: flags.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK),
|
||||
});
|
||||
|
||||
|
||||
let interface =
|
||||
interfaces
|
||||
.entry(name.clone())
|
||||
.or_insert_with(|| NetworkInterface {
|
||||
name,
|
||||
addresses: Vec::new(),
|
||||
is_up: flags.contains(nix::net::if_::InterfaceFlags::IFF_UP),
|
||||
is_loopback: flags
|
||||
.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK),
|
||||
});
|
||||
|
||||
if let Some(address) = ifaddr.address {
|
||||
if let Some(sockaddr) = address.as_sockaddr_in() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::from(sockaddr.ip()));
|
||||
|
|
@ -138,21 +142,21 @@ impl NetworkUtils {
|
|||
error!("Failed to get network interfaces: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interfaces.into_values().collect()
|
||||
}
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
fn get_interfaces_windows() -> Vec<NetworkInterface> {
|
||||
use ipconfig::get_adapters;
|
||||
|
||||
|
||||
let mut interfaces = Vec::new();
|
||||
|
||||
|
||||
match get_adapters() {
|
||||
Ok(adapters) => {
|
||||
for adapter in adapters {
|
||||
let mut addresses = Vec::new();
|
||||
|
||||
|
||||
// Get IPv4 addresses
|
||||
for addr in adapter.ipv4_addresses() {
|
||||
addresses.push(IpAddress {
|
||||
|
|
@ -162,7 +166,7 @@ impl NetworkUtils {
|
|||
is_private: Self::is_private_ipv4(addr),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Get IPv6 addresses
|
||||
for addr in adapter.ipv6_addresses() {
|
||||
addresses.push(IpAddress {
|
||||
|
|
@ -172,7 +176,7 @@ impl NetworkUtils {
|
|||
is_private: Self::is_private_ipv6(addr),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
interfaces.push(NetworkInterface {
|
||||
name: adapter.friendly_name().to_string(),
|
||||
addresses,
|
||||
|
|
@ -185,10 +189,10 @@ impl NetworkUtils {
|
|||
error!("Failed to get network interfaces: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interfaces
|
||||
}
|
||||
|
||||
|
||||
/// Check if an IP address is private
|
||||
fn is_private_ip(ip: &IpAddr) -> bool {
|
||||
match ip {
|
||||
|
|
@ -196,29 +200,29 @@ impl NetworkUtils {
|
|||
IpAddr::V6(ipv6) => Self::is_private_ipv6(ipv6),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check if an IPv4 address is private
|
||||
fn is_private_ipv4(ip: &Ipv4Addr) -> bool {
|
||||
let octets = ip.octets();
|
||||
|
||||
|
||||
// 10.0.0.0/8
|
||||
if octets[0] == 10 {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// 172.16.0.0/12
|
||||
if octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// 192.168.0.0/16
|
||||
if octets[0] == 192 && octets[1] == 168 {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
/// Check if an IPv6 address is private
|
||||
fn is_private_ipv6(ip: &Ipv6Addr) -> bool {
|
||||
// Check for link-local addresses (fe80::/10)
|
||||
|
|
@ -226,35 +230,35 @@ impl NetworkUtils {
|
|||
if segments[0] & 0xffc0 == 0xfe80 {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for unique local addresses (fc00::/7)
|
||||
if segments[0] & 0xfe00 == 0xfc00 {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
/// Get hostname
|
||||
pub fn get_hostname() -> Option<String> {
|
||||
hostname::get()
|
||||
.ok()
|
||||
.and_then(|name| name.into_string().ok())
|
||||
}
|
||||
|
||||
|
||||
/// Test network connectivity to a host
|
||||
pub async fn test_connectivity(host: &str, port: u16) -> bool {
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
let addr = format!("{}:{}", host, port);
|
||||
match timeout(Duration::from_secs(3), TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get network statistics
|
||||
pub fn get_network_stats() -> NetworkStats {
|
||||
NetworkStats {
|
||||
|
|
@ -278,12 +282,16 @@ pub struct NetworkStats {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_private_ipv4() {
|
||||
assert!(NetworkUtils::is_private_ipv4(&"10.0.0.1".parse().unwrap()));
|
||||
assert!(NetworkUtils::is_private_ipv4(&"172.16.0.1".parse().unwrap()));
|
||||
assert!(NetworkUtils::is_private_ipv4(&"192.168.1.1".parse().unwrap()));
|
||||
assert!(NetworkUtils::is_private_ipv4(
|
||||
&"172.16.0.1".parse().unwrap()
|
||||
));
|
||||
assert!(NetworkUtils::is_private_ipv4(
|
||||
&"192.168.1.1".parse().unwrap()
|
||||
));
|
||||
assert!(!NetworkUtils::is_private_ipv4(&"8.8.8.8".parse().unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use crate::state::AppState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::{Command, Child};
|
||||
use std::process::{Child, Command};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::State;
|
||||
use crate::state::AppState;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -24,12 +24,16 @@ impl NgrokManager {
|
|||
tunnel_info: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_tunnel(&self, port: u16, auth_token: Option<String>) -> Result<NgrokTunnel, String> {
|
||||
|
||||
pub async fn start_tunnel(
|
||||
&self,
|
||||
port: u16,
|
||||
auth_token: Option<String>,
|
||||
) -> Result<NgrokTunnel, String> {
|
||||
// Check if ngrok is installed
|
||||
let ngrok_path = which::which("ngrok")
|
||||
.map_err(|_| "ngrok not found. Please install ngrok first.".to_string())?;
|
||||
|
||||
|
||||
// Set auth token if provided
|
||||
if let Some(token) = auth_token {
|
||||
Command::new(&ngrok_path)
|
||||
|
|
@ -37,72 +41,78 @@ impl NgrokManager {
|
|||
.output()
|
||||
.map_err(|e| format!("Failed to set ngrok auth token: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
// Start ngrok tunnel
|
||||
let child = Command::new(&ngrok_path)
|
||||
.args(&["http", &port.to_string(), "--log=stdout"])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start ngrok: {}", e))?;
|
||||
|
||||
|
||||
// Wait a bit for ngrok to start
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
|
||||
// Get tunnel information via ngrok API
|
||||
let tunnel_info = self.get_tunnel_info().await?;
|
||||
|
||||
|
||||
// Store process and tunnel info
|
||||
*self.process.lock().unwrap() = Some(child);
|
||||
*self.tunnel_info.lock().unwrap() = Some(tunnel_info.clone());
|
||||
|
||||
|
||||
info!("ngrok tunnel started: {}", tunnel_info.url);
|
||||
|
||||
|
||||
Ok(tunnel_info)
|
||||
}
|
||||
|
||||
|
||||
pub async fn stop_tunnel(&self) -> Result<(), String> {
|
||||
if let Some(mut child) = self.process.lock().unwrap().take() {
|
||||
child.kill()
|
||||
child
|
||||
.kill()
|
||||
.map_err(|e| format!("Failed to stop ngrok: {}", e))?;
|
||||
|
||||
|
||||
info!("ngrok tunnel stopped");
|
||||
}
|
||||
|
||||
|
||||
*self.tunnel_info.lock().unwrap() = None;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn get_tunnel_status(&self) -> Option<NgrokTunnel> {
|
||||
self.tunnel_info.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
|
||||
async fn get_tunnel_info(&self) -> Result<NgrokTunnel, String> {
|
||||
// Query ngrok local API
|
||||
let response = reqwest::get("http://localhost:4040/api/tunnels")
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query ngrok API: {}", e))?;
|
||||
|
||||
let data: serde_json::Value = response.json()
|
||||
|
||||
let data: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse ngrok API response: {}", e))?;
|
||||
|
||||
|
||||
// Extract tunnel URL
|
||||
let tunnels = data["tunnels"].as_array()
|
||||
let tunnels = data["tunnels"]
|
||||
.as_array()
|
||||
.ok_or_else(|| "No tunnels found".to_string())?;
|
||||
|
||||
let tunnel = tunnels.iter()
|
||||
|
||||
let tunnel = tunnels
|
||||
.iter()
|
||||
.find(|t| t["proto"].as_str() == Some("https"))
|
||||
.or_else(|| tunnels.first())
|
||||
.ok_or_else(|| "No tunnel found".to_string())?;
|
||||
|
||||
let url = tunnel["public_url"].as_str()
|
||||
|
||||
let url = tunnel["public_url"]
|
||||
.as_str()
|
||||
.ok_or_else(|| "No public URL found".to_string())?;
|
||||
|
||||
let port = tunnel["config"]["addr"].as_str()
|
||||
|
||||
let port = tunnel["config"]["addr"]
|
||||
.as_str()
|
||||
.and_then(|addr| addr.split(':').last())
|
||||
.and_then(|p| p.parse::<u16>().ok())
|
||||
.unwrap_or(3000);
|
||||
|
||||
|
||||
Ok(NgrokTunnel {
|
||||
url: url.to_string(),
|
||||
port,
|
||||
|
|
@ -121,15 +131,11 @@ pub async fn start_ngrok_tunnel(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_ngrok_tunnel(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
pub async fn stop_ngrok_tunnel(state: State<'_, AppState>) -> Result<(), String> {
|
||||
state.ngrok_manager.stop_tunnel().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_ngrok_status(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Option<NgrokTunnel>, String> {
|
||||
pub async fn get_ngrok_status(state: State<'_, AppState>) -> Result<Option<NgrokTunnel>, String> {
|
||||
Ok(state.ngrok_manager.get_tunnel_status())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Notification type enumeration
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
|
|
@ -127,7 +127,7 @@ impl NotificationManager {
|
|||
metadata: HashMap<String, serde_json::Value>,
|
||||
) -> Result<String, String> {
|
||||
let settings = self.settings.read().await;
|
||||
|
||||
|
||||
// Check if notifications are enabled
|
||||
if !settings.enabled {
|
||||
return Ok("notifications_disabled".to_string());
|
||||
|
|
@ -154,12 +154,15 @@ impl NotificationManager {
|
|||
};
|
||||
|
||||
// Store notification
|
||||
self.notifications.write().await.insert(notification_id.clone(), notification.clone());
|
||||
self.notifications
|
||||
.write()
|
||||
.await
|
||||
.insert(notification_id.clone(), notification.clone());
|
||||
|
||||
// Add to history
|
||||
let mut history = self.notification_history.write().await;
|
||||
history.push(notification.clone());
|
||||
|
||||
|
||||
// Trim history if it exceeds max size
|
||||
if history.len() > self.max_history_size {
|
||||
let drain_count = history.len() - self.max_history_size;
|
||||
|
|
@ -168,8 +171,11 @@ impl NotificationManager {
|
|||
|
||||
// Show system notification if enabled
|
||||
if settings.show_in_system {
|
||||
match self.show_system_notification(&title, &body, notification_type).await {
|
||||
Ok(_) => {},
|
||||
match self
|
||||
.show_system_notification(&title, &body, notification_type)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to show system notification: {}", e);
|
||||
}
|
||||
|
|
@ -178,7 +184,8 @@ impl NotificationManager {
|
|||
|
||||
// Emit notification event to frontend
|
||||
if let Some(app_handle) = self.app_handle.read().await.as_ref() {
|
||||
app_handle.emit("notification:new", ¬ification)
|
||||
app_handle
|
||||
.emit("notification:new", ¬ification)
|
||||
.map_err(|e| format!("Failed to emit notification event: {}", e))?;
|
||||
}
|
||||
|
||||
|
|
@ -193,13 +200,11 @@ impl NotificationManager {
|
|||
notification_type: NotificationType,
|
||||
) -> Result<(), String> {
|
||||
let app_handle_guard = self.app_handle.read().await;
|
||||
let app_handle = app_handle_guard.as_ref()
|
||||
let app_handle = app_handle_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "App handle not set".to_string())?;
|
||||
|
||||
let mut builder = app_handle.notification()
|
||||
.builder()
|
||||
.title(title)
|
||||
.body(body);
|
||||
let mut builder = app_handle.notification().builder().title(title).body(body);
|
||||
|
||||
// Set icon based on notification type
|
||||
let icon = match notification_type {
|
||||
|
|
@ -217,7 +222,8 @@ impl NotificationManager {
|
|||
builder = builder.icon(icon_str);
|
||||
}
|
||||
|
||||
builder.show()
|
||||
builder
|
||||
.show()
|
||||
.map_err(|e| format!("Failed to show notification: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -228,13 +234,13 @@ impl NotificationManager {
|
|||
let mut notifications = self.notifications.write().await;
|
||||
if let Some(notification) = notifications.get_mut(notification_id) {
|
||||
notification.read = true;
|
||||
|
||||
|
||||
// Update history
|
||||
let mut history = self.notification_history.write().await;
|
||||
if let Some(hist_notification) = history.iter_mut().find(|n| n.id == notification_id) {
|
||||
hist_notification.read = true;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Notification not found".to_string())
|
||||
|
|
@ -247,12 +253,12 @@ impl NotificationManager {
|
|||
for notification in notifications.values_mut() {
|
||||
notification.read = true;
|
||||
}
|
||||
|
||||
|
||||
let mut history = self.notification_history.write().await;
|
||||
for notification in history.iter_mut() {
|
||||
notification.read = true;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +269,9 @@ impl NotificationManager {
|
|||
|
||||
/// Get unread notification count
|
||||
pub async fn get_unread_count(&self) -> usize {
|
||||
self.notifications.read().await
|
||||
self.notifications
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.filter(|n| !n.read)
|
||||
.count()
|
||||
|
|
@ -311,50 +319,69 @@ impl NotificationManager {
|
|||
body,
|
||||
vec![],
|
||||
HashMap::new(),
|
||||
).await
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Show update available notification
|
||||
pub async fn notify_update_available(&self, version: &str, download_url: &str) -> Result<String, String> {
|
||||
pub async fn notify_update_available(
|
||||
&self,
|
||||
version: &str,
|
||||
download_url: &str,
|
||||
) -> Result<String, String> {
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert("version".to_string(), serde_json::Value::String(version.to_string()));
|
||||
metadata.insert("download_url".to_string(), serde_json::Value::String(download_url.to_string()));
|
||||
metadata.insert(
|
||||
"version".to_string(),
|
||||
serde_json::Value::String(version.to_string()),
|
||||
);
|
||||
metadata.insert(
|
||||
"download_url".to_string(),
|
||||
serde_json::Value::String(download_url.to_string()),
|
||||
);
|
||||
|
||||
self.show_notification(
|
||||
NotificationType::UpdateAvailable,
|
||||
NotificationPriority::High,
|
||||
"Update Available".to_string(),
|
||||
format!("VibeTunnel {} is now available. Click to download.", version),
|
||||
vec![
|
||||
NotificationAction {
|
||||
id: "download".to_string(),
|
||||
label: "Download".to_string(),
|
||||
action_type: "open_url".to_string(),
|
||||
}
|
||||
],
|
||||
format!(
|
||||
"VibeTunnel {} is now available. Click to download.",
|
||||
version
|
||||
),
|
||||
vec![NotificationAction {
|
||||
id: "download".to_string(),
|
||||
label: "Download".to_string(),
|
||||
action_type: "open_url".to_string(),
|
||||
}],
|
||||
metadata,
|
||||
).await
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Show permission required notification
|
||||
pub async fn notify_permission_required(&self, permission: &str, reason: &str) -> Result<String, String> {
|
||||
pub async fn notify_permission_required(
|
||||
&self,
|
||||
permission: &str,
|
||||
reason: &str,
|
||||
) -> Result<String, String> {
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert("permission".to_string(), serde_json::Value::String(permission.to_string()));
|
||||
metadata.insert(
|
||||
"permission".to_string(),
|
||||
serde_json::Value::String(permission.to_string()),
|
||||
);
|
||||
|
||||
self.show_notification(
|
||||
NotificationType::PermissionRequired,
|
||||
NotificationPriority::High,
|
||||
"Permission Required".to_string(),
|
||||
format!("{} permission is required: {}", permission, reason),
|
||||
vec![
|
||||
NotificationAction {
|
||||
id: "grant".to_string(),
|
||||
label: "Grant Permission".to_string(),
|
||||
action_type: "request_permission".to_string(),
|
||||
}
|
||||
],
|
||||
vec![NotificationAction {
|
||||
id: "grant".to_string(),
|
||||
label: "Grant Permission".to_string(),
|
||||
action_type: "request_permission".to_string(),
|
||||
}],
|
||||
metadata,
|
||||
).await
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Show error notification
|
||||
|
|
@ -366,7 +393,8 @@ impl NotificationManager {
|
|||
error_message.to_string(),
|
||||
vec![],
|
||||
HashMap::new(),
|
||||
).await
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Show success notification
|
||||
|
|
@ -378,6 +406,7 @@ impl NotificationManager {
|
|||
message.to_string(),
|
||||
vec![],
|
||||
HashMap::new(),
|
||||
).await
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::AppHandle;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Permission type enumeration
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
|
|
@ -75,7 +75,7 @@ impl PermissionsManager {
|
|||
app_handle: Arc::new(RwLock::new(None)),
|
||||
notification_manager: None,
|
||||
};
|
||||
|
||||
|
||||
// Initialize default permissions
|
||||
tokio::spawn({
|
||||
let permissions = manager.permissions.clone();
|
||||
|
|
@ -84,7 +84,7 @@ impl PermissionsManager {
|
|||
*permissions.write().await = default_permissions;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
|
|
@ -94,137 +94,167 @@ impl PermissionsManager {
|
|||
}
|
||||
|
||||
/// Set the notification manager
|
||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
||||
pub fn set_notification_manager(
|
||||
&mut self,
|
||||
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||
) {
|
||||
self.notification_manager = Some(notification_manager);
|
||||
}
|
||||
|
||||
/// Initialize default permissions based on platform
|
||||
fn initialize_permissions() -> HashMap<PermissionType, PermissionInfo> {
|
||||
let mut permissions = HashMap::new();
|
||||
|
||||
|
||||
// Get current platform
|
||||
let platform = std::env::consts::OS;
|
||||
|
||||
|
||||
match platform {
|
||||
"macos" => {
|
||||
permissions.insert(PermissionType::ScreenRecording, PermissionInfo {
|
||||
permission_type: PermissionType::ScreenRecording,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: false,
|
||||
platform_specific: true,
|
||||
description: "Required for recording terminal sessions with system UI".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
});
|
||||
|
||||
permissions.insert(PermissionType::Accessibility, PermissionInfo {
|
||||
permission_type: PermissionType::Accessibility,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: false,
|
||||
platform_specific: true,
|
||||
description: "Required for advanced terminal integration features".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
});
|
||||
|
||||
permissions.insert(PermissionType::NotificationAccess, PermissionInfo {
|
||||
permission_type: PermissionType::NotificationAccess,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: false,
|
||||
platform_specific: true,
|
||||
description: "Required to show system notifications".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
});
|
||||
permissions.insert(
|
||||
PermissionType::ScreenRecording,
|
||||
PermissionInfo {
|
||||
permission_type: PermissionType::ScreenRecording,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: false,
|
||||
platform_specific: true,
|
||||
description: "Required for recording terminal sessions with system UI"
|
||||
.to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
},
|
||||
);
|
||||
|
||||
permissions.insert(
|
||||
PermissionType::Accessibility,
|
||||
PermissionInfo {
|
||||
permission_type: PermissionType::Accessibility,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: false,
|
||||
platform_specific: true,
|
||||
description: "Required for advanced terminal integration features"
|
||||
.to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
},
|
||||
);
|
||||
|
||||
permissions.insert(
|
||||
PermissionType::NotificationAccess,
|
||||
PermissionInfo {
|
||||
permission_type: PermissionType::NotificationAccess,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: false,
|
||||
platform_specific: true,
|
||||
description: "Required to show system notifications".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
"windows" => {
|
||||
permissions.insert(PermissionType::TerminalAccess, PermissionInfo {
|
||||
permission_type: PermissionType::TerminalAccess,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: true,
|
||||
platform_specific: true,
|
||||
description: "Required to create and manage terminal sessions".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
});
|
||||
|
||||
permissions.insert(PermissionType::AutoStart, PermissionInfo {
|
||||
permission_type: PermissionType::AutoStart,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: false,
|
||||
platform_specific: true,
|
||||
description: "Required to start VibeTunnel with Windows".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
});
|
||||
permissions.insert(
|
||||
PermissionType::TerminalAccess,
|
||||
PermissionInfo {
|
||||
permission_type: PermissionType::TerminalAccess,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: true,
|
||||
platform_specific: true,
|
||||
description: "Required to create and manage terminal sessions".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
},
|
||||
);
|
||||
|
||||
permissions.insert(
|
||||
PermissionType::AutoStart,
|
||||
PermissionInfo {
|
||||
permission_type: PermissionType::AutoStart,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
required: false,
|
||||
platform_specific: true,
|
||||
description: "Required to start VibeTunnel with Windows".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
"linux" => {
|
||||
permissions.insert(PermissionType::FileSystemFull, PermissionInfo {
|
||||
permission_type: PermissionType::FileSystemFull,
|
||||
status: PermissionStatus::Granted,
|
||||
required: true,
|
||||
platform_specific: false,
|
||||
description: "Required for saving recordings and configurations".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
});
|
||||
permissions.insert(
|
||||
PermissionType::FileSystemFull,
|
||||
PermissionInfo {
|
||||
permission_type: PermissionType::FileSystemFull,
|
||||
status: PermissionStatus::Granted,
|
||||
required: true,
|
||||
platform_specific: false,
|
||||
description: "Required for saving recordings and configurations"
|
||||
.to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
// Add common permissions
|
||||
permissions.insert(PermissionType::NetworkAccess, PermissionInfo {
|
||||
permission_type: PermissionType::NetworkAccess,
|
||||
status: PermissionStatus::Granted,
|
||||
required: true,
|
||||
platform_specific: false,
|
||||
description: "Required for web server and remote access features".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
});
|
||||
|
||||
permissions.insert(PermissionType::FileSystemRestricted, PermissionInfo {
|
||||
permission_type: PermissionType::FileSystemRestricted,
|
||||
status: PermissionStatus::Granted,
|
||||
required: true,
|
||||
platform_specific: false,
|
||||
description: "Required for basic application functionality".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
});
|
||||
|
||||
permissions.insert(
|
||||
PermissionType::NetworkAccess,
|
||||
PermissionInfo {
|
||||
permission_type: PermissionType::NetworkAccess,
|
||||
status: PermissionStatus::Granted,
|
||||
required: true,
|
||||
platform_specific: false,
|
||||
description: "Required for web server and remote access features".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
},
|
||||
);
|
||||
|
||||
permissions.insert(
|
||||
PermissionType::FileSystemRestricted,
|
||||
PermissionInfo {
|
||||
permission_type: PermissionType::FileSystemRestricted,
|
||||
status: PermissionStatus::Granted,
|
||||
required: true,
|
||||
platform_specific: false,
|
||||
description: "Required for basic application functionality".to_string(),
|
||||
last_checked: None,
|
||||
request_count: 0,
|
||||
},
|
||||
);
|
||||
|
||||
permissions
|
||||
}
|
||||
|
||||
/// Check all permissions
|
||||
pub async fn check_all_permissions(&self) -> Vec<PermissionInfo> {
|
||||
let mut permissions = self.permissions.write().await;
|
||||
|
||||
|
||||
for (permission_type, info) in permissions.iter_mut() {
|
||||
info.status = self.check_permission_internal(*permission_type).await;
|
||||
info.last_checked = Some(Utc::now());
|
||||
}
|
||||
|
||||
|
||||
permissions.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Check specific permission
|
||||
pub async fn check_permission(&self, permission_type: PermissionType) -> PermissionStatus {
|
||||
let status = self.check_permission_internal(permission_type).await;
|
||||
|
||||
|
||||
// Update stored status
|
||||
if let Some(info) = self.permissions.write().await.get_mut(&permission_type) {
|
||||
info.status = status;
|
||||
info.last_checked = Some(Utc::now());
|
||||
}
|
||||
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
/// Internal permission checking logic
|
||||
async fn check_permission_internal(&self, permission_type: PermissionType) -> PermissionStatus {
|
||||
let platform = std::env::consts::OS;
|
||||
|
||||
|
||||
match (platform, permission_type) {
|
||||
#[cfg(target_os = "macos")]
|
||||
("macos", PermissionType::ScreenRecording) => {
|
||||
|
|
@ -251,14 +281,17 @@ impl PermissionsManager {
|
|||
}
|
||||
|
||||
/// Request permission
|
||||
pub async fn request_permission(&self, permission_type: PermissionType) -> Result<PermissionRequestResult, String> {
|
||||
pub async fn request_permission(
|
||||
&self,
|
||||
permission_type: PermissionType,
|
||||
) -> Result<PermissionRequestResult, String> {
|
||||
// Update request count
|
||||
if let Some(info) = self.permissions.write().await.get_mut(&permission_type) {
|
||||
info.request_count += 1;
|
||||
}
|
||||
|
||||
|
||||
let platform = std::env::consts::OS;
|
||||
|
||||
|
||||
match (platform, permission_type) {
|
||||
#[cfg(target_os = "macos")]
|
||||
("macos", PermissionType::ScreenRecording) => {
|
||||
|
|
@ -283,7 +316,10 @@ impl PermissionsManager {
|
|||
}
|
||||
|
||||
/// Get permission info
|
||||
pub async fn get_permission_info(&self, permission_type: PermissionType) -> Option<PermissionInfo> {
|
||||
pub async fn get_permission_info(
|
||||
&self,
|
||||
permission_type: PermissionType,
|
||||
) -> Option<PermissionInfo> {
|
||||
self.permissions.read().await.get(&permission_type).cloned()
|
||||
}
|
||||
|
||||
|
|
@ -294,7 +330,9 @@ impl PermissionsManager {
|
|||
|
||||
/// Get required permissions
|
||||
pub async fn get_required_permissions(&self) -> Vec<PermissionInfo> {
|
||||
self.permissions.read().await
|
||||
self.permissions
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.filter(|info| info.required)
|
||||
.cloned()
|
||||
|
|
@ -303,7 +341,9 @@ impl PermissionsManager {
|
|||
|
||||
/// Get missing required permissions
|
||||
pub async fn get_missing_required_permissions(&self) -> Vec<PermissionInfo> {
|
||||
self.permissions.read().await
|
||||
self.permissions
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.filter(|info| info.required && info.status != PermissionStatus::Granted)
|
||||
.cloned()
|
||||
|
|
@ -312,15 +352,21 @@ impl PermissionsManager {
|
|||
|
||||
/// Check if all required permissions are granted
|
||||
pub async fn all_required_permissions_granted(&self) -> bool {
|
||||
!self.permissions.read().await
|
||||
!self
|
||||
.permissions
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.any(|info| info.required && info.status != PermissionStatus::Granted)
|
||||
}
|
||||
|
||||
/// Open system settings for permission
|
||||
pub async fn open_system_settings(&self, permission_type: PermissionType) -> Result<(), String> {
|
||||
pub async fn open_system_settings(
|
||||
&self,
|
||||
permission_type: PermissionType,
|
||||
) -> Result<(), String> {
|
||||
let platform = std::env::consts::OS;
|
||||
|
||||
|
||||
match (platform, permission_type) {
|
||||
#[cfg(target_os = "macos")]
|
||||
("macos", PermissionType::ScreenRecording) => {
|
||||
|
|
@ -335,9 +381,7 @@ impl PermissionsManager {
|
|||
self.open_notification_settings_macos().await
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
("windows", PermissionType::AutoStart) => {
|
||||
self.open_startup_settings_windows().await
|
||||
}
|
||||
("windows", PermissionType::AutoStart) => self.open_startup_settings_windows().await,
|
||||
_ => Err("No system settings available for this permission".to_string()),
|
||||
}
|
||||
}
|
||||
|
|
@ -347,12 +391,12 @@ impl PermissionsManager {
|
|||
async fn check_screen_recording_permission_macos(&self) -> PermissionStatus {
|
||||
// Use CGDisplayStream API to check screen recording permission
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
let output = Command::new("osascript")
|
||||
.arg("-e")
|
||||
.arg("tell application \"System Events\" to get properties")
|
||||
.output();
|
||||
|
||||
|
||||
match output {
|
||||
Ok(output) if output.status.success() => PermissionStatus::Granted,
|
||||
_ => PermissionStatus::NotDetermined,
|
||||
|
|
@ -360,22 +404,28 @@ impl PermissionsManager {
|
|||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn request_screen_recording_permission_macos(&self) -> Result<PermissionRequestResult, String> {
|
||||
async fn request_screen_recording_permission_macos(
|
||||
&self,
|
||||
) -> Result<PermissionRequestResult, String> {
|
||||
// Show notification about needing to grant permission
|
||||
if let Some(notification_manager) = &self.notification_manager {
|
||||
let _ = notification_manager.notify_permission_required(
|
||||
"Screen Recording",
|
||||
"VibeTunnel needs screen recording permission to capture terminal sessions"
|
||||
).await;
|
||||
let _ = notification_manager
|
||||
.notify_permission_required(
|
||||
"Screen Recording",
|
||||
"VibeTunnel needs screen recording permission to capture terminal sessions",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
|
||||
// Open system preferences
|
||||
let _ = self.open_screen_recording_settings_macos().await;
|
||||
|
||||
|
||||
Ok(PermissionRequestResult {
|
||||
permission_type: PermissionType::ScreenRecording,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
message: Some("Please grant screen recording permission in System Settings".to_string()),
|
||||
message: Some(
|
||||
"Please grant screen recording permission in System Settings".to_string(),
|
||||
),
|
||||
requires_restart: true,
|
||||
requires_system_settings: true,
|
||||
})
|
||||
|
|
@ -384,24 +434,24 @@ impl PermissionsManager {
|
|||
#[cfg(target_os = "macos")]
|
||||
async fn open_screen_recording_settings_macos(&self) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
Command::new("open")
|
||||
.arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open system preferences: {}", e))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn check_accessibility_permission_macos(&self) -> PermissionStatus {
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
let output = Command::new("osascript")
|
||||
.arg("-e")
|
||||
.arg("tell application \"System Events\" to get UI elements enabled")
|
||||
.output();
|
||||
|
||||
|
||||
match output {
|
||||
Ok(output) if output.status.success() => {
|
||||
let result = String::from_utf8_lossy(&output.stdout);
|
||||
|
|
@ -416,9 +466,11 @@ impl PermissionsManager {
|
|||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn request_accessibility_permission_macos(&self) -> Result<PermissionRequestResult, String> {
|
||||
async fn request_accessibility_permission_macos(
|
||||
&self,
|
||||
) -> Result<PermissionRequestResult, String> {
|
||||
let _ = self.open_accessibility_settings_macos().await;
|
||||
|
||||
|
||||
Ok(PermissionRequestResult {
|
||||
permission_type: PermissionType::Accessibility,
|
||||
status: PermissionStatus::NotDetermined,
|
||||
|
|
@ -431,12 +483,12 @@ impl PermissionsManager {
|
|||
#[cfg(target_os = "macos")]
|
||||
async fn open_accessibility_settings_macos(&self) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
Command::new("open")
|
||||
.arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open system preferences: {}", e))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -447,7 +499,9 @@ impl PermissionsManager {
|
|||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn request_notification_permission_macos(&self) -> Result<PermissionRequestResult, String> {
|
||||
async fn request_notification_permission_macos(
|
||||
&self,
|
||||
) -> Result<PermissionRequestResult, String> {
|
||||
Ok(PermissionRequestResult {
|
||||
permission_type: PermissionType::NotificationAccess,
|
||||
status: PermissionStatus::Granted,
|
||||
|
|
@ -460,12 +514,12 @@ impl PermissionsManager {
|
|||
#[cfg(target_os = "macos")]
|
||||
async fn open_notification_settings_macos(&self) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
Command::new("open")
|
||||
.arg("x-apple.systempreferences:com.apple.preference.notifications")
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open system preferences: {}", e))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -479,7 +533,7 @@ impl PermissionsManager {
|
|||
async fn check_auto_start_permission_windows(&self) -> PermissionStatus {
|
||||
// Check if auto-start is configured
|
||||
use crate::auto_launch;
|
||||
|
||||
|
||||
match auto_launch::get_auto_launch().await {
|
||||
Ok(enabled) => {
|
||||
if enabled {
|
||||
|
|
@ -495,24 +549,29 @@ impl PermissionsManager {
|
|||
#[cfg(target_os = "windows")]
|
||||
async fn open_startup_settings_windows(&self) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
Command::new("cmd")
|
||||
.args(&["/c", "start", "ms-settings:startupapps"])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open startup settings: {}", e))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show permission required notification
|
||||
pub async fn notify_permission_required(&self, permission_info: &PermissionInfo) -> Result<(), String> {
|
||||
pub async fn notify_permission_required(
|
||||
&self,
|
||||
permission_info: &PermissionInfo,
|
||||
) -> Result<(), String> {
|
||||
if let Some(notification_manager) = &self.notification_manager {
|
||||
notification_manager.notify_permission_required(
|
||||
&format!("{:?}", permission_info.permission_type),
|
||||
&permission_info.description
|
||||
).await?;
|
||||
notification_manager
|
||||
.notify_permission_required(
|
||||
&format!("{:?}", permission_info.permission_type),
|
||||
&permission_info.description,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -526,4 +585,4 @@ pub struct PermissionStats {
|
|||
pub required_permissions: usize,
|
||||
pub missing_required: usize,
|
||||
pub platform: String,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::process::Command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::TcpListener;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tracing::{info, error};
|
||||
use std::process::Command;
|
||||
use tracing::{error, info};
|
||||
|
||||
/// Information about a process using a port
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -20,11 +20,16 @@ impl ProcessDetails {
|
|||
}
|
||||
self.name.contains("vibetunnel") || self.name.contains("VibeTunnel")
|
||||
}
|
||||
|
||||
|
||||
/// Check if this is one of our managed servers
|
||||
pub fn is_managed_server(&self) -> bool {
|
||||
self.name == "vibetunnel" ||
|
||||
self.name.contains("node") && self.path.as_ref().map(|p| p.contains("VibeTunnel")).unwrap_or(false)
|
||||
self.name == "vibetunnel"
|
||||
|| self.name.contains("node")
|
||||
&& self
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|p| p.contains("VibeTunnel"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,25 +60,25 @@ impl PortConflictResolver {
|
|||
pub async fn is_port_available(port: u16) -> bool {
|
||||
TcpListener::bind(format!("127.0.0.1:{}", port)).is_ok()
|
||||
}
|
||||
|
||||
|
||||
/// Detect what process is using a port
|
||||
pub async fn detect_conflict(port: u16) -> Option<PortConflict> {
|
||||
// First check if port is actually in use
|
||||
if Self::is_port_available(port).await {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
// Platform-specific conflict detection
|
||||
#[cfg(target_os = "macos")]
|
||||
return Self::detect_conflict_macos(port).await;
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return Self::detect_conflict_linux(port).await;
|
||||
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return Self::detect_conflict_windows(port).await;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn detect_conflict_macos(port: u16) -> Option<PortConflict> {
|
||||
// Use lsof to find process using the port
|
||||
|
|
@ -81,23 +86,23 @@ impl PortConflictResolver {
|
|||
.args(&["-i", &format!(":{}", port), "-n", "-P", "-F"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let process_info = Self::parse_lsof_output(&stdout)?;
|
||||
|
||||
|
||||
// Get root process
|
||||
let root_process = Self::find_root_process(&process_info).await;
|
||||
|
||||
|
||||
// Find alternative ports
|
||||
let alternatives = Self::find_available_ports(port, 3).await;
|
||||
|
||||
|
||||
// Determine action
|
||||
let action = Self::determine_action(&process_info, &root_process);
|
||||
|
||||
|
||||
Some(PortConflict {
|
||||
port,
|
||||
process: process_info,
|
||||
|
|
@ -106,7 +111,7 @@ impl PortConflictResolver {
|
|||
alternative_ports: alternatives,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn detect_conflict_linux(port: u16) -> Option<PortConflict> {
|
||||
// Try lsof first
|
||||
|
|
@ -120,7 +125,7 @@ impl PortConflictResolver {
|
|||
let root_process = Self::find_root_process(&process_info).await;
|
||||
let alternatives = Self::find_available_ports(port, 3).await;
|
||||
let action = Self::determine_action(&process_info, &root_process);
|
||||
|
||||
|
||||
return Some(PortConflict {
|
||||
port,
|
||||
process: process_info,
|
||||
|
|
@ -131,12 +136,9 @@ impl PortConflictResolver {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback to netstat
|
||||
if let Ok(output) = Command::new("netstat")
|
||||
.args(&["-tulpn"])
|
||||
.output()
|
||||
{
|
||||
if let Ok(output) = Command::new("netstat").args(&["-tulpn"]).output() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
// Parse netstat output (simplified)
|
||||
for line in stdout.lines() {
|
||||
|
|
@ -145,17 +147,18 @@ impl PortConflictResolver {
|
|||
if let Some(pid_part) = line.split_whitespace().last() {
|
||||
if let Some(pid_str) = pid_part.split('/').next() {
|
||||
if let Ok(pid) = pid_str.parse::<u32>() {
|
||||
let name = pid_part.split('/').nth(1).unwrap_or("unknown").to_string();
|
||||
let name =
|
||||
pid_part.split('/').nth(1).unwrap_or("unknown").to_string();
|
||||
let process_info = ProcessDetails {
|
||||
pid,
|
||||
name,
|
||||
path: None,
|
||||
parent_pid: None,
|
||||
};
|
||||
|
||||
|
||||
let alternatives = Self::find_available_ports(port, 3).await;
|
||||
let action = Self::determine_action(&process_info, &None);
|
||||
|
||||
|
||||
return Some(PortConflict {
|
||||
port,
|
||||
process: process_info,
|
||||
|
|
@ -169,10 +172,10 @@ impl PortConflictResolver {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn detect_conflict_windows(port: u16) -> Option<PortConflict> {
|
||||
// Use netstat to find process using the port
|
||||
|
|
@ -180,9 +183,9 @@ impl PortConflictResolver {
|
|||
.args(&["-ano", "-p", "tcp"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
|
||||
// Parse netstat output to find the PID
|
||||
for line in stdout.lines() {
|
||||
if line.contains(&format!(":{}", port)) && line.contains("LISTENING") {
|
||||
|
|
@ -205,10 +208,10 @@ impl PortConflictResolver {
|
|||
path: None,
|
||||
parent_pid: None,
|
||||
};
|
||||
|
||||
|
||||
let alternatives = Self::find_available_ports(port, 3).await;
|
||||
let action = Self::determine_action(&process_info, &None);
|
||||
|
||||
|
||||
return Some(PortConflict {
|
||||
port,
|
||||
process: process_info,
|
||||
|
|
@ -223,16 +226,16 @@ impl PortConflictResolver {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
/// Parse lsof output
|
||||
fn parse_lsof_output(output: &str) -> Option<ProcessDetails> {
|
||||
let mut pid: Option<u32> = None;
|
||||
let mut name: Option<String> = None;
|
||||
let mut ppid: Option<u32> = None;
|
||||
|
||||
|
||||
// Parse lsof field output format
|
||||
for line in output.lines() {
|
||||
if line.starts_with('p') {
|
||||
|
|
@ -243,11 +246,11 @@ impl PortConflictResolver {
|
|||
ppid = line[1..].parse().ok();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let (Some(pid), Some(name)) = (pid, name) {
|
||||
// Get additional process info
|
||||
let path = Self::get_process_path(pid);
|
||||
|
||||
|
||||
Some(ProcessDetails {
|
||||
pid,
|
||||
name,
|
||||
|
|
@ -258,7 +261,7 @@ impl PortConflictResolver {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get process path
|
||||
fn get_process_path(pid: u32) -> Option<String> {
|
||||
#[cfg(unix)]
|
||||
|
|
@ -267,29 +270,27 @@ impl PortConflictResolver {
|
|||
.args(&["-p", &pid.to_string(), "-o", "comm="])
|
||||
.output()
|
||||
{
|
||||
let path = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !path.is_empty() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
/// Find root process
|
||||
async fn find_root_process(process: &ProcessDetails) -> Option<ProcessDetails> {
|
||||
let mut current = process.clone();
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
|
||||
|
||||
while let Some(parent_pid) = current.parent_pid {
|
||||
if parent_pid <= 1 || visited.contains(&parent_pid) {
|
||||
break;
|
||||
}
|
||||
visited.insert(current.pid);
|
||||
|
||||
|
||||
// Get parent process info
|
||||
if let Some(parent_info) = Self::get_process_info(parent_pid).await {
|
||||
// If parent is VibeTunnel, it's our root
|
||||
|
|
@ -301,10 +302,10 @@ impl PortConflictResolver {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
/// Get process info by PID
|
||||
async fn get_process_info(pid: u32) -> Option<ProcessDetails> {
|
||||
#[cfg(unix)]
|
||||
|
|
@ -315,13 +316,13 @@ impl PortConflictResolver {
|
|||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let parts: Vec<&str> = stdout.trim().split_whitespace().collect();
|
||||
|
||||
|
||||
if parts.len() >= 3 {
|
||||
let pid = parts[0].parse().ok()?;
|
||||
let ppid = parts[1].parse().ok();
|
||||
let name = parts[2..].join(" ");
|
||||
let path = Self::get_process_path(pid);
|
||||
|
||||
|
||||
return Some(ProcessDetails {
|
||||
pid,
|
||||
name,
|
||||
|
|
@ -331,22 +332,22 @@ impl PortConflictResolver {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Windows implementation would use WMI or similar
|
||||
// For now, return None
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
/// Find available ports near a given port
|
||||
async fn find_available_ports(near_port: u16, count: usize) -> Vec<u16> {
|
||||
let mut available_ports = Vec::new();
|
||||
let start = near_port.saturating_sub(10).max(1024);
|
||||
let end = near_port.saturating_add(100).min(65535);
|
||||
|
||||
|
||||
for port in start..=end {
|
||||
if port != near_port && Self::is_port_available(port).await {
|
||||
available_ports.push(port);
|
||||
|
|
@ -355,12 +356,15 @@ impl PortConflictResolver {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
available_ports
|
||||
}
|
||||
|
||||
|
||||
/// Determine action for conflict resolution
|
||||
fn determine_action(process: &ProcessDetails, root_process: &Option<ProcessDetails>) -> ConflictAction {
|
||||
fn determine_action(
|
||||
process: &ProcessDetails,
|
||||
root_process: &Option<ProcessDetails>,
|
||||
) -> ConflictAction {
|
||||
// If it's our managed server, kill it
|
||||
if process.is_managed_server() {
|
||||
return ConflictAction::KillOurInstance {
|
||||
|
|
@ -368,7 +372,7 @@ impl PortConflictResolver {
|
|||
process_name: process.name.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// If root process is VibeTunnel, kill the whole app
|
||||
if let Some(root) = root_process {
|
||||
if root.is_vibetunnel() {
|
||||
|
|
@ -378,7 +382,7 @@ impl PortConflictResolver {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If the process itself is VibeTunnel
|
||||
if process.is_vibetunnel() {
|
||||
return ConflictAction::KillOurInstance {
|
||||
|
|
@ -386,43 +390,46 @@ impl PortConflictResolver {
|
|||
process_name: process.name.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, it's an external app
|
||||
ConflictAction::ReportExternalApp {
|
||||
name: process.name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Resolve a port conflict
|
||||
pub async fn resolve_conflict(conflict: &PortConflict) -> Result<(), String> {
|
||||
match &conflict.suggested_action {
|
||||
ConflictAction::KillOurInstance { pid, process_name } => {
|
||||
info!("Killing conflicting process: {} (PID: {})", process_name, pid);
|
||||
|
||||
info!(
|
||||
"Killing conflicting process: {} (PID: {})",
|
||||
process_name, pid
|
||||
);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let output = Command::new("kill")
|
||||
.args(&["-9", &pid.to_string()])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute kill command: {}", e))?;
|
||||
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!("Failed to kill process {}", pid));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let output = Command::new("taskkill")
|
||||
.args(&["/F", "/PID", &pid.to_string()])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute taskkill command: {}", e))?;
|
||||
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!("Failed to kill process {}", pid));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wait for port to be released
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
Ok(())
|
||||
|
|
@ -433,38 +440,41 @@ impl PortConflictResolver {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Force kill a process
|
||||
pub async fn force_kill_process(conflict: &PortConflict) -> Result<(), String> {
|
||||
info!("Force killing process: {} (PID: {})", conflict.process.name, conflict.process.pid);
|
||||
|
||||
info!(
|
||||
"Force killing process: {} (PID: {})",
|
||||
conflict.process.name, conflict.process.pid
|
||||
);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let output = Command::new("kill")
|
||||
.args(&["-9", &conflict.process.pid.to_string()])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute kill command: {}", e))?;
|
||||
|
||||
|
||||
if !output.status.success() {
|
||||
error!("Failed to kill process with regular permissions");
|
||||
return Err(format!("Failed to kill process {}", conflict.process.pid));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let output = Command::new("taskkill")
|
||||
.args(&["/F", "/PID", &conflict.process.pid.to_string()])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute taskkill command: {}", e))?;
|
||||
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!("Failed to kill process {}", conflict.process.pid));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wait for port to be released
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,28 @@
|
|||
use axum::{
|
||||
Router,
|
||||
routing::{get, post, delete},
|
||||
response::IntoResponse,
|
||||
extract::{ws::WebSocketUpgrade, Path, State as AxumState, Query},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
middleware,
|
||||
};
|
||||
use axum::extract::ws::{WebSocket, Message};
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||
use axum::{
|
||||
extract::{ws::WebSocketUpgrade, Path, Query, State as AxumState},
|
||||
http::StatusCode,
|
||||
middleware,
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use futures::sink::SinkExt;
|
||||
use futures::stream::{Stream, StreamExt as FuturesStreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::Infallible;
|
||||
use tokio::time::{interval, Duration};
|
||||
use tower_http::cors::{CorsLayer, Any};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use futures::sink::SinkExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{info, error, debug};
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
use tokio::time::{interval, Duration};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::terminal::TerminalManager;
|
||||
use crate::auth::{AuthConfig, auth_middleware, check_auth, login};
|
||||
use crate::auth::{auth_middleware, check_auth, login, AuthConfig};
|
||||
use crate::session_monitor::SessionMonitor;
|
||||
use crate::terminal::TerminalManager;
|
||||
|
||||
// Combined app state for Axum
|
||||
#[derive(Clone)]
|
||||
|
|
@ -99,8 +98,11 @@ impl HttpServer {
|
|||
pub fn port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
|
||||
pub fn new(terminal_manager: Arc<TerminalManager>, session_monitor: Arc<SessionMonitor>) -> Self {
|
||||
|
||||
pub fn new(
|
||||
terminal_manager: Arc<TerminalManager>,
|
||||
session_monitor: Arc<SessionMonitor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
terminal_manager,
|
||||
auth_config: Arc::new(AuthConfig::new(false, None)),
|
||||
|
|
@ -110,8 +112,12 @@ impl HttpServer {
|
|||
handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_auth(terminal_manager: Arc<TerminalManager>, session_monitor: Arc<SessionMonitor>, auth_config: AuthConfig) -> Self {
|
||||
|
||||
pub fn with_auth(
|
||||
terminal_manager: Arc<TerminalManager>,
|
||||
session_monitor: Arc<SessionMonitor>,
|
||||
auth_config: AuthConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
terminal_manager,
|
||||
auth_config: Arc::new(auth_config),
|
||||
|
|
@ -122,71 +128,69 @@ impl HttpServer {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn start(&mut self) -> Result<u16, String> {
|
||||
self.start_with_mode("localhost").await
|
||||
}
|
||||
|
||||
|
||||
pub async fn start_with_mode(&mut self, mode: &str) -> Result<u16, String> {
|
||||
// Determine bind address based on mode
|
||||
let bind_addr = match mode {
|
||||
"localhost" => "127.0.0.1:0",
|
||||
"network" => "0.0.0.0:0", // Bind to all interfaces
|
||||
"network" => "0.0.0.0:0", // Bind to all interfaces
|
||||
_ => "127.0.0.1:0",
|
||||
};
|
||||
|
||||
|
||||
// Find available port
|
||||
let listener = TcpListener::bind(bind_addr)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to bind to {}: {}", bind_addr, e))?;
|
||||
|
||||
let addr = listener.local_addr()
|
||||
|
||||
let addr = listener
|
||||
.local_addr()
|
||||
.map_err(|e| format!("Failed to get local address: {}", e))?;
|
||||
|
||||
|
||||
self.port = addr.port();
|
||||
|
||||
|
||||
info!("Starting HTTP server on port {}", self.port);
|
||||
|
||||
|
||||
// Create shutdown channel
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
self.shutdown_tx = Some(shutdown_tx);
|
||||
|
||||
|
||||
// Build router
|
||||
let app = self.build_router();
|
||||
|
||||
|
||||
// Start server
|
||||
let handle = tokio::spawn(async move {
|
||||
let server = axum::serve(listener, app)
|
||||
.with_graceful_shutdown(async {
|
||||
let _ = shutdown_rx.await;
|
||||
info!("Graceful shutdown initiated");
|
||||
});
|
||||
|
||||
let server = axum::serve(listener, app).with_graceful_shutdown(async {
|
||||
let _ = shutdown_rx.await;
|
||||
info!("Graceful shutdown initiated");
|
||||
});
|
||||
|
||||
if let Err(e) = server.await {
|
||||
error!("Server error: {}", e);
|
||||
}
|
||||
|
||||
|
||||
info!("Server task completed");
|
||||
});
|
||||
|
||||
|
||||
self.handle = Some(handle);
|
||||
|
||||
|
||||
Ok(self.port)
|
||||
}
|
||||
|
||||
|
||||
pub async fn stop(&mut self) -> Result<(), String> {
|
||||
info!("Stopping HTTP server...");
|
||||
|
||||
|
||||
// Send shutdown signal
|
||||
if let Some(tx) = self.shutdown_tx.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
|
||||
|
||||
// Wait for server task to complete
|
||||
if let Some(handle) = self.handle.take() {
|
||||
match tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(10),
|
||||
handle
|
||||
).await {
|
||||
match tokio::time::timeout(tokio::time::Duration::from_secs(10), handle).await {
|
||||
Ok(Ok(())) => {
|
||||
info!("HTTP server stopped gracefully");
|
||||
}
|
||||
|
|
@ -200,26 +204,26 @@ impl HttpServer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn build_router(&self) -> Router {
|
||||
let app_state = AppState {
|
||||
terminal_manager: self.terminal_manager.clone(),
|
||||
auth_config: self.auth_config.clone(),
|
||||
session_monitor: self.session_monitor.clone(),
|
||||
};
|
||||
|
||||
|
||||
// Don't serve static files in Tauri - the frontend is served by Tauri itself
|
||||
// This server is only for the terminal API
|
||||
|
||||
|
||||
// Create auth routes that use auth config
|
||||
let auth_routes = Router::new()
|
||||
.route("/api/auth/check", get(check_auth))
|
||||
.route("/api/auth/login", post(login))
|
||||
.with_state(app_state.auth_config.clone());
|
||||
|
||||
|
||||
// Create protected routes that use full app state
|
||||
let protected_routes = Router::new()
|
||||
.route("/api/sessions", get(list_sessions).post(create_session))
|
||||
|
|
@ -243,17 +247,19 @@ impl HttpServer {
|
|||
.route("/api/cleanup-exited", post(cleanup_exited))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
app_state.auth_config.clone(),
|
||||
auth_middleware
|
||||
auth_middleware,
|
||||
))
|
||||
.with_state(app_state);
|
||||
|
||||
|
||||
Router::new()
|
||||
.merge(auth_routes)
|
||||
.merge(protected_routes)
|
||||
.layer(CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any))
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,14 +268,17 @@ async fn list_sessions(
|
|||
AxumState(state): AxumState<AppState>,
|
||||
) -> Result<Json<Vec<SessionInfo>>, StatusCode> {
|
||||
let sessions = state.terminal_manager.list_sessions().await;
|
||||
|
||||
let session_infos: Vec<SessionInfo> = sessions.into_iter().map(|s| SessionInfo {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
status: "running".to_string(),
|
||||
created_at: s.created_at,
|
||||
}).collect();
|
||||
|
||||
|
||||
let session_infos: Vec<SessionInfo> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionInfo {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
status: "running".to_string(),
|
||||
created_at: s.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(session_infos))
|
||||
}
|
||||
|
||||
|
|
@ -277,18 +286,22 @@ async fn create_session(
|
|||
AxumState(state): AxumState<AppState>,
|
||||
Json(req): Json<CreateSessionRequest>,
|
||||
) -> Result<Json<SessionInfo>, StatusCode> {
|
||||
let session = state.terminal_manager.create_session(
|
||||
req.name.unwrap_or_else(|| "Terminal".to_string()),
|
||||
req.rows.unwrap_or(24),
|
||||
req.cols.unwrap_or(80),
|
||||
req.cwd,
|
||||
None,
|
||||
None,
|
||||
).await.map_err(|e| {
|
||||
error!("Failed to create session: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let session = state
|
||||
.terminal_manager
|
||||
.create_session(
|
||||
req.name.unwrap_or_else(|| "Terminal".to_string()),
|
||||
req.rows.unwrap_or(24),
|
||||
req.cols.unwrap_or(80),
|
||||
req.cwd,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to create session: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(Json(SessionInfo {
|
||||
id: session.id,
|
||||
name: session.name,
|
||||
|
|
@ -302,15 +315,18 @@ async fn get_session(
|
|||
AxumState(state): AxumState<AppState>,
|
||||
) -> Result<Json<SessionInfo>, StatusCode> {
|
||||
let sessions = state.terminal_manager.list_sessions().await;
|
||||
|
||||
sessions.into_iter()
|
||||
|
||||
sessions
|
||||
.into_iter()
|
||||
.find(|s| s.id == id)
|
||||
.map(|s| Json(SessionInfo {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
status: "running".to_string(),
|
||||
created_at: s.created_at,
|
||||
}))
|
||||
.map(|s| {
|
||||
Json(SessionInfo {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
status: "running".to_string(),
|
||||
created_at: s.created_at,
|
||||
})
|
||||
})
|
||||
.ok_or(StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +334,10 @@ async fn delete_session(
|
|||
Path(id): Path<String>,
|
||||
AxumState(state): AxumState<AppState>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
state.terminal_manager.close_session(&id).await
|
||||
state
|
||||
.terminal_manager
|
||||
.close_session(&id)
|
||||
.await
|
||||
.map(|_| StatusCode::NO_CONTENT)
|
||||
.map_err(|_| StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
|
@ -334,7 +353,10 @@ async fn resize_session(
|
|||
AxumState(state): AxumState<AppState>,
|
||||
Json(req): Json<ResizeRequest>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
state.terminal_manager.resize_session(&id, req.rows, req.cols).await
|
||||
state
|
||||
.terminal_manager
|
||||
.resize_session(&id, req.rows, req.cols)
|
||||
.await
|
||||
.map(|_| StatusCode::NO_CONTENT)
|
||||
.map_err(|_| StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
|
@ -353,22 +375,27 @@ async fn handle_terminal_websocket(
|
|||
terminal_manager: Arc<TerminalManager>,
|
||||
) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
|
||||
// Get the terminal session
|
||||
let _session = match terminal_manager.get_session(&session_id).await {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
let _ = sender.send(Message::Text("Session not found".to_string())).await;
|
||||
let _ = sender
|
||||
.send(Message::Text("Session not found".to_string()))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Spawn task to read from terminal and send to WebSocket
|
||||
let session_id_clone = session_id.clone();
|
||||
let terminal_manager_clone = terminal_manager.clone();
|
||||
let read_task = tokio::spawn(async move {
|
||||
loop {
|
||||
match terminal_manager_clone.read_from_session(&session_id_clone).await {
|
||||
match terminal_manager_clone
|
||||
.read_from_session(&session_id_clone)
|
||||
.await
|
||||
{
|
||||
Ok(data) if !data.is_empty() => {
|
||||
if sender.send(Message::Binary(data)).await.is_err() {
|
||||
break;
|
||||
|
|
@ -385,7 +412,7 @@ async fn handle_terminal_websocket(
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Handle incoming WebSocket messages
|
||||
while let Some(msg) = receiver.next().await {
|
||||
match msg {
|
||||
|
|
@ -399,15 +426,12 @@ async fn handle_terminal_websocket(
|
|||
// Handle text messages (e.g., resize commands)
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if json["type"] == "resize" {
|
||||
if let (Some(rows), Some(cols)) = (
|
||||
json["rows"].as_u64(),
|
||||
json["cols"].as_u64()
|
||||
) {
|
||||
let _ = terminal_manager.resize_session(
|
||||
&session_id,
|
||||
rows as u16,
|
||||
cols as u16
|
||||
).await;
|
||||
if let (Some(rows), Some(cols)) =
|
||||
(json["rows"].as_u64(), json["cols"].as_u64())
|
||||
{
|
||||
let _ = terminal_manager
|
||||
.resize_session(&session_id, rows as u16, cols as u16)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -420,10 +444,10 @@ async fn handle_terminal_websocket(
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Cancel the read task
|
||||
read_task.abort();
|
||||
|
||||
|
||||
debug!("WebSocket connection closed for session {}", session_id);
|
||||
}
|
||||
|
||||
|
|
@ -437,7 +461,10 @@ async fn send_input(
|
|||
AxumState(state): AxumState<AppState>,
|
||||
Json(req): Json<InputRequest>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
state.terminal_manager.write_to_session(&id, req.input.as_bytes()).await
|
||||
state
|
||||
.terminal_manager
|
||||
.write_to_session(&id, req.input.as_bytes())
|
||||
.await
|
||||
.map(|_| StatusCode::NO_CONTENT)
|
||||
.map_err(|_| StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
|
@ -448,14 +475,15 @@ async fn terminal_stream(
|
|||
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
|
||||
// Check if session exists
|
||||
let sessions = state.terminal_manager.list_sessions().await;
|
||||
let session = sessions.into_iter()
|
||||
let session = sessions
|
||||
.into_iter()
|
||||
.find(|s| s.id == id)
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
|
||||
// Create the SSE stream
|
||||
let session_id = id.clone();
|
||||
let terminal_manager = state.terminal_manager.clone();
|
||||
|
||||
|
||||
let stream = async_stream::stream! {
|
||||
// Send initial header
|
||||
let header = serde_json::json!({
|
||||
|
|
@ -463,18 +491,18 @@ async fn terminal_stream(
|
|||
"width": session.cols,
|
||||
"height": session.rows
|
||||
});
|
||||
|
||||
|
||||
yield Ok(Event::default()
|
||||
.event("header")
|
||||
.data(header.to_string()));
|
||||
|
||||
|
||||
// Poll for terminal output
|
||||
let mut poll_interval = interval(Duration::from_millis(10));
|
||||
let exit_sent = false;
|
||||
|
||||
|
||||
loop {
|
||||
poll_interval.tick().await;
|
||||
|
||||
|
||||
// Check if session still exists
|
||||
let sessions = terminal_manager.list_sessions().await;
|
||||
if !sessions.iter().any(|s| s.id == session_id) && !exit_sent {
|
||||
|
|
@ -486,7 +514,7 @@ async fn terminal_stream(
|
|||
let _ = exit_sent; // Prevent duplicate exit events
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Read any available output
|
||||
match terminal_manager.read_from_session(&session_id).await {
|
||||
Ok(data) if !data.is_empty() => {
|
||||
|
|
@ -494,7 +522,7 @@ async fn terminal_stream(
|
|||
let timestamp = chrono::Utc::now().timestamp();
|
||||
let output = String::from_utf8_lossy(&data);
|
||||
let event_data = serde_json::json!([timestamp, "o", output]);
|
||||
|
||||
|
||||
yield Ok(Event::default()
|
||||
.event("data")
|
||||
.data(event_data.to_string()));
|
||||
|
|
@ -515,7 +543,7 @@ async fn terminal_stream(
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
|
||||
}
|
||||
|
||||
|
|
@ -525,17 +553,16 @@ async fn session_events_stream(
|
|||
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
|
||||
// Clone the session monitor Arc to avoid lifetime issues
|
||||
let session_monitor = state.session_monitor.clone();
|
||||
|
||||
|
||||
// Start monitoring if not already started
|
||||
session_monitor.start_monitoring().await;
|
||||
|
||||
|
||||
// Create SSE stream from session monitor
|
||||
let stream = session_monitor.create_sse_stream()
|
||||
.map(|data| {
|
||||
data.map(|json| Event::default().data(json))
|
||||
.map_err(|_| unreachable!())
|
||||
});
|
||||
|
||||
let stream = session_monitor.create_sse_stream().map(|data| {
|
||||
data.map(|json| Event::default().data(json))
|
||||
.map_err(|_| unreachable!())
|
||||
});
|
||||
|
||||
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
|
||||
}
|
||||
|
||||
|
|
@ -544,49 +571,50 @@ async fn browse_directory(
|
|||
Query(params): Query<BrowseQuery>,
|
||||
) -> Result<Json<DirectoryListing>, StatusCode> {
|
||||
let path_str = params.path.unwrap_or_else(|| "~".to_string());
|
||||
|
||||
|
||||
// Expand tilde to home directory
|
||||
let path = if path_str.starts_with('~') {
|
||||
let home = dirs::home_dir()
|
||||
.ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let home = dirs::home_dir().ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
home.join(path_str.strip_prefix("~/").unwrap_or(""))
|
||||
} else {
|
||||
PathBuf::from(&path_str)
|
||||
};
|
||||
|
||||
|
||||
// Check if path exists and is a directory
|
||||
if !path.exists() {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
if !path.is_dir() {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
// Read directory entries
|
||||
let mut files = Vec::new();
|
||||
let entries = fs::read_dir(&path)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let entries = fs::read_dir(&path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let metadata = entry.metadata()
|
||||
let metadata = entry
|
||||
.metadata()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let created = metadata.created()
|
||||
|
||||
let created = metadata
|
||||
.created()
|
||||
.map(|t| {
|
||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||
datetime.to_rfc3339()
|
||||
})
|
||||
.unwrap_or_else(|_| String::new());
|
||||
|
||||
let modified = metadata.modified()
|
||||
|
||||
let modified = metadata
|
||||
.modified()
|
||||
.map(|t| {
|
||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||
datetime.to_rfc3339()
|
||||
})
|
||||
.unwrap_or_else(|_| String::new());
|
||||
|
||||
|
||||
files.push(FileInfo {
|
||||
name: entry.file_name().to_string_lossy().to_string(),
|
||||
created,
|
||||
|
|
@ -595,54 +623,49 @@ async fn browse_directory(
|
|||
is_dir: metadata.is_dir(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Sort directories first, then files, alphabetically
|
||||
files.sort_by(|a, b| {
|
||||
match (a.is_dir, b.is_dir) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => a.name.cmp(&b.name),
|
||||
}
|
||||
files.sort_by(|a, b| match (a.is_dir, b.is_dir) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => a.name.cmp(&b.name),
|
||||
});
|
||||
|
||||
|
||||
Ok(Json(DirectoryListing {
|
||||
absolute_path: path.to_string_lossy().to_string(),
|
||||
files,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
Json(req): Json<CreateDirRequest>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
async fn create_directory(Json(req): Json<CreateDirRequest>) -> Result<StatusCode, StatusCode> {
|
||||
// Validate directory name
|
||||
if req.name.is_empty() ||
|
||||
req.name.contains('/') ||
|
||||
req.name.contains('\\') ||
|
||||
req.name.starts_with('.') {
|
||||
if req.name.is_empty()
|
||||
|| req.name.contains('/')
|
||||
|| req.name.contains('\\')
|
||||
|| req.name.starts_with('.')
|
||||
{
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
// Expand path
|
||||
let base_path = if req.path.starts_with('~') {
|
||||
let home = dirs::home_dir()
|
||||
.ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let home = dirs::home_dir().ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
home.join(req.path.strip_prefix("~/").unwrap_or(""))
|
||||
} else {
|
||||
PathBuf::from(&req.path)
|
||||
};
|
||||
|
||||
|
||||
// Create full path
|
||||
let full_path = base_path.join(&req.name);
|
||||
|
||||
|
||||
// Check if directory already exists
|
||||
if full_path.exists() {
|
||||
return Err(StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
|
||||
// Create directory
|
||||
fs::create_dir(&full_path)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
fs::create_dir(&full_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
|
|
@ -652,20 +675,30 @@ async fn cleanup_exited(
|
|||
// Get list of all sessions
|
||||
let sessions = state.terminal_manager.list_sessions().await;
|
||||
let mut cleaned_sessions = Vec::new();
|
||||
|
||||
|
||||
// Check each session and close if the process has exited
|
||||
for session in sessions {
|
||||
// Try to write empty data to check if session is alive
|
||||
if state.terminal_manager.write_to_session(&session.id, &[]).await.is_err() {
|
||||
if state
|
||||
.terminal_manager
|
||||
.write_to_session(&session.id, &[])
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
// Session is dead, clean it up
|
||||
if state.terminal_manager.close_session(&session.id).await.is_ok() {
|
||||
if state
|
||||
.terminal_manager
|
||||
.close_session(&session.id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
cleaned_sessions.push(session.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let count = cleaned_sessions.len();
|
||||
|
||||
|
||||
Ok(Json(CleanupResponse {
|
||||
success: true,
|
||||
message: format!("{} exited sessions cleaned up", count),
|
||||
|
|
@ -679,14 +712,15 @@ async fn get_snapshot(
|
|||
) -> Result<String, StatusCode> {
|
||||
// Check if session exists
|
||||
let sessions = state.terminal_manager.list_sessions().await;
|
||||
let session = sessions.into_iter()
|
||||
let session = sessions
|
||||
.into_iter()
|
||||
.find(|s| s.id == id)
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
|
||||
// For Tauri, we don't have access to the stream-out file like the Node.js version
|
||||
// Instead, we'll return a minimal snapshot with just the header
|
||||
// The frontend can use the regular stream endpoint for actual content
|
||||
|
||||
|
||||
let cast_data = serde_json::json!({
|
||||
"version": 2,
|
||||
"width": session.cols,
|
||||
|
|
@ -703,7 +737,7 @@ async fn get_snapshot(
|
|||
]
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Return as a single line JSON (asciicast v2 format)
|
||||
Ok(cast_data.to_string())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio::time::{interval, Duration};
|
||||
use chrono::Utc;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Information about a terminal session
|
||||
|
|
@ -25,19 +25,10 @@ pub struct SessionInfo {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum SessionEvent {
|
||||
SessionCreated {
|
||||
session: SessionInfo,
|
||||
},
|
||||
SessionUpdated {
|
||||
session: SessionInfo,
|
||||
},
|
||||
SessionClosed {
|
||||
id: String,
|
||||
},
|
||||
SessionActivity {
|
||||
id: String,
|
||||
timestamp: String,
|
||||
},
|
||||
SessionCreated { session: SessionInfo },
|
||||
SessionUpdated { session: SessionInfo },
|
||||
SessionClosed { id: String },
|
||||
SessionActivity { id: String, timestamp: String },
|
||||
}
|
||||
|
||||
/// Session monitoring service
|
||||
|
|
@ -64,15 +55,15 @@ impl SessionMonitor {
|
|||
|
||||
tokio::spawn(async move {
|
||||
let mut monitor_interval = interval(Duration::from_secs(5));
|
||||
|
||||
|
||||
loop {
|
||||
monitor_interval.tick().await;
|
||||
|
||||
|
||||
// Get current sessions from terminal manager
|
||||
let current_sessions = terminal_manager.list_sessions().await;
|
||||
let mut sessions_map = sessions.write().await;
|
||||
let mut updated_sessions = HashMap::new();
|
||||
|
||||
|
||||
// Check for new or updated sessions
|
||||
for session in current_sessions {
|
||||
let session_info = SessionInfo {
|
||||
|
|
@ -86,51 +77,56 @@ impl SessionMonitor {
|
|||
is_active: true,
|
||||
client_count: 0, // TODO: Track actual client count
|
||||
};
|
||||
|
||||
|
||||
// Check if this is a new session
|
||||
if !sessions_map.contains_key(&session.id) {
|
||||
// Broadcast session created event
|
||||
Self::broadcast_event(
|
||||
&subscribers,
|
||||
SessionEvent::SessionCreated {
|
||||
session: session_info.clone()
|
||||
}
|
||||
).await;
|
||||
session: session_info.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
// Check if session was updated
|
||||
if let Some(existing) = sessions_map.get(&session.id) {
|
||||
if existing.rows != session_info.rows ||
|
||||
existing.cols != session_info.cols {
|
||||
if existing.rows != session_info.rows
|
||||
|| existing.cols != session_info.cols
|
||||
{
|
||||
// Broadcast session updated event
|
||||
Self::broadcast_event(
|
||||
&subscribers,
|
||||
SessionEvent::SessionUpdated {
|
||||
session: session_info.clone()
|
||||
}
|
||||
).await;
|
||||
session: session_info.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updated_sessions.insert(session.id.clone(), session_info);
|
||||
}
|
||||
|
||||
|
||||
// Check for closed sessions
|
||||
let closed_sessions: Vec<String> = sessions_map.keys()
|
||||
let closed_sessions: Vec<String> = sessions_map
|
||||
.keys()
|
||||
.filter(|id| !updated_sessions.contains_key(*id))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
|
||||
for session_id in closed_sessions {
|
||||
// Broadcast session closed event
|
||||
Self::broadcast_event(
|
||||
&subscribers,
|
||||
SessionEvent::SessionClosed {
|
||||
id: session_id.clone()
|
||||
}
|
||||
).await;
|
||||
id: session_id.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
|
||||
// Update the sessions map
|
||||
*sessions_map = updated_sessions;
|
||||
}
|
||||
|
|
@ -138,21 +134,27 @@ impl SessionMonitor {
|
|||
}
|
||||
|
||||
/// Subscribe to session events
|
||||
#[allow(dead_code)]
|
||||
pub async fn subscribe(&self) -> mpsc::UnboundedReceiver<SessionEvent> {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let subscriber_id = Uuid::new_v4().to_string();
|
||||
|
||||
self.event_subscribers.write().await.insert(subscriber_id, tx);
|
||||
|
||||
|
||||
self.event_subscribers
|
||||
.write()
|
||||
.await
|
||||
.insert(subscriber_id, tx);
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
/// Unsubscribe from session events
|
||||
#[allow(dead_code)]
|
||||
pub async fn unsubscribe(&self, subscriber_id: &str) {
|
||||
self.event_subscribers.write().await.remove(subscriber_id);
|
||||
}
|
||||
|
||||
/// Get current session count
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_session_count(&self) -> usize {
|
||||
self.sessions.read().await.len()
|
||||
}
|
||||
|
|
@ -163,23 +165,26 @@ impl SessionMonitor {
|
|||
}
|
||||
|
||||
/// Get a specific session
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_session(&self, id: &str) -> Option<SessionInfo> {
|
||||
self.sessions.read().await.get(id).cloned()
|
||||
}
|
||||
|
||||
/// Notify activity for a session
|
||||
#[allow(dead_code)]
|
||||
pub async fn notify_activity(&self, session_id: &str) {
|
||||
if let Some(session) = self.sessions.write().await.get_mut(session_id) {
|
||||
session.last_activity = Utc::now().to_rfc3339();
|
||||
|
||||
|
||||
// Broadcast activity event
|
||||
Self::broadcast_event(
|
||||
&self.event_subscribers,
|
||||
SessionEvent::SessionActivity {
|
||||
id: session_id.to_string(),
|
||||
timestamp: session.last_activity.clone(),
|
||||
}
|
||||
).await;
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,13 +195,13 @@ impl SessionMonitor {
|
|||
) {
|
||||
let subscribers_read = subscribers.read().await;
|
||||
let mut dead_subscribers = Vec::new();
|
||||
|
||||
|
||||
for (id, tx) in subscribers_read.iter() {
|
||||
if tx.send(event.clone()).is_err() {
|
||||
dead_subscribers.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove dead subscribers
|
||||
if !dead_subscribers.is_empty() {
|
||||
drop(subscribers_read);
|
||||
|
|
@ -208,13 +213,16 @@ impl SessionMonitor {
|
|||
}
|
||||
|
||||
/// Create an SSE stream for session events
|
||||
pub fn create_sse_stream(self: Arc<Self>) -> impl futures::Stream<Item = Result<String, std::convert::Infallible>> + Send + 'static {
|
||||
pub fn create_sse_stream(
|
||||
self: Arc<Self>,
|
||||
) -> impl futures::Stream<Item = Result<String, std::convert::Infallible>> + Send + 'static
|
||||
{
|
||||
async_stream::stream! {
|
||||
// Subscribe to events
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let subscriber_id = Uuid::new_v4().to_string();
|
||||
self.event_subscribers.write().await.insert(subscriber_id.clone(), tx);
|
||||
|
||||
|
||||
// Send initial sessions
|
||||
let session_list = self.sessions.read().await.values().cloned().collect::<Vec<_>>();
|
||||
let initial_event = serde_json::json!({
|
||||
|
|
@ -222,16 +230,16 @@ impl SessionMonitor {
|
|||
"sessions": session_list,
|
||||
"count": session_list.len()
|
||||
});
|
||||
|
||||
|
||||
yield Ok(format!("data: {}\n\n", initial_event));
|
||||
|
||||
|
||||
// Send events as they come
|
||||
while let Some(event) = rx.recv().await {
|
||||
if let Ok(json) = serde_json::to_string(&event) {
|
||||
yield Ok(format!("data: {}\n\n", json));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Clean up subscriber on drop
|
||||
self.event_subscribers.write().await.remove(&subscriber_id);
|
||||
}
|
||||
|
|
@ -254,14 +262,14 @@ impl SessionMonitor {
|
|||
let sessions = self.sessions.read().await;
|
||||
let active_sessions = sessions.values().filter(|s| s.is_active).count();
|
||||
let total_clients = sessions.values().map(|s| s.client_count).sum();
|
||||
|
||||
|
||||
// TODO: Track more detailed statistics
|
||||
SessionStats {
|
||||
total_sessions: sessions.len(),
|
||||
active_sessions,
|
||||
total_clients,
|
||||
uptime_seconds: 0, // TODO: Track uptime
|
||||
uptime_seconds: 0, // TODO: Track uptime
|
||||
sessions_created_today: 0, // TODO: Track daily stats
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use directories::ProjectDirs;
|
||||
use tauri::{Manager, State};
|
||||
use crate::state::AppState;
|
||||
use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{Manager, State};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GeneralSettings {
|
||||
|
|
@ -193,7 +193,7 @@ impl Default for Settings {
|
|||
default_notification_types.insert("error".to_string(), true);
|
||||
default_notification_types.insert("server_status".to_string(), true);
|
||||
default_notification_types.insert("update_available".to_string(), true);
|
||||
|
||||
|
||||
let mut enabled_terminals = HashMap::new();
|
||||
enabled_terminals.insert("Terminal".to_string(), true);
|
||||
enabled_terminals.insert("iTerm2".to_string(), true);
|
||||
|
|
@ -202,7 +202,7 @@ impl Default for Settings {
|
|||
enabled_terminals.insert("Warp".to_string(), true);
|
||||
enabled_terminals.insert("Ghostty".to_string(), false);
|
||||
enabled_terminals.insert("WezTerm".to_string(), false);
|
||||
|
||||
|
||||
Self {
|
||||
general: GeneralSettings {
|
||||
launch_at_login: false,
|
||||
|
|
@ -328,48 +328,45 @@ impl Default for Settings {
|
|||
impl Settings {
|
||||
pub fn load() -> Result<Self, String> {
|
||||
let config_path = Self::config_path()?;
|
||||
|
||||
|
||||
if !config_path.exists() {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
|
||||
|
||||
let contents = std::fs::read_to_string(&config_path)
|
||||
.map_err(|e| format!("Failed to read settings: {}", e))?;
|
||||
|
||||
toml::from_str(&contents)
|
||||
.map_err(|e| format!("Failed to parse settings: {}", e))
|
||||
|
||||
toml::from_str(&contents).map_err(|e| format!("Failed to parse settings: {}", e))
|
||||
}
|
||||
|
||||
|
||||
pub fn save(&self) -> Result<(), String> {
|
||||
let config_path = Self::config_path()?;
|
||||
|
||||
|
||||
// Ensure the config directory exists
|
||||
if let Some(parent) = config_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create config directory: {}", e))?;
|
||||
}
|
||||
|
||||
|
||||
let contents = toml::to_string_pretty(self)
|
||||
.map_err(|e| format!("Failed to serialize settings: {}", e))?;
|
||||
|
||||
|
||||
std::fs::write(&config_path, contents)
|
||||
.map_err(|e| format!("Failed to write settings: {}", e))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn config_path() -> Result<PathBuf, String> {
|
||||
let proj_dirs = ProjectDirs::from("com", "vibetunnel", "VibeTunnel")
|
||||
.ok_or_else(|| "Failed to get project directories".to_string())?;
|
||||
|
||||
|
||||
Ok(proj_dirs.config_dir().join("settings.toml"))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_settings(
|
||||
_state: State<'_, AppState>,
|
||||
) -> Result<Settings, String> {
|
||||
pub async fn get_settings(_state: State<'_, AppState>) -> Result<Settings, String> {
|
||||
Settings::load()
|
||||
}
|
||||
|
||||
|
|
@ -380,20 +377,23 @@ pub async fn save_settings(
|
|||
app: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
settings.save()?;
|
||||
|
||||
|
||||
// Apply settings that need immediate effect
|
||||
if settings.general.launch_at_login {
|
||||
crate::auto_launch::enable_auto_launch()?;
|
||||
} else {
|
||||
crate::auto_launch::disable_auto_launch()?;
|
||||
}
|
||||
|
||||
|
||||
// Apply dock icon visibility on macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Check if any windows are visible
|
||||
let has_visible_windows = app.windows().values().any(|w| w.is_visible().unwrap_or(false));
|
||||
|
||||
let has_visible_windows = app
|
||||
.windows()
|
||||
.values()
|
||||
.any(|w| w.is_visible().unwrap_or(false));
|
||||
|
||||
if !has_visible_windows && !settings.general.show_dock_icon {
|
||||
// Hide dock icon if no windows are visible and setting is disabled
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
|
@ -403,6 +403,6 @@ pub async fn save_settings(
|
|||
}
|
||||
// Note: If windows are visible, we always show the dock icon regardless of setting
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
use tokio::sync::RwLock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use crate::terminal::TerminalManager;
|
||||
use crate::server::HttpServer;
|
||||
use crate::ngrok::NgrokManager;
|
||||
use crate::cast::CastManager;
|
||||
use crate::tty_forward::TTYForwardManager;
|
||||
use crate::session_monitor::SessionMonitor;
|
||||
use crate::notification_manager::NotificationManager;
|
||||
use crate::welcome::WelcomeManager;
|
||||
use crate::permissions::PermissionsManager;
|
||||
use crate::updater::UpdateManager;
|
||||
use crate::backend_manager::BackendManager;
|
||||
use crate::debug_features::DebugFeaturesManager;
|
||||
use crate::api_testing::APITestingManager;
|
||||
use crate::auth_cache::AuthCacheManager;
|
||||
use crate::backend_manager::BackendManager;
|
||||
use crate::cast::CastManager;
|
||||
use crate::debug_features::DebugFeaturesManager;
|
||||
use crate::ngrok::NgrokManager;
|
||||
use crate::notification_manager::NotificationManager;
|
||||
use crate::permissions::PermissionsManager;
|
||||
use crate::server::HttpServer;
|
||||
use crate::session_monitor::SessionMonitor;
|
||||
use crate::terminal::TerminalManager;
|
||||
use crate::terminal_integrations::TerminalIntegrationsManager;
|
||||
use crate::terminal_spawn_service::TerminalSpawnService;
|
||||
use crate::tty_forward::TTYForwardManager;
|
||||
use crate::updater::UpdateManager;
|
||||
use crate::welcome::WelcomeManager;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
|
|
@ -44,40 +44,40 @@ impl AppState {
|
|||
pub fn new() -> Self {
|
||||
let mut terminal_manager = TerminalManager::new();
|
||||
let cast_manager = Arc::new(CastManager::new());
|
||||
|
||||
|
||||
// Connect terminal manager to cast manager
|
||||
terminal_manager.set_cast_manager(cast_manager.clone());
|
||||
|
||||
|
||||
let terminal_manager = Arc::new(terminal_manager);
|
||||
let session_monitor = Arc::new(SessionMonitor::new(terminal_manager.clone()));
|
||||
let notification_manager = Arc::new(NotificationManager::new());
|
||||
let mut permissions_manager = PermissionsManager::new();
|
||||
permissions_manager.set_notification_manager(notification_manager.clone());
|
||||
|
||||
|
||||
let current_version = env!("CARGO_PKG_VERSION").to_string();
|
||||
let mut update_manager = UpdateManager::new(current_version);
|
||||
update_manager.set_notification_manager(notification_manager.clone());
|
||||
|
||||
|
||||
let mut backend_manager = BackendManager::new();
|
||||
backend_manager.set_notification_manager(notification_manager.clone());
|
||||
|
||||
|
||||
let mut debug_features_manager = DebugFeaturesManager::new();
|
||||
debug_features_manager.set_notification_manager(notification_manager.clone());
|
||||
|
||||
|
||||
let mut api_testing_manager = APITestingManager::new();
|
||||
api_testing_manager.set_notification_manager(notification_manager.clone());
|
||||
|
||||
|
||||
let mut auth_cache_manager = AuthCacheManager::new();
|
||||
auth_cache_manager.set_notification_manager(notification_manager.clone());
|
||||
|
||||
|
||||
let mut terminal_integrations_manager = TerminalIntegrationsManager::new();
|
||||
terminal_integrations_manager.set_notification_manager(notification_manager.clone());
|
||||
|
||||
|
||||
let terminal_integrations_manager = Arc::new(terminal_integrations_manager);
|
||||
let terminal_spawn_service = Arc::new(TerminalSpawnService::new(
|
||||
terminal_integrations_manager.clone()
|
||||
terminal_integrations_manager.clone(),
|
||||
));
|
||||
|
||||
|
||||
Self {
|
||||
terminal_manager,
|
||||
http_server: Arc::new(RwLock::new(None)),
|
||||
|
|
@ -99,4 +99,4 @@ impl AppState {
|
|||
terminal_spawn_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::io::{Read, Write};
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system, PtyPair, Child};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use bytes::Bytes;
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
use tracing::{info, error, debug};
|
||||
use crate::cast::CastManager;
|
||||
use bytes::Bytes;
|
||||
use chrono::Utc;
|
||||
use portable_pty::{native_pty_system, Child, CommandBuilder, PtyPair, PtySize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tracing::{debug, error, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TerminalManager {
|
||||
|
|
@ -24,9 +24,12 @@ pub struct TerminalSession {
|
|||
pub created_at: String,
|
||||
pub cwd: String,
|
||||
pty_pair: PtyPair,
|
||||
#[allow(dead_code)]
|
||||
child: Box<dyn Child + Send + Sync>,
|
||||
writer: Box<dyn Write + Send>,
|
||||
#[allow(dead_code)]
|
||||
reader_thread: Option<std::thread::JoinHandle<()>>,
|
||||
#[allow(dead_code)]
|
||||
output_tx: mpsc::UnboundedSender<Bytes>,
|
||||
pub output_rx: Arc<Mutex<mpsc::UnboundedReceiver<Bytes>>>,
|
||||
}
|
||||
|
|
@ -53,7 +56,7 @@ impl TerminalManager {
|
|||
shell: Option<String>,
|
||||
) -> Result<crate::commands::Terminal, String> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
|
||||
|
||||
// Set up PTY
|
||||
let pty_system = native_pty_system();
|
||||
let pty_pair = pty_system
|
||||
|
|
@ -77,12 +80,12 @@ impl TerminalManager {
|
|||
});
|
||||
|
||||
let mut cmd = CommandBuilder::new(&shell);
|
||||
|
||||
|
||||
// Set working directory
|
||||
if let Some(cwd) = &cwd {
|
||||
cmd.cwd(cwd);
|
||||
}
|
||||
|
||||
|
||||
// Set environment variables
|
||||
if let Some(env_vars) = env {
|
||||
for (key, value) in env_vars {
|
||||
|
|
@ -97,16 +100,16 @@ impl TerminalManager {
|
|||
.map_err(|e| format!("Failed to spawn shell: {}", e))?;
|
||||
|
||||
let pid = child.process_id().unwrap_or(0);
|
||||
|
||||
|
||||
// Set up output channel
|
||||
let (output_tx, output_rx) = mpsc::unbounded_channel();
|
||||
|
||||
|
||||
// Get reader and writer
|
||||
let reader = pty_pair
|
||||
.master
|
||||
.try_clone_reader()
|
||||
.map_err(|e| format!("Failed to clone reader: {}", e))?;
|
||||
|
||||
|
||||
let writer = pty_pair
|
||||
.master
|
||||
.take_writer()
|
||||
|
|
@ -119,7 +122,7 @@ impl TerminalManager {
|
|||
let reader_thread = std::thread::spawn(move || {
|
||||
let mut reader = reader;
|
||||
let mut buffer = [0u8; 4096];
|
||||
|
||||
|
||||
loop {
|
||||
match reader.read(&mut buffer) {
|
||||
Ok(0) => {
|
||||
|
|
@ -128,7 +131,7 @@ impl TerminalManager {
|
|||
}
|
||||
Ok(n) => {
|
||||
let data = Bytes::copy_from_slice(&buffer[..n]);
|
||||
|
||||
|
||||
// Record output to cast file if recording
|
||||
if let Some(cast_manager) = &cast_manager_clone {
|
||||
let cm = cast_manager.clone();
|
||||
|
|
@ -138,7 +141,7 @@ impl TerminalManager {
|
|||
let _ = cm.add_output(&sid, &data_clone).await;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if output_tx_clone.send(data).is_err() {
|
||||
debug!("Output channel closed");
|
||||
break;
|
||||
|
|
@ -159,7 +162,12 @@ impl TerminalManager {
|
|||
rows,
|
||||
cols,
|
||||
created_at: Utc::now().to_rfc3339(),
|
||||
cwd: cwd.unwrap_or_else(|| std::env::current_dir().unwrap().to_string_lossy().to_string()),
|
||||
cwd: cwd.unwrap_or_else(|| {
|
||||
std::env::current_dir()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}),
|
||||
pty_pair,
|
||||
child,
|
||||
writer,
|
||||
|
|
@ -169,7 +177,10 @@ impl TerminalManager {
|
|||
};
|
||||
|
||||
// Store session
|
||||
self.sessions.write().await.insert(id.clone(), Arc::new(RwLock::new(session)));
|
||||
self.sessions
|
||||
.write()
|
||||
.await
|
||||
.insert(id.clone(), Arc::new(RwLock::new(session)));
|
||||
|
||||
info!("Created terminal session: {} ({})", name, id);
|
||||
|
||||
|
|
@ -186,7 +197,7 @@ impl TerminalManager {
|
|||
pub async fn list_sessions(&self) -> Vec<crate::commands::Terminal> {
|
||||
let sessions = self.sessions.read().await;
|
||||
let mut result = Vec::new();
|
||||
|
||||
|
||||
for (id, session) in sessions.iter() {
|
||||
let session = session.read().await;
|
||||
result.push(crate::commands::Terminal {
|
||||
|
|
@ -198,7 +209,7 @@ impl TerminalManager {
|
|||
created_at: session.created_at.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
|
@ -209,26 +220,26 @@ impl TerminalManager {
|
|||
pub async fn close_all_sessions(&self) -> Result<(), String> {
|
||||
let mut sessions = self.sessions.write().await;
|
||||
let session_count = sessions.len();
|
||||
|
||||
|
||||
// Clear all sessions
|
||||
sessions.clear();
|
||||
|
||||
|
||||
info!("Closed all {} terminal sessions", session_count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub async fn close_session(&self, id: &str) -> Result<(), String> {
|
||||
let mut sessions = self.sessions.write().await;
|
||||
|
||||
|
||||
if let Some(session_arc) = sessions.remove(id) {
|
||||
// Stop recording if active
|
||||
if let Some(cast_manager) = &self.cast_manager {
|
||||
let _ = cast_manager.remove_recorder(id).await;
|
||||
}
|
||||
|
||||
|
||||
// Session will be dropped when it goes out of scope
|
||||
drop(session_arc);
|
||||
|
||||
|
||||
info!("Closed terminal session: {}", id);
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -239,8 +250,9 @@ impl TerminalManager {
|
|||
pub async fn resize_session(&self, id: &str, rows: u16, cols: u16) -> Result<(), String> {
|
||||
if let Some(session_arc) = self.get_session(id).await {
|
||||
let mut session = session_arc.write().await;
|
||||
|
||||
session.pty_pair
|
||||
|
||||
session
|
||||
.pty_pair
|
||||
.master
|
||||
.resize(PtySize {
|
||||
rows,
|
||||
|
|
@ -249,10 +261,10 @@ impl TerminalManager {
|
|||
pixel_height: 0,
|
||||
})
|
||||
.map_err(|e| format!("Failed to resize PTY: {}", e))?;
|
||||
|
||||
|
||||
session.rows = rows;
|
||||
session.cols = cols;
|
||||
|
||||
|
||||
// Update recorder dimensions if recording
|
||||
if let Some(cast_manager) = &self.cast_manager {
|
||||
if let Some(recorder) = cast_manager.get_recorder(id).await {
|
||||
|
|
@ -260,7 +272,7 @@ impl TerminalManager {
|
|||
rec.resize(cols, rows).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
debug!("Resized terminal {} to {}x{}", id, cols, rows);
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -271,20 +283,22 @@ impl TerminalManager {
|
|||
pub async fn write_to_session(&self, id: &str, data: &[u8]) -> Result<(), String> {
|
||||
if let Some(session_arc) = self.get_session(id).await {
|
||||
let mut session = session_arc.write().await;
|
||||
|
||||
|
||||
// Record input to cast file if recording
|
||||
if let Some(cast_manager) = &self.cast_manager {
|
||||
let _ = cast_manager.add_input(id, data).await;
|
||||
}
|
||||
|
||||
session.writer
|
||||
|
||||
session
|
||||
.writer
|
||||
.write_all(data)
|
||||
.map_err(|e| format!("Failed to write to PTY: {}", e))?;
|
||||
|
||||
session.writer
|
||||
|
||||
session
|
||||
.writer
|
||||
.flush()
|
||||
.map_err(|e| format!("Failed to flush PTY: {}", e))?;
|
||||
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Session not found: {}", id))
|
||||
|
|
@ -295,7 +309,7 @@ impl TerminalManager {
|
|||
if let Some(session_arc) = self.get_session(id).await {
|
||||
let session = session_arc.read().await;
|
||||
let mut rx = session.output_rx.lock().unwrap();
|
||||
|
||||
|
||||
// Try to receive data without blocking
|
||||
match rx.try_recv() {
|
||||
Ok(data) => Ok(data.to_vec()),
|
||||
|
|
@ -312,4 +326,4 @@ impl TerminalManager {
|
|||
|
||||
// Make TerminalSession Send + Sync
|
||||
unsafe impl Send for TerminalSession {}
|
||||
unsafe impl Sync for TerminalSession {}
|
||||
unsafe impl Sync for TerminalSession {}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Check for Terminal.app
|
||||
if let Ok(_) = Command::new("open")
|
||||
.args(&["-Ra", "Terminal.app"])
|
||||
.output()
|
||||
{
|
||||
if let Ok(_) = Command::new("open").args(&["-Ra", "Terminal.app"]).output() {
|
||||
available_terminals.push(TerminalInfo {
|
||||
name: "Terminal".to_string(),
|
||||
path: "/System/Applications/Utilities/Terminal.app".to_string(),
|
||||
|
|
@ -33,10 +30,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
}
|
||||
|
||||
// Check for iTerm2
|
||||
if let Ok(_) = Command::new("open")
|
||||
.args(&["-Ra", "iTerm.app"])
|
||||
.output()
|
||||
{
|
||||
if let Ok(_) = Command::new("open").args(&["-Ra", "iTerm.app"]).output() {
|
||||
available_terminals.push(TerminalInfo {
|
||||
name: "iTerm2".to_string(),
|
||||
path: "/Applications/iTerm.app".to_string(),
|
||||
|
|
@ -45,10 +39,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
}
|
||||
|
||||
// Check for Warp
|
||||
if let Ok(output) = Command::new("which")
|
||||
.arg("warp")
|
||||
.output()
|
||||
{
|
||||
if let Ok(output) = Command::new("which").arg("warp").output() {
|
||||
if output.status.success() {
|
||||
available_terminals.push(TerminalInfo {
|
||||
name: "Warp".to_string(),
|
||||
|
|
@ -59,10 +50,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
}
|
||||
|
||||
// Check for Hyper
|
||||
if let Ok(_) = Command::new("open")
|
||||
.args(&["-Ra", "Hyper.app"])
|
||||
.output()
|
||||
{
|
||||
if let Ok(_) = Command::new("open").args(&["-Ra", "Hyper.app"]).output() {
|
||||
available_terminals.push(TerminalInfo {
|
||||
name: "Hyper".to_string(),
|
||||
path: "/Applications/Hyper.app".to_string(),
|
||||
|
|
@ -71,10 +59,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
}
|
||||
|
||||
// Check for Alacritty
|
||||
if let Ok(output) = Command::new("which")
|
||||
.arg("alacritty")
|
||||
.output()
|
||||
{
|
||||
if let Ok(output) = Command::new("which").arg("alacritty").output() {
|
||||
if output.status.success() {
|
||||
available_terminals.push(TerminalInfo {
|
||||
name: "Alacritty".to_string(),
|
||||
|
|
@ -114,10 +99,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Check for Windows Terminal
|
||||
if let Ok(output) = Command::new("where")
|
||||
.arg("wt.exe")
|
||||
.output()
|
||||
{
|
||||
if let Ok(output) = Command::new("where").arg("wt.exe").output() {
|
||||
if output.status.success() {
|
||||
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
available_terminals.push(TerminalInfo {
|
||||
|
|
@ -134,10 +116,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
}
|
||||
|
||||
// Check for PowerShell
|
||||
if let Ok(output) = Command::new("where")
|
||||
.arg("powershell.exe")
|
||||
.output()
|
||||
{
|
||||
if let Ok(output) = Command::new("where").arg("powershell.exe").output() {
|
||||
if output.status.success() {
|
||||
available_terminals.push(TerminalInfo {
|
||||
name: "PowerShell".to_string(),
|
||||
|
|
@ -148,10 +127,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
}
|
||||
|
||||
// Check for Command Prompt
|
||||
if let Ok(output) = Command::new("where")
|
||||
.arg("cmd.exe")
|
||||
.output()
|
||||
{
|
||||
if let Ok(output) = Command::new("where").arg("cmd.exe").output() {
|
||||
if output.status.success() {
|
||||
available_terminals.push(TerminalInfo {
|
||||
name: "Command Prompt".to_string(),
|
||||
|
|
@ -187,10 +163,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
];
|
||||
|
||||
for (cmd, name) in terminals {
|
||||
if let Ok(output) = Command::new("which")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
{
|
||||
if let Ok(output) = Command::new("which").arg(cmd).output() {
|
||||
if output.status.success() {
|
||||
available_terminals.push(TerminalInfo {
|
||||
name: name.to_string(),
|
||||
|
|
@ -205,17 +178,20 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
|||
if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
|
||||
match desktop.to_lowercase().as_str() {
|
||||
"gnome" | "ubuntu" => {
|
||||
default_terminal = available_terminals.iter()
|
||||
default_terminal = available_terminals
|
||||
.iter()
|
||||
.find(|t| t.name == "GNOME Terminal")
|
||||
.cloned();
|
||||
}
|
||||
"kde" => {
|
||||
default_terminal = available_terminals.iter()
|
||||
default_terminal = available_terminals
|
||||
.iter()
|
||||
.find(|t| t.name == "Konsole")
|
||||
.cloned();
|
||||
}
|
||||
"xfce" => {
|
||||
default_terminal = available_terminals.iter()
|
||||
default_terminal = available_terminals
|
||||
.iter()
|
||||
.find(|t| t.name == "XFCE Terminal")
|
||||
.cloned();
|
||||
}
|
||||
|
|
@ -247,7 +223,7 @@ pub async fn get_default_shell() -> Result<String, String> {
|
|||
if let Ok(shell) = std::env::var("SHELL") {
|
||||
return Ok(shell);
|
||||
}
|
||||
|
||||
|
||||
// Fallback to common shells
|
||||
let shells = vec!["/bin/zsh", "/bin/bash", "/bin/sh"];
|
||||
for shell in shells {
|
||||
|
|
@ -256,22 +232,19 @@ pub async fn get_default_shell() -> Result<String, String> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, default to PowerShell
|
||||
if let Ok(output) = Command::new("where")
|
||||
.arg("powershell.exe")
|
||||
.output()
|
||||
{
|
||||
if let Ok(output) = Command::new("where").arg("powershell.exe").output() {
|
||||
if output.status.success() {
|
||||
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback to cmd
|
||||
return Ok("cmd.exe".to_string());
|
||||
}
|
||||
|
||||
|
||||
Err("Could not detect default shell".to_string())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Terminal emulator type
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub enum TerminalEmulator {
|
||||
SystemDefault,
|
||||
Terminal, // macOS Terminal.app
|
||||
ITerm2, // iTerm2
|
||||
Hyper, // Hyper
|
||||
Alacritty, // Alacritty
|
||||
Kitty, // Kitty
|
||||
WezTerm, // WezTerm
|
||||
Ghostty, // Ghostty
|
||||
Warp, // Warp
|
||||
Terminal, // macOS Terminal.app
|
||||
ITerm2, // iTerm2
|
||||
Hyper, // Hyper
|
||||
Alacritty, // Alacritty
|
||||
Kitty, // Kitty
|
||||
WezTerm, // WezTerm
|
||||
Ghostty, // Ghostty
|
||||
Warp, // Warp
|
||||
WindowsTerminal, // Windows Terminal
|
||||
ConEmu, // ConEmu
|
||||
Cmder, // Cmder
|
||||
Gnome, // GNOME Terminal
|
||||
Konsole, // KDE Konsole
|
||||
Xterm, // XTerm
|
||||
Custom, // Custom terminal
|
||||
ConEmu, // ConEmu
|
||||
Cmder, // Cmder
|
||||
Gnome, // GNOME Terminal
|
||||
Konsole, // KDE Konsole
|
||||
Xterm, // XTerm
|
||||
Custom, // Custom terminal
|
||||
}
|
||||
|
||||
impl TerminalEmulator {
|
||||
|
|
@ -141,7 +141,7 @@ impl TerminalIntegrationsManager {
|
|||
async move {
|
||||
let default_configs = Self::initialize_default_configs();
|
||||
*configs.write().await = default_configs;
|
||||
|
||||
|
||||
let default_schemes = Self::initialize_url_schemes();
|
||||
*url_schemes.write().await = default_schemes;
|
||||
}
|
||||
|
|
@ -151,7 +151,10 @@ impl TerminalIntegrationsManager {
|
|||
}
|
||||
|
||||
/// Set the notification manager
|
||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
||||
pub fn set_notification_manager(
|
||||
&mut self,
|
||||
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||
) {
|
||||
self.notification_manager = Some(notification_manager);
|
||||
}
|
||||
|
||||
|
|
@ -160,123 +163,148 @@ impl TerminalIntegrationsManager {
|
|||
let mut configs = HashMap::new();
|
||||
|
||||
// WezTerm configuration
|
||||
configs.insert(TerminalEmulator::WezTerm, TerminalConfig {
|
||||
emulator: TerminalEmulator::WezTerm,
|
||||
name: "WezTerm".to_string(),
|
||||
executable_path: PathBuf::from("/Applications/WezTerm.app/Contents/MacOS/wezterm"),
|
||||
args_template: vec![
|
||||
"start".to_string(),
|
||||
"--cwd".to_string(),
|
||||
"{working_directory}".to_string(),
|
||||
"--".to_string(),
|
||||
"{command}".to_string(),
|
||||
"{args}".to_string(),
|
||||
],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: true,
|
||||
supports_splits: true,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: true,
|
||||
supports_url_scheme: false,
|
||||
supports_remote_control: true,
|
||||
configs.insert(
|
||||
TerminalEmulator::WezTerm,
|
||||
TerminalConfig {
|
||||
emulator: TerminalEmulator::WezTerm,
|
||||
name: "WezTerm".to_string(),
|
||||
executable_path: PathBuf::from("/Applications/WezTerm.app/Contents/MacOS/wezterm"),
|
||||
args_template: vec![
|
||||
"start".to_string(),
|
||||
"--cwd".to_string(),
|
||||
"{working_directory}".to_string(),
|
||||
"--".to_string(),
|
||||
"{command}".to_string(),
|
||||
"{args}".to_string(),
|
||||
],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: true,
|
||||
supports_splits: true,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: true,
|
||||
supports_url_scheme: false,
|
||||
supports_remote_control: true,
|
||||
},
|
||||
platform: vec![
|
||||
"macos".to_string(),
|
||||
"windows".to_string(),
|
||||
"linux".to_string(),
|
||||
],
|
||||
},
|
||||
platform: vec!["macos".to_string(), "windows".to_string(), "linux".to_string()],
|
||||
});
|
||||
);
|
||||
|
||||
// Ghostty configuration
|
||||
configs.insert(TerminalEmulator::Ghostty, TerminalConfig {
|
||||
emulator: TerminalEmulator::Ghostty,
|
||||
name: "Ghostty".to_string(),
|
||||
executable_path: PathBuf::from("/Applications/Ghostty.app/Contents/MacOS/ghostty"),
|
||||
args_template: vec![
|
||||
"--working-directory".to_string(),
|
||||
"{working_directory}".to_string(),
|
||||
"--command".to_string(),
|
||||
"{command}".to_string(),
|
||||
"{args}".to_string(),
|
||||
],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: true,
|
||||
supports_splits: true,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: false,
|
||||
supports_url_scheme: false,
|
||||
supports_remote_control: false,
|
||||
configs.insert(
|
||||
TerminalEmulator::Ghostty,
|
||||
TerminalConfig {
|
||||
emulator: TerminalEmulator::Ghostty,
|
||||
name: "Ghostty".to_string(),
|
||||
executable_path: PathBuf::from("/Applications/Ghostty.app/Contents/MacOS/ghostty"),
|
||||
args_template: vec![
|
||||
"--working-directory".to_string(),
|
||||
"{working_directory}".to_string(),
|
||||
"--command".to_string(),
|
||||
"{command}".to_string(),
|
||||
"{args}".to_string(),
|
||||
],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: true,
|
||||
supports_splits: true,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: false,
|
||||
supports_url_scheme: false,
|
||||
supports_remote_control: false,
|
||||
},
|
||||
platform: vec!["macos".to_string()],
|
||||
},
|
||||
platform: vec!["macos".to_string()],
|
||||
});
|
||||
);
|
||||
|
||||
// iTerm2 configuration
|
||||
configs.insert(TerminalEmulator::ITerm2, TerminalConfig {
|
||||
emulator: TerminalEmulator::ITerm2,
|
||||
name: "iTerm2".to_string(),
|
||||
executable_path: PathBuf::from("/Applications/iTerm.app/Contents/MacOS/iTerm2"),
|
||||
args_template: vec![],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: true,
|
||||
supports_splits: true,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: true,
|
||||
supports_url_scheme: true,
|
||||
supports_remote_control: true,
|
||||
configs.insert(
|
||||
TerminalEmulator::ITerm2,
|
||||
TerminalConfig {
|
||||
emulator: TerminalEmulator::ITerm2,
|
||||
name: "iTerm2".to_string(),
|
||||
executable_path: PathBuf::from("/Applications/iTerm.app/Contents/MacOS/iTerm2"),
|
||||
args_template: vec![],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: true,
|
||||
supports_splits: true,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: true,
|
||||
supports_url_scheme: true,
|
||||
supports_remote_control: true,
|
||||
},
|
||||
platform: vec!["macos".to_string()],
|
||||
},
|
||||
platform: vec!["macos".to_string()],
|
||||
});
|
||||
);
|
||||
|
||||
// Alacritty configuration
|
||||
configs.insert(TerminalEmulator::Alacritty, TerminalConfig {
|
||||
emulator: TerminalEmulator::Alacritty,
|
||||
name: "Alacritty".to_string(),
|
||||
executable_path: PathBuf::from("/Applications/Alacritty.app/Contents/MacOS/alacritty"),
|
||||
args_template: vec![
|
||||
"--working-directory".to_string(),
|
||||
"{working_directory}".to_string(),
|
||||
"-e".to_string(),
|
||||
"{command}".to_string(),
|
||||
"{args}".to_string(),
|
||||
],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: false,
|
||||
supports_splits: false,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: false,
|
||||
supports_url_scheme: false,
|
||||
supports_remote_control: false,
|
||||
configs.insert(
|
||||
TerminalEmulator::Alacritty,
|
||||
TerminalConfig {
|
||||
emulator: TerminalEmulator::Alacritty,
|
||||
name: "Alacritty".to_string(),
|
||||
executable_path: PathBuf::from(
|
||||
"/Applications/Alacritty.app/Contents/MacOS/alacritty",
|
||||
),
|
||||
args_template: vec![
|
||||
"--working-directory".to_string(),
|
||||
"{working_directory}".to_string(),
|
||||
"-e".to_string(),
|
||||
"{command}".to_string(),
|
||||
"{args}".to_string(),
|
||||
],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: false,
|
||||
supports_splits: false,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: false,
|
||||
supports_url_scheme: false,
|
||||
supports_remote_control: false,
|
||||
},
|
||||
platform: vec![
|
||||
"macos".to_string(),
|
||||
"windows".to_string(),
|
||||
"linux".to_string(),
|
||||
],
|
||||
},
|
||||
platform: vec!["macos".to_string(), "windows".to_string(), "linux".to_string()],
|
||||
});
|
||||
);
|
||||
|
||||
// Kitty configuration
|
||||
configs.insert(TerminalEmulator::Kitty, TerminalConfig {
|
||||
emulator: TerminalEmulator::Kitty,
|
||||
name: "Kitty".to_string(),
|
||||
executable_path: PathBuf::from("/Applications/kitty.app/Contents/MacOS/kitty"),
|
||||
args_template: vec![
|
||||
"--directory".to_string(),
|
||||
"{working_directory}".to_string(),
|
||||
"{command}".to_string(),
|
||||
"{args}".to_string(),
|
||||
],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: true,
|
||||
supports_splits: true,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: true,
|
||||
supports_url_scheme: false,
|
||||
supports_remote_control: true,
|
||||
configs.insert(
|
||||
TerminalEmulator::Kitty,
|
||||
TerminalConfig {
|
||||
emulator: TerminalEmulator::Kitty,
|
||||
name: "Kitty".to_string(),
|
||||
executable_path: PathBuf::from("/Applications/kitty.app/Contents/MacOS/kitty"),
|
||||
args_template: vec![
|
||||
"--directory".to_string(),
|
||||
"{working_directory}".to_string(),
|
||||
"{command}".to_string(),
|
||||
"{args}".to_string(),
|
||||
],
|
||||
env_vars: HashMap::new(),
|
||||
features: TerminalFeatures {
|
||||
supports_tabs: true,
|
||||
supports_splits: true,
|
||||
supports_profiles: true,
|
||||
supports_themes: true,
|
||||
supports_scripting: true,
|
||||
supports_url_scheme: false,
|
||||
supports_remote_control: true,
|
||||
},
|
||||
platform: vec!["macos".to_string(), "linux".to_string()],
|
||||
},
|
||||
platform: vec!["macos".to_string(), "linux".to_string()],
|
||||
});
|
||||
);
|
||||
|
||||
configs
|
||||
}
|
||||
|
|
@ -285,12 +313,15 @@ impl TerminalIntegrationsManager {
|
|||
fn initialize_url_schemes() -> HashMap<TerminalEmulator, TerminalURLScheme> {
|
||||
let mut schemes = HashMap::new();
|
||||
|
||||
schemes.insert(TerminalEmulator::ITerm2, TerminalURLScheme {
|
||||
scheme: "iterm2".to_string(),
|
||||
supports_ssh: true,
|
||||
supports_local: true,
|
||||
template: "iterm2://ssh/{user}@{host}:{port}".to_string(),
|
||||
});
|
||||
schemes.insert(
|
||||
TerminalEmulator::ITerm2,
|
||||
TerminalURLScheme {
|
||||
scheme: "iterm2".to_string(),
|
||||
supports_ssh: true,
|
||||
supports_local: true,
|
||||
template: "iterm2://ssh/{user}@{host}:{port}".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
schemes
|
||||
}
|
||||
|
|
@ -304,7 +335,10 @@ impl TerminalIntegrationsManager {
|
|||
let info = self.check_terminal_installation(emulator, config).await;
|
||||
if info.installed {
|
||||
detected.push(info.clone());
|
||||
self.detected_terminals.write().await.insert(*emulator, info);
|
||||
self.detected_terminals
|
||||
.write()
|
||||
.await
|
||||
.insert(*emulator, info);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,10 +350,15 @@ impl TerminalIntegrationsManager {
|
|||
}
|
||||
|
||||
/// Check if a specific terminal is installed
|
||||
async fn check_terminal_installation(&self, emulator: &TerminalEmulator, config: &TerminalConfig) -> TerminalIntegrationInfo {
|
||||
async fn check_terminal_installation(
|
||||
&self,
|
||||
emulator: &TerminalEmulator,
|
||||
config: &TerminalConfig,
|
||||
) -> TerminalIntegrationInfo {
|
||||
let installed = config.executable_path.exists();
|
||||
let version = if installed {
|
||||
self.get_terminal_version(emulator, &config.executable_path).await
|
||||
self.get_terminal_version(emulator, &config.executable_path)
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -328,31 +367,39 @@ impl TerminalIntegrationsManager {
|
|||
emulator: *emulator,
|
||||
installed,
|
||||
version,
|
||||
path: if installed { Some(config.executable_path.clone()) } else { None },
|
||||
path: if installed {
|
||||
Some(config.executable_path.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
is_default: false,
|
||||
config: if installed { Some(config.clone()) } else { None },
|
||||
config: if installed {
|
||||
Some(config.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get terminal version
|
||||
async fn get_terminal_version(&self, emulator: &TerminalEmulator, path: &PathBuf) -> Option<String> {
|
||||
async fn get_terminal_version(
|
||||
&self,
|
||||
emulator: &TerminalEmulator,
|
||||
path: &PathBuf,
|
||||
) -> Option<String> {
|
||||
match emulator {
|
||||
TerminalEmulator::WezTerm => {
|
||||
Command::new(path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
.map(|v| v.trim().to_string())
|
||||
}
|
||||
TerminalEmulator::Alacritty => {
|
||||
Command::new(path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
.map(|v| v.trim().to_string())
|
||||
}
|
||||
TerminalEmulator::WezTerm => Command::new(path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
.map(|v| v.trim().to_string()),
|
||||
TerminalEmulator::Alacritty => Command::new(path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
.map(|v| v.trim().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -413,10 +460,12 @@ impl TerminalIntegrationsManager {
|
|||
|
||||
// Notify user
|
||||
if let Some(notification_manager) = &self.notification_manager {
|
||||
let _ = notification_manager.notify_success(
|
||||
"Default Terminal Changed",
|
||||
&format!("Default terminal set to {}", emulator.display_name())
|
||||
).await;
|
||||
let _ = notification_manager
|
||||
.notify_success(
|
||||
"Default Terminal Changed",
|
||||
&format!("Default terminal set to {}", emulator.display_name()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -461,7 +510,8 @@ impl TerminalIntegrationsManager {
|
|||
options: TerminalLaunchOptions,
|
||||
) -> Result<(), String> {
|
||||
let configs = self.configs.read().await;
|
||||
let config = configs.get(&emulator)
|
||||
let config = configs
|
||||
.get(&emulator)
|
||||
.ok_or_else(|| "Terminal configuration not found".to_string())?;
|
||||
|
||||
let mut command = Command::new(&config.executable_path);
|
||||
|
|
@ -488,7 +538,8 @@ impl TerminalIntegrationsManager {
|
|||
}
|
||||
|
||||
// Launch terminal
|
||||
command.spawn()
|
||||
command
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to launch terminal: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -503,11 +554,16 @@ impl TerminalIntegrationsManager {
|
|||
script.push_str(" activate\n");
|
||||
|
||||
if options.tab {
|
||||
script.push_str(" tell application \"System Events\" to keystroke \"t\" using command down\n");
|
||||
script.push_str(
|
||||
" tell application \"System Events\" to keystroke \"t\" using command down\n",
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(cwd) = options.working_directory {
|
||||
script.push_str(&format!(" do script \"cd '{}'\" in front window\n", cwd.display()));
|
||||
script.push_str(&format!(
|
||||
" do script \"cd '{}'\" in front window\n",
|
||||
cwd.display()
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(command) = options.command {
|
||||
|
|
@ -516,7 +572,10 @@ impl TerminalIntegrationsManager {
|
|||
} else {
|
||||
format!("{} {}", command, options.args.join(" "))
|
||||
};
|
||||
script.push_str(&format!(" do script \"{}\" in front window\n", full_command));
|
||||
script.push_str(&format!(
|
||||
" do script \"{}\" in front window\n",
|
||||
full_command
|
||||
));
|
||||
}
|
||||
|
||||
script.push_str("end tell\n");
|
||||
|
|
@ -552,7 +611,8 @@ impl TerminalIntegrationsManager {
|
|||
}
|
||||
}
|
||||
|
||||
command.spawn()
|
||||
command
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to launch Windows Terminal: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -565,7 +625,7 @@ impl TerminalIntegrationsManager {
|
|||
|
||||
// Try common terminal emulators
|
||||
let terminals = ["gnome-terminal", "konsole", "xfce4-terminal", "xterm"];
|
||||
|
||||
|
||||
for terminal in &terminals {
|
||||
if let Ok(output) = Command::new("which").arg(terminal).output() {
|
||||
if output.status.success() {
|
||||
|
|
@ -600,7 +660,8 @@ impl TerminalIntegrationsManager {
|
|||
}
|
||||
}
|
||||
|
||||
return command.spawn()
|
||||
return command
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to launch terminal: {}", e))
|
||||
.map(|_| ());
|
||||
}
|
||||
|
|
@ -620,7 +681,8 @@ impl TerminalIntegrationsManager {
|
|||
) -> Option<String> {
|
||||
let schemes = self.url_schemes.read().await;
|
||||
schemes.get(&emulator).map(|scheme| {
|
||||
scheme.template
|
||||
scheme
|
||||
.template
|
||||
.replace("{user}", user)
|
||||
.replace("{host}", host)
|
||||
.replace("{port}", &port.to_string())
|
||||
|
|
@ -639,11 +701,20 @@ impl TerminalIntegrationsManager {
|
|||
|
||||
/// List detected terminals
|
||||
pub async fn list_detected_terminals(&self) -> Vec<TerminalIntegrationInfo> {
|
||||
self.detected_terminals.read().await.values().cloned().collect()
|
||||
self.detected_terminals
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
fn replace_template_variables(&self, template: &str, options: &TerminalLaunchOptions) -> String {
|
||||
fn replace_template_variables(
|
||||
&self,
|
||||
template: &str,
|
||||
options: &TerminalLaunchOptions,
|
||||
) -> String {
|
||||
let mut result = template.to_string();
|
||||
|
||||
if let Some(cwd) = &options.working_directory {
|
||||
|
|
@ -677,4 +748,4 @@ pub struct TerminalIntegrationStats {
|
|||
pub installed_terminals: usize,
|
||||
pub default_terminal: TerminalEmulator,
|
||||
pub terminals_by_platform: HashMap<String, Vec<TerminalEmulator>>,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use tokio::sync::mpsc;
|
||||
use std::sync::Arc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Request to spawn a terminal
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -23,17 +23,20 @@ pub struct TerminalSpawnResponse {
|
|||
/// Terminal Spawn Service - manages background terminal spawning
|
||||
pub struct TerminalSpawnService {
|
||||
request_tx: mpsc::Sender<TerminalSpawnRequest>,
|
||||
#[allow(dead_code)]
|
||||
terminal_integrations_manager: Arc<crate::terminal_integrations::TerminalIntegrationsManager>,
|
||||
}
|
||||
|
||||
impl TerminalSpawnService {
|
||||
pub fn new(
|
||||
terminal_integrations_manager: Arc<crate::terminal_integrations::TerminalIntegrationsManager>,
|
||||
terminal_integrations_manager: Arc<
|
||||
crate::terminal_integrations::TerminalIntegrationsManager,
|
||||
>,
|
||||
) -> Self {
|
||||
let (tx, mut rx) = mpsc::channel::<TerminalSpawnRequest>(100);
|
||||
|
||||
|
||||
let manager_clone = terminal_integrations_manager.clone();
|
||||
|
||||
|
||||
// Spawn background worker to handle terminal spawn requests
|
||||
tokio::spawn(async move {
|
||||
while let Some(request) = rx.recv().await {
|
||||
|
|
@ -43,23 +46,27 @@ impl TerminalSpawnService {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Self {
|
||||
request_tx: tx,
|
||||
terminal_integrations_manager,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Queue a terminal spawn request
|
||||
pub async fn spawn_terminal(&self, request: TerminalSpawnRequest) -> Result<(), String> {
|
||||
self.request_tx.send(request).await
|
||||
self.request_tx
|
||||
.send(request)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to queue terminal spawn: {}", e))
|
||||
}
|
||||
|
||||
|
||||
/// Handle a spawn request
|
||||
async fn handle_spawn_request(
|
||||
request: TerminalSpawnRequest,
|
||||
terminal_integrations_manager: Arc<crate::terminal_integrations::TerminalIntegrationsManager>,
|
||||
terminal_integrations_manager: Arc<
|
||||
crate::terminal_integrations::TerminalIntegrationsManager,
|
||||
>,
|
||||
) -> Result<TerminalSpawnResponse, String> {
|
||||
// Determine which terminal to use
|
||||
let terminal_type = if let Some(terminal) = &request.terminal_type {
|
||||
|
|
@ -78,11 +85,13 @@ impl TerminalSpawnService {
|
|||
} else {
|
||||
terminal_integrations_manager.get_default_terminal().await
|
||||
};
|
||||
|
||||
|
||||
// Build launch options
|
||||
let mut launch_options = crate::terminal_integrations::TerminalLaunchOptions {
|
||||
command: request.command,
|
||||
working_directory: request.working_directory.map(|s| std::path::PathBuf::from(s)),
|
||||
working_directory: request
|
||||
.working_directory
|
||||
.map(|s| std::path::PathBuf::from(s)),
|
||||
args: vec![],
|
||||
env_vars: request.environment.unwrap_or_default(),
|
||||
title: Some(format!("VibeTunnel Session {}", request.session_id)),
|
||||
|
|
@ -91,16 +100,22 @@ impl TerminalSpawnService {
|
|||
split: None,
|
||||
window_size: None,
|
||||
};
|
||||
|
||||
|
||||
// If no command specified, create a VibeTunnel session command
|
||||
if launch_options.command.is_none() {
|
||||
// Get server status to build the correct URL
|
||||
let port = 4020; // Default port, should get from settings
|
||||
launch_options.command = Some(format!("vt connect localhost:{}/{}", port, request.session_id));
|
||||
launch_options.command = Some(format!(
|
||||
"vt connect localhost:{}/{}",
|
||||
port, request.session_id
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Launch the terminal
|
||||
match terminal_integrations_manager.launch_terminal(Some(terminal_type), launch_options).await {
|
||||
match terminal_integrations_manager
|
||||
.launch_terminal(Some(terminal_type), launch_options)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(TerminalSpawnResponse {
|
||||
success: true,
|
||||
error: None,
|
||||
|
|
@ -113,7 +128,7 @@ impl TerminalSpawnService {
|
|||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Spawn terminal for a specific session
|
||||
pub async fn spawn_terminal_for_session(
|
||||
&self,
|
||||
|
|
@ -127,10 +142,10 @@ impl TerminalSpawnService {
|
|||
working_directory: None,
|
||||
environment: None,
|
||||
};
|
||||
|
||||
|
||||
self.spawn_terminal(request).await
|
||||
}
|
||||
|
||||
|
||||
/// Spawn terminal with custom command
|
||||
pub async fn spawn_terminal_with_command(
|
||||
&self,
|
||||
|
|
@ -145,7 +160,7 @@ impl TerminalSpawnService {
|
|||
working_directory,
|
||||
environment: None,
|
||||
};
|
||||
|
||||
|
||||
self.spawn_terminal(request).await
|
||||
}
|
||||
}
|
||||
|
|
@ -158,7 +173,9 @@ pub async fn spawn_terminal_for_session(
|
|||
state: tauri::State<'_, crate::state::AppState>,
|
||||
) -> Result<(), String> {
|
||||
let spawn_service = &state.terminal_spawn_service;
|
||||
spawn_service.spawn_terminal_for_session(session_id, terminal_type).await
|
||||
spawn_service
|
||||
.spawn_terminal_for_session(session_id, terminal_type)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -169,7 +186,9 @@ pub async fn spawn_terminal_with_command(
|
|||
state: tauri::State<'_, crate::state::AppState>,
|
||||
) -> Result<(), String> {
|
||||
let spawn_service = &state.terminal_spawn_service;
|
||||
spawn_service.spawn_terminal_with_command(command, working_directory, terminal_type).await
|
||||
spawn_service
|
||||
.spawn_terminal_with_command(command, working_directory, terminal_type)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -179,4 +198,4 @@ pub async fn spawn_custom_terminal(
|
|||
) -> Result<(), String> {
|
||||
let spawn_service = &state.terminal_spawn_service;
|
||||
spawn_service.spawn_terminal(request).await
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use tauri::menu::{Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
use tauri::AppHandle;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder, Menu};
|
||||
|
||||
pub struct TrayMenuManager;
|
||||
|
||||
|
|
@ -10,35 +10,33 @@ impl TrayMenuManager {
|
|||
.id("server_status")
|
||||
.enabled(false)
|
||||
.build(app)?;
|
||||
|
||||
|
||||
// Dashboard access
|
||||
let dashboard = MenuItemBuilder::new("Open Dashboard")
|
||||
.id("dashboard")
|
||||
.build(app)?;
|
||||
|
||||
|
||||
// Session info
|
||||
let sessions_info = MenuItemBuilder::new("0 active sessions")
|
||||
.id("sessions_info")
|
||||
.enabled(false)
|
||||
.build(app)?;
|
||||
|
||||
|
||||
// Help submenu
|
||||
let show_tutorial = MenuItemBuilder::new("Show Tutorial")
|
||||
.id("show_tutorial")
|
||||
.build(app)?;
|
||||
|
||||
let website = MenuItemBuilder::new("Website")
|
||||
.id("website")
|
||||
.build(app)?;
|
||||
|
||||
|
||||
let website = MenuItemBuilder::new("Website").id("website").build(app)?;
|
||||
|
||||
let report_issue = MenuItemBuilder::new("Report Issue")
|
||||
.id("report_issue")
|
||||
.build(app)?;
|
||||
|
||||
|
||||
let check_updates = MenuItemBuilder::new("Check for Updates...")
|
||||
.id("check_updates")
|
||||
.build(app)?;
|
||||
|
||||
|
||||
// Version info (disabled menu item) - read from Cargo.toml
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let version_text = format!("Version {}", version);
|
||||
|
|
@ -46,11 +44,11 @@ impl TrayMenuManager {
|
|||
.id("version_info")
|
||||
.enabled(false)
|
||||
.build(app)?;
|
||||
|
||||
|
||||
let about = MenuItemBuilder::new("About VibeTunnel")
|
||||
.id("about")
|
||||
.build(app)?;
|
||||
|
||||
|
||||
let help_menu = SubmenuBuilder::new(app, "Help")
|
||||
.item(&show_tutorial)
|
||||
.separator()
|
||||
|
|
@ -63,17 +61,15 @@ impl TrayMenuManager {
|
|||
.separator()
|
||||
.item(&about)
|
||||
.build()?;
|
||||
|
||||
|
||||
// Settings
|
||||
let settings = MenuItemBuilder::new("Settings...")
|
||||
.id("settings")
|
||||
.build(app)?;
|
||||
|
||||
|
||||
// Quit
|
||||
let quit = MenuItemBuilder::new("Quit")
|
||||
.id("quit")
|
||||
.build(app)?;
|
||||
|
||||
let quit = MenuItemBuilder::new("Quit").id("quit").build(app)?;
|
||||
|
||||
// Build the complete menu - matching Mac app exactly
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(&server_status)
|
||||
|
|
@ -86,10 +82,10 @@ impl TrayMenuManager {
|
|||
.separator()
|
||||
.item(&quit)
|
||||
.build()?;
|
||||
|
||||
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
|
||||
pub async fn update_server_status(app: &AppHandle, port: u16, running: bool) {
|
||||
if let Some(_tray) = app.tray_by_id("main") {
|
||||
let status_text = if running {
|
||||
|
|
@ -97,16 +93,16 @@ impl TrayMenuManager {
|
|||
} else {
|
||||
"Server: Stopped".to_string()
|
||||
};
|
||||
|
||||
|
||||
// Note: In Tauri v2, dynamic menu updates require rebuilding the menu
|
||||
// For now, we'll just log the status
|
||||
tracing::debug!("Server status: {}", status_text);
|
||||
|
||||
|
||||
// TODO: Implement menu rebuilding for dynamic updates
|
||||
// This would involve recreating the entire menu with updated text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn update_session_count(app: &AppHandle, count: usize) {
|
||||
if let Some(_tray) = app.tray_by_id("main") {
|
||||
let text = if count == 0 {
|
||||
|
|
@ -116,13 +112,13 @@ impl TrayMenuManager {
|
|||
} else {
|
||||
format!("{} active sessions", count)
|
||||
};
|
||||
|
||||
|
||||
tracing::debug!("Session count: {}", text);
|
||||
|
||||
|
||||
// TODO: Implement menu rebuilding for dynamic updates
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn update_access_mode(_app: &AppHandle, mode: &str) {
|
||||
// Update checkmarks in access mode menu
|
||||
let _modes = vec![
|
||||
|
|
@ -130,10 +126,10 @@ impl TrayMenuManager {
|
|||
("access_network", mode == "network"),
|
||||
("access_ngrok", mode == "ngrok"),
|
||||
];
|
||||
|
||||
|
||||
// Note: In Tauri v2, we need to rebuild the menu to update checkmarks
|
||||
tracing::debug!("Access mode updated to: {}", mode);
|
||||
|
||||
|
||||
// TODO: Implement menu rebuilding for dynamic updates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock, oneshot};
|
||||
use std::io::{Read, Write};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
use uuid::Uuid;
|
||||
use tracing::{info, error};
|
||||
use bytes::Bytes;
|
||||
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::{mpsc, oneshot, RwLock};
|
||||
use tracing::{error, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Represents a forwarded TTY session
|
||||
pub struct ForwardedSession {
|
||||
|
|
@ -42,16 +42,17 @@ impl TTYForwardManager {
|
|||
shell: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
|
||||
|
||||
// Create TCP listener
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", local_port))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to bind to port {}: {}", local_port, e))?;
|
||||
|
||||
let actual_port = listener.local_addr()
|
||||
|
||||
let actual_port = listener
|
||||
.local_addr()
|
||||
.map_err(|e| format!("Failed to get local address: {}", e))?
|
||||
.port();
|
||||
|
||||
|
||||
// Create session
|
||||
let session = ForwardedSession {
|
||||
id: id.clone(),
|
||||
|
|
@ -61,14 +62,14 @@ impl TTYForwardManager {
|
|||
connected: false,
|
||||
client_count: 0,
|
||||
};
|
||||
|
||||
|
||||
// Store session
|
||||
self.sessions.write().await.insert(id.clone(), session);
|
||||
|
||||
|
||||
// Create shutdown channel
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
self.listeners.write().await.insert(id.clone(), shutdown_tx);
|
||||
|
||||
|
||||
// Start listening for connections
|
||||
let sessions = self.sessions.clone();
|
||||
let session_id = id.clone();
|
||||
|
|
@ -81,7 +82,7 @@ impl TTYForwardManager {
|
|||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
Self::accept_connections(
|
||||
listener,
|
||||
|
|
@ -91,9 +92,10 @@ impl TTYForwardManager {
|
|||
remote_port,
|
||||
shell,
|
||||
shutdown_rx,
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
|
||||
info!("Started TTY forward on port {} (ID: {})", actual_port, id);
|
||||
Ok(id)
|
||||
}
|
||||
|
|
@ -114,18 +116,18 @@ impl TTYForwardManager {
|
|||
match accept_result {
|
||||
Ok((stream, addr)) => {
|
||||
info!("New TTY forward connection from {}", addr);
|
||||
|
||||
|
||||
// Update client count
|
||||
if let Some(session) = sessions.write().await.get_mut(&session_id) {
|
||||
session.client_count += 1;
|
||||
session.connected = true;
|
||||
}
|
||||
|
||||
|
||||
// Handle the connection
|
||||
let sessions_clone = sessions.clone();
|
||||
let session_id_clone = session_id.clone();
|
||||
let shell_clone = shell.clone();
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::handle_client(
|
||||
stream,
|
||||
|
|
@ -135,7 +137,7 @@ impl TTYForwardManager {
|
|||
).await {
|
||||
error!("Error handling TTY forward client: {}", e);
|
||||
}
|
||||
|
||||
|
||||
// Decrease client count
|
||||
if let Some(session) = sessions_clone.write().await.get_mut(&session_id_clone) {
|
||||
session.client_count = session.client_count.saturating_sub(1);
|
||||
|
|
@ -188,7 +190,7 @@ impl TTYForwardManager {
|
|||
.master
|
||||
.try_clone_reader()
|
||||
.map_err(|e| format!("Failed to clone reader: {}", e))?;
|
||||
|
||||
|
||||
let mut writer = pty_pair
|
||||
.master
|
||||
.take_writer()
|
||||
|
|
@ -221,7 +223,7 @@ impl TTYForwardManager {
|
|||
}
|
||||
});
|
||||
|
||||
// Task 2: Read from PTY and write to TCP
|
||||
// Task 2: Read from PTY and write to TCP
|
||||
let pty_to_tcp = tokio::spawn(async move {
|
||||
while let Some(data) = rx_from_pty.recv().await {
|
||||
if tcp_writer.write_all(&data).await.is_err() {
|
||||
|
|
@ -262,7 +264,7 @@ impl TTYForwardManager {
|
|||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
||||
rt.block_on(async {
|
||||
while let Some(data) = rx_from_tcp.recv().await {
|
||||
if writer.write_all(&data).is_err() {
|
||||
|
|
@ -293,19 +295,21 @@ impl TTYForwardManager {
|
|||
pub async fn stop_forward(&self, id: &str) -> Result<(), String> {
|
||||
// Remove session
|
||||
self.sessions.write().await.remove(id);
|
||||
|
||||
|
||||
// Send shutdown signal
|
||||
if let Some(shutdown_tx) = self.listeners.write().await.remove(id) {
|
||||
let _ = shutdown_tx.send(());
|
||||
}
|
||||
|
||||
|
||||
info!("Stopped TTY forward session: {}", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all active forwarding sessions
|
||||
pub async fn list_forwards(&self) -> Vec<ForwardedSession> {
|
||||
self.sessions.read().await
|
||||
self.sessions
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.map(|s| ForwardedSession {
|
||||
id: s.id.clone(),
|
||||
|
|
@ -320,36 +324,38 @@ impl TTYForwardManager {
|
|||
|
||||
/// Get a specific forwarding session
|
||||
pub async fn get_forward(&self, id: &str) -> Option<ForwardedSession> {
|
||||
self.sessions.read().await.get(id).map(|s| ForwardedSession {
|
||||
id: s.id.clone(),
|
||||
local_port: s.local_port,
|
||||
remote_host: s.remote_host.clone(),
|
||||
remote_port: s.remote_port,
|
||||
connected: s.connected,
|
||||
client_count: s.client_count,
|
||||
})
|
||||
self.sessions
|
||||
.read()
|
||||
.await
|
||||
.get(id)
|
||||
.map(|s| ForwardedSession {
|
||||
id: s.id.clone(),
|
||||
local_port: s.local_port,
|
||||
remote_host: s.remote_host.clone(),
|
||||
remote_port: s.remote_port,
|
||||
connected: s.connected,
|
||||
client_count: s.client_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP endpoint handler for terminal spawn requests
|
||||
pub async fn handle_terminal_spawn(
|
||||
port: u16,
|
||||
_shell: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
pub async fn handle_terminal_spawn(port: u16, _shell: Option<String>) -> Result<(), String> {
|
||||
// Listen for HTTP requests on the specified port
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to bind spawn listener: {}", e))?;
|
||||
|
||||
|
||||
info!("Terminal spawn service listening on port {}", port);
|
||||
|
||||
|
||||
loop {
|
||||
let (stream, addr) = listener.accept()
|
||||
let (stream, addr) = listener
|
||||
.accept()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to accept spawn connection: {}", e))?;
|
||||
|
||||
|
||||
info!("Terminal spawn request from {}", addr);
|
||||
|
||||
|
||||
// Handle the spawn request
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_spawn_request(stream, None).await {
|
||||
|
|
@ -360,18 +366,16 @@ pub async fn handle_terminal_spawn(
|
|||
}
|
||||
|
||||
/// Handle a single terminal spawn request
|
||||
async fn handle_spawn_request(
|
||||
mut stream: TcpStream,
|
||||
_shell: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
async fn handle_spawn_request(mut stream: TcpStream, _shell: Option<String>) -> Result<(), String> {
|
||||
// Simple HTTP response
|
||||
let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nTerminal spawned\r\n";
|
||||
stream.write_all(response)
|
||||
stream
|
||||
.write_all(response)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to write response: {}", e))?;
|
||||
|
||||
|
||||
// TODO: Implement actual terminal spawning logic
|
||||
// This would integrate with the system's terminal emulator
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use chrono::{DateTime, Utc, TimeZone};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Update channel type
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
|
|
@ -23,7 +23,7 @@ impl UpdateChannel {
|
|||
UpdateChannel::Custom => "custom",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"stable" => UpdateChannel::Stable,
|
||||
|
|
@ -155,7 +155,10 @@ impl UpdateManager {
|
|||
}
|
||||
|
||||
/// Set the notification manager
|
||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
||||
pub fn set_notification_manager(
|
||||
&mut self,
|
||||
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||
) {
|
||||
self.notification_manager = Some(notification_manager);
|
||||
}
|
||||
|
||||
|
|
@ -166,12 +169,13 @@ impl UpdateManager {
|
|||
let mut updater_settings = self.settings.write().await;
|
||||
updater_settings.channel = UpdateChannel::from_str(&update_settings.channel);
|
||||
updater_settings.check_on_startup = true;
|
||||
updater_settings.check_interval_hours = match update_settings.check_frequency.as_str() {
|
||||
"daily" => 24,
|
||||
"weekly" => 168,
|
||||
"monthly" => 720,
|
||||
_ => 24,
|
||||
};
|
||||
updater_settings.check_interval_hours =
|
||||
match update_settings.check_frequency.as_str() {
|
||||
"daily" => 24,
|
||||
"weekly" => 168,
|
||||
"monthly" => 720,
|
||||
_ => 24,
|
||||
};
|
||||
updater_settings.auto_download = update_settings.auto_download;
|
||||
updater_settings.auto_install = update_settings.auto_install;
|
||||
updater_settings.show_release_notes = update_settings.show_release_notes;
|
||||
|
|
@ -189,7 +193,7 @@ impl UpdateManager {
|
|||
/// Update settings
|
||||
pub async fn update_settings(&self, settings: UpdaterSettings) -> Result<(), String> {
|
||||
*self.settings.write().await = settings.clone();
|
||||
|
||||
|
||||
// Save to persistent settings
|
||||
if let Ok(mut app_settings) = crate::settings::Settings::load() {
|
||||
app_settings.updates = Some(crate::settings::UpdateSettings {
|
||||
|
|
@ -207,7 +211,7 @@ impl UpdateManager {
|
|||
});
|
||||
app_settings.save()?;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -224,30 +228,37 @@ impl UpdateManager {
|
|||
state.status = UpdateStatus::Checking;
|
||||
state.last_error = None;
|
||||
}
|
||||
|
||||
|
||||
// Emit checking event
|
||||
self.emit_update_event("checking", None).await;
|
||||
|
||||
|
||||
let app_handle_guard = self.app_handle.read().await;
|
||||
let app_handle = app_handle_guard.as_ref()
|
||||
let app_handle = app_handle_guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| "App handle not set".to_string())?;
|
||||
|
||||
|
||||
// Get the updater instance
|
||||
let updater = app_handle.updater_builder();
|
||||
|
||||
|
||||
// Configure updater based on settings
|
||||
let settings = self.settings.read().await;
|
||||
|
||||
|
||||
// Build updater with channel-specific endpoint
|
||||
let updater_result = match settings.channel {
|
||||
UpdateChannel::Stable => updater.endpoints(vec![
|
||||
"https://releases.vibetunnel.com/stable/{{target}}/{{arch}}/{{current_version}}".parse().unwrap()
|
||||
"https://releases.vibetunnel.com/stable/{{target}}/{{arch}}/{{current_version}}"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
]),
|
||||
UpdateChannel::Beta => updater.endpoints(vec![
|
||||
"https://releases.vibetunnel.com/beta/{{target}}/{{arch}}/{{current_version}}".parse().unwrap()
|
||||
"https://releases.vibetunnel.com/beta/{{target}}/{{arch}}/{{current_version}}"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
]),
|
||||
UpdateChannel::Nightly => updater.endpoints(vec![
|
||||
"https://releases.vibetunnel.com/nightly/{{target}}/{{arch}}/{{current_version}}".parse().unwrap()
|
||||
"https://releases.vibetunnel.com/nightly/{{target}}/{{arch}}/{{current_version}}"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
]),
|
||||
UpdateChannel::Custom => {
|
||||
if let Some(endpoint) = &settings.custom_endpoint {
|
||||
|
|
@ -260,90 +271,97 @@ impl UpdateManager {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Build and check
|
||||
match updater_result {
|
||||
Ok(updater_builder) => match updater_builder.build() {
|
||||
Ok(updater) => {
|
||||
match updater.check().await {
|
||||
Ok(Some(update)) => {
|
||||
let update_info = UpdateInfo {
|
||||
version: update.version.clone(),
|
||||
notes: update.body.clone().unwrap_or_default(),
|
||||
pub_date: update.date.map(|d| Utc.timestamp_opt(d.unix_timestamp(), 0).single().unwrap_or(Utc::now())),
|
||||
download_size: None, // TODO: Get from update
|
||||
signature: None,
|
||||
download_url: String::new(), // Will be set by updater
|
||||
channel: settings.channel,
|
||||
};
|
||||
|
||||
// Update state
|
||||
{
|
||||
match updater.check().await {
|
||||
Ok(Some(update)) => {
|
||||
let update_info = UpdateInfo {
|
||||
version: update.version.clone(),
|
||||
notes: update.body.clone().unwrap_or_default(),
|
||||
pub_date: update.date.map(|d| {
|
||||
Utc.timestamp_opt(d.unix_timestamp(), 0)
|
||||
.single()
|
||||
.unwrap_or(Utc::now())
|
||||
}),
|
||||
download_size: None, // TODO: Get from update
|
||||
signature: None,
|
||||
download_url: String::new(), // Will be set by updater
|
||||
channel: settings.channel,
|
||||
};
|
||||
|
||||
// Update state
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::Available;
|
||||
state.available_update = Some(update_info.clone());
|
||||
state.last_check = Some(Utc::now());
|
||||
}
|
||||
|
||||
// Emit available event
|
||||
self.emit_update_event("available", Some(&update_info))
|
||||
.await;
|
||||
|
||||
// Show notification
|
||||
if let Some(notification_manager) = &self.notification_manager {
|
||||
let _ = notification_manager
|
||||
.notify_update_available(
|
||||
&update_info.version,
|
||||
&update_info.download_url,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Auto-download if enabled
|
||||
if settings.auto_download {
|
||||
let _ = self.download_update().await;
|
||||
}
|
||||
|
||||
Ok(Some(update_info))
|
||||
}
|
||||
Ok(None) => {
|
||||
// No update available
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::Available;
|
||||
state.available_update = Some(update_info.clone());
|
||||
state.status = UpdateStatus::NoUpdate;
|
||||
state.last_check = Some(Utc::now());
|
||||
|
||||
self.emit_update_event("no-update", None).await;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Emit available event
|
||||
self.emit_update_event("available", Some(&update_info)).await;
|
||||
|
||||
// Show notification
|
||||
if let Some(notification_manager) = &self.notification_manager {
|
||||
let _ = notification_manager.notify_update_available(
|
||||
&update_info.version,
|
||||
&update_info.download_url
|
||||
).await;
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to check for updates: {}", e);
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::Error;
|
||||
state.last_error = Some(error_msg.clone());
|
||||
state.last_check = Some(Utc::now());
|
||||
|
||||
self.emit_update_event("error", None).await;
|
||||
|
||||
Err(error_msg)
|
||||
}
|
||||
|
||||
// Auto-download if enabled
|
||||
if settings.auto_download {
|
||||
let _ = self.download_update().await;
|
||||
}
|
||||
|
||||
Ok(Some(update_info))
|
||||
}
|
||||
Ok(None) => {
|
||||
// No update available
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::NoUpdate;
|
||||
state.last_check = Some(Utc::now());
|
||||
|
||||
self.emit_update_event("no-update", None).await;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to check for updates: {}", e);
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::Error;
|
||||
state.last_error = Some(error_msg.clone());
|
||||
state.last_check = Some(Utc::now());
|
||||
|
||||
self.emit_update_event("error", None).await;
|
||||
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to build updater: {}", e);
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::Error;
|
||||
state.last_error = Some(error_msg.clone());
|
||||
|
||||
Err(error_msg)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to build updater: {}", e);
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::Error;
|
||||
state.last_error = Some(error_msg.clone());
|
||||
|
||||
Err(error_msg)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to configure updater endpoints: {}", e);
|
||||
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::Error;
|
||||
state.last_error = Some(error_msg.clone());
|
||||
|
||||
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
|
|
@ -355,11 +373,11 @@ impl UpdateManager {
|
|||
let state = self.state.read().await;
|
||||
state.available_update.is_some()
|
||||
};
|
||||
|
||||
|
||||
if !update_available {
|
||||
return Err("No update available to download".to_string());
|
||||
}
|
||||
|
||||
|
||||
// Update status
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
|
|
@ -372,28 +390,28 @@ impl UpdateManager {
|
|||
eta_seconds: None,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
self.emit_update_event("downloading", None).await;
|
||||
|
||||
|
||||
// TODO: Implement actual download with progress tracking
|
||||
// For now, simulate download completion
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
|
||||
// Update status to ready
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::Ready;
|
||||
state.progress = None;
|
||||
}
|
||||
|
||||
|
||||
self.emit_update_event("ready", None).await;
|
||||
|
||||
|
||||
// Auto-install if enabled
|
||||
let settings = self.settings.read().await;
|
||||
if settings.auto_install {
|
||||
let _ = self.install_update().await;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -406,17 +424,17 @@ impl UpdateManager {
|
|||
}
|
||||
state.available_update.clone()
|
||||
};
|
||||
|
||||
|
||||
let update_info = update_info.ok_or_else(|| "No update available".to_string())?;
|
||||
|
||||
|
||||
// Update status
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.status = UpdateStatus::Installing;
|
||||
}
|
||||
|
||||
|
||||
self.emit_update_event("installing", None).await;
|
||||
|
||||
|
||||
// Add to history
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
|
|
@ -430,19 +448,19 @@ impl UpdateManager {
|
|||
notes: Some(update_info.notes.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// TODO: Implement actual installation
|
||||
// For now, return success
|
||||
|
||||
|
||||
self.emit_update_event("installed", None).await;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel update
|
||||
pub async fn cancel_update(&self) -> Result<(), String> {
|
||||
let mut state = self.state.write().await;
|
||||
|
||||
|
||||
match state.status {
|
||||
UpdateStatus::Downloading => {
|
||||
// TODO: Cancel download
|
||||
|
|
@ -459,15 +477,15 @@ impl UpdateManager {
|
|||
let mut settings = self.settings.write().await;
|
||||
settings.channel = channel;
|
||||
drop(settings);
|
||||
|
||||
|
||||
// Save settings
|
||||
self.update_settings(self.get_settings().await).await?;
|
||||
|
||||
|
||||
// Clear current update info when switching channels
|
||||
let mut state = self.state.write().await;
|
||||
state.available_update = None;
|
||||
state.status = UpdateStatus::Idle;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -486,10 +504,11 @@ impl UpdateManager {
|
|||
if !settings.check_on_startup {
|
||||
return;
|
||||
}
|
||||
|
||||
let check_interval = std::time::Duration::from_secs(settings.check_interval_hours as u64 * 3600);
|
||||
|
||||
let check_interval =
|
||||
std::time::Duration::from_secs(settings.check_interval_hours as u64 * 3600);
|
||||
drop(settings);
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let _ = self.check_for_updates().await;
|
||||
|
|
@ -506,7 +525,7 @@ impl UpdateManager {
|
|||
"update": update_info,
|
||||
"state": self.get_state().await,
|
||||
});
|
||||
|
||||
|
||||
let _ = app_handle.emit("updater:event", event_data);
|
||||
}
|
||||
}
|
||||
|
|
@ -520,4 +539,4 @@ pub struct UpdateCheckResult {
|
|||
pub latest_version: Option<String>,
|
||||
pub channel: UpdateChannel,
|
||||
pub checked_at: DateTime<Utc>,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use tauri::{AppHandle, Manager, Emitter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Tutorial step structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -73,7 +73,7 @@ impl WelcomeManager {
|
|||
tutorials: Arc::new(RwLock::new(Vec::new())),
|
||||
app_handle: Arc::new(RwLock::new(None)),
|
||||
};
|
||||
|
||||
|
||||
// Initialize default tutorials
|
||||
tokio::spawn({
|
||||
let tutorials = manager.tutorials.clone();
|
||||
|
|
@ -82,7 +82,7 @@ impl WelcomeManager {
|
|||
*tutorials.write().await = default_tutorials;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ impl WelcomeManager {
|
|||
// Check if this is first launch based on settings
|
||||
let mut state = self.state.write().await;
|
||||
state.first_launch = settings.general.show_welcome_on_startup.unwrap_or(true);
|
||||
|
||||
|
||||
// Mark first launch as false for next time
|
||||
if state.first_launch {
|
||||
state.onboarding_date = Some(Utc::now());
|
||||
|
|
@ -110,13 +110,14 @@ impl WelcomeManager {
|
|||
/// Save welcome state
|
||||
pub async fn save_state(&self) -> Result<(), String> {
|
||||
let state = self.state.read().await;
|
||||
|
||||
|
||||
// Update settings to reflect welcome state
|
||||
if let Ok(mut settings) = crate::settings::Settings::load() {
|
||||
settings.general.show_welcome_on_startup = Some(!state.tutorial_completed && !state.tutorial_skipped);
|
||||
settings.general.show_welcome_on_startup =
|
||||
Some(!state.tutorial_completed && !state.tutorial_skipped);
|
||||
settings.save().map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +139,9 @@ impl WelcomeManager {
|
|||
|
||||
/// Get specific tutorial category
|
||||
pub async fn get_tutorial_category(&self, category_id: &str) -> Option<TutorialCategory> {
|
||||
self.tutorials.read().await
|
||||
self.tutorials
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.find(|c| c.id == category_id)
|
||||
.cloned()
|
||||
|
|
@ -147,31 +150,29 @@ impl WelcomeManager {
|
|||
/// Complete a tutorial step
|
||||
pub async fn complete_step(&self, step_id: &str) -> Result<(), String> {
|
||||
let mut state = self.state.write().await;
|
||||
|
||||
|
||||
if !state.completed_steps.contains(&step_id.to_string()) {
|
||||
state.completed_steps.push(step_id.to_string());
|
||||
|
||||
|
||||
// Check if all steps are completed
|
||||
let tutorials = self.tutorials.read().await;
|
||||
let total_steps: usize = tutorials.iter()
|
||||
.map(|c| c.steps.len())
|
||||
.sum();
|
||||
|
||||
let total_steps: usize = tutorials.iter().map(|c| c.steps.len()).sum();
|
||||
|
||||
if state.completed_steps.len() >= total_steps {
|
||||
state.tutorial_completed = true;
|
||||
}
|
||||
|
||||
|
||||
// Save state
|
||||
drop(state);
|
||||
drop(tutorials);
|
||||
self.save_state().await?;
|
||||
|
||||
|
||||
// Emit progress event
|
||||
if let Some(app_handle) = self.app_handle.read().await.as_ref() {
|
||||
let _ = app_handle.emit("tutorial:step_completed", step_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -181,9 +182,9 @@ impl WelcomeManager {
|
|||
state.tutorial_skipped = true;
|
||||
state.first_launch = false;
|
||||
drop(state);
|
||||
|
||||
|
||||
self.save_state().await?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -194,9 +195,9 @@ impl WelcomeManager {
|
|||
state.tutorial_completed = false;
|
||||
state.tutorial_skipped = false;
|
||||
drop(state);
|
||||
|
||||
|
||||
self.save_state().await?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +213,7 @@ impl WelcomeManager {
|
|||
tauri::WebviewWindowBuilder::new(
|
||||
app_handle,
|
||||
"welcome",
|
||||
tauri::WebviewUrl::App("welcome.html".into())
|
||||
tauri::WebviewUrl::App("welcome.html".into()),
|
||||
)
|
||||
.title("Welcome to VibeTunnel")
|
||||
.inner_size(800.0, 600.0)
|
||||
|
|
@ -224,7 +225,7 @@ impl WelcomeManager {
|
|||
} else {
|
||||
return Err("App handle not set".to_string());
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -408,34 +409,37 @@ VibeTunnel will be ready whenever you need it."#.to_string(),
|
|||
pub async fn get_progress(&self) -> TutorialProgress {
|
||||
let state = self.state.read().await;
|
||||
let tutorials = self.tutorials.read().await;
|
||||
|
||||
let total_steps: usize = tutorials.iter()
|
||||
.map(|c| c.steps.len())
|
||||
.sum();
|
||||
|
||||
|
||||
let total_steps: usize = tutorials.iter().map(|c| c.steps.len()).sum();
|
||||
|
||||
let completed_steps = state.completed_steps.len();
|
||||
let percentage = if total_steps > 0 {
|
||||
(completed_steps as f32 / total_steps as f32 * 100.0) as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
|
||||
TutorialProgress {
|
||||
total_steps,
|
||||
completed_steps,
|
||||
percentage,
|
||||
categories: tutorials.iter().map(|category| {
|
||||
let category_completed = category.steps.iter()
|
||||
.filter(|s| state.completed_steps.contains(&s.id))
|
||||
.count();
|
||||
|
||||
CategoryProgress {
|
||||
category_id: category.id.clone(),
|
||||
category_name: category.name.clone(),
|
||||
total_steps: category.steps.len(),
|
||||
completed_steps: category_completed,
|
||||
}
|
||||
}).collect(),
|
||||
categories: tutorials
|
||||
.iter()
|
||||
.map(|category| {
|
||||
let category_completed = category
|
||||
.steps
|
||||
.iter()
|
||||
.filter(|s| state.completed_steps.contains(&s.id))
|
||||
.count();
|
||||
|
||||
CategoryProgress {
|
||||
category_id: category.id.clone(),
|
||||
category_name: category.name.clone(),
|
||||
total_steps: category.steps.len(),
|
||||
completed_steps: category_completed,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -456,4 +460,4 @@ pub struct CategoryProgress {
|
|||
pub category_name: String,
|
||||
pub total_steps: usize,
|
||||
pub completed_steps: usize,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue