fix: install/uninstall UI state, progress bar, auto-Tor hidden services
- Install progress bar replaces action buttons (no overlay) - Hide status badge during install/uninstall - Uninstall keeps progress state until container disappears from WebSocket - Uninstall RPC timeout increased to 660s (Bitcoin UTXO flush) - Installing apps appear in My Apps immediately as placeholders - Auto-configure Tor hidden service for every app on install - Widen Tor module visibility for install hooks - Only clear stale install entries on error status Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -801,6 +801,48 @@ autopilot.active=false\n",
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-configure Tor hidden service for protocol services (LND, ElectrumX, Bitcoin)
|
||||
{
|
||||
use crate::api::rpc::tor::{
|
||||
known_service_port, is_protocol_service, load_services_config,
|
||||
save_services_config, regenerate_torrc, restart_tor, wait_for_hostname,
|
||||
sync_single_hostname, TorServiceEntry,
|
||||
};
|
||||
|
||||
let tor_port = known_service_port(package_id);
|
||||
if tor_port > 0 {
|
||||
let config_dir = self.config.data_dir.join("tor-config");
|
||||
let mut config = load_services_config(&config_dir).await;
|
||||
let already_exists = config.services.iter().any(|s| s.name == package_id);
|
||||
|
||||
if !already_exists {
|
||||
let is_proto = is_protocol_service(package_id);
|
||||
config.services.push(TorServiceEntry {
|
||||
name: package_id.to_string(),
|
||||
local_port: tor_port,
|
||||
remote_port: None,
|
||||
unauthenticated: is_proto,
|
||||
enabled: true,
|
||||
});
|
||||
if let Err(e) = save_services_config(&config_dir, &config).await {
|
||||
tracing::warn!("Failed to save Tor config for {}: {}", package_id, e);
|
||||
} else if let Err(e) = regenerate_torrc(&config).await {
|
||||
tracing::warn!("Failed to regenerate torrc for {}: {}", package_id, e);
|
||||
} else if let Err(e) = restart_tor().await {
|
||||
tracing::warn!("Failed to restart Tor for {}: {}", package_id, e);
|
||||
} else {
|
||||
let onion = wait_for_hostname(package_id, 30).await;
|
||||
if let Some(ref addr) = onion {
|
||||
sync_single_hostname(package_id, addr).await;
|
||||
info!("Tor hidden service created for {} → {}", package_id, addr);
|
||||
} else {
|
||||
info!("Tor hidden service created for {} (hostname pending)", package_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if package_id == "nextcloud" {
|
||||
let host_ip = &self.config.host_ip;
|
||||
// Wait for Nextcloud to finish first-run initialization
|
||||
|
||||
@@ -23,12 +23,12 @@ pub(super) struct TorService {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub(super) struct ServicesConfig {
|
||||
pub(in crate::api::rpc) struct ServicesConfig {
|
||||
pub services: Vec<TorServiceEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(super) struct TorServiceEntry {
|
||||
pub(in crate::api::rpc) struct TorServiceEntry {
|
||||
pub name: String,
|
||||
pub local_port: u16,
|
||||
#[serde(default)]
|
||||
@@ -106,7 +106,7 @@ pub(super) async fn rename_hidden_service_dir(name: &str, timestamp: u64) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn restart_tor() -> Result<()> {
|
||||
pub(in crate::api::rpc) async fn restart_tor() -> Result<()> {
|
||||
dispatch_tor_action(serde_json::json!({
|
||||
"action": "write-torrc-and-restart",
|
||||
})).await
|
||||
@@ -131,7 +131,7 @@ pub(super) fn detect_hidden_service_base() -> String {
|
||||
"/var/lib/tor".to_string()
|
||||
}
|
||||
|
||||
pub(super) async fn regenerate_torrc(config: &ServicesConfig) -> Result<()> {
|
||||
pub(in crate::api::rpc) async fn regenerate_torrc(config: &ServicesConfig) -> Result<()> {
|
||||
let base = detect_hidden_service_base();
|
||||
let mut lines = Vec::new();
|
||||
|
||||
@@ -175,7 +175,7 @@ pub(super) async fn regenerate_torrc(config: &ServicesConfig) -> Result<()> {
|
||||
|
||||
// ─── Hostname Sync ───────────────────────────────────────────────
|
||||
|
||||
pub(super) async fn sync_single_hostname(name: &str, address: &str) {
|
||||
pub(in crate::api::rpc) async fn sync_single_hostname(name: &str, address: &str) {
|
||||
let hostnames_dir = Path::new("/var/lib/archipelago/tor-hostnames");
|
||||
if let Err(e) = tokio::fs::create_dir_all(hostnames_dir).await {
|
||||
warn!("Failed to create tor-hostnames dir: {}", e);
|
||||
@@ -306,7 +306,7 @@ fn is_valid_v3_onion(s: &str) -> bool {
|
||||
|
||||
// ─── Known Ports ─────────────────────────────────────────────────
|
||||
|
||||
pub(super) fn known_service_port(name: &str) -> u16 {
|
||||
pub(in crate::api::rpc) fn known_service_port(name: &str) -> u16 {
|
||||
match name {
|
||||
"archipelago" => 80,
|
||||
"bitcoin" | "bitcoin-knots" => 8333,
|
||||
@@ -331,7 +331,7 @@ pub(super) fn known_service_port(name: &str) -> u16 {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_protocol_service(name: &str) -> bool {
|
||||
pub(in crate::api::rpc) fn is_protocol_service(name: &str) -> bool {
|
||||
matches!(name, "bitcoin" | "bitcoin-knots" | "electrs" | "electrumx" | "lnd")
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ fn tor_data_dir() -> String {
|
||||
std::env::var("TOR_DATA_DIR").unwrap_or_else(|_| TOR_DATA_DIR.to_string())
|
||||
}
|
||||
|
||||
pub(super) async fn load_services_config(config_dir: &std::path::Path) -> ServicesConfig {
|
||||
pub(in crate::api::rpc) async fn load_services_config(config_dir: &std::path::Path) -> ServicesConfig {
|
||||
let path = config_dir.join(SERVICES_CONFIG);
|
||||
match tokio::fs::read_to_string(&path).await {
|
||||
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
|
||||
@@ -349,7 +349,7 @@ pub(super) async fn load_services_config(config_dir: &std::path::Path) -> Servic
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn save_services_config(config_dir: &std::path::Path, config: &ServicesConfig) -> Result<()> {
|
||||
pub(in crate::api::rpc) async fn save_services_config(config_dir: &std::path::Path, config: &ServicesConfig) -> Result<()> {
|
||||
tokio::fs::create_dir_all(config_dir).await.context("Failed to create tor config dir")?;
|
||||
let path = config_dir.join(SERVICES_CONFIG);
|
||||
let content = serde_json::to_string_pretty(config).context("Failed to serialize services config")?;
|
||||
@@ -418,7 +418,7 @@ pub(super) async fn notify_federation_peers_address_change(
|
||||
|
||||
// ─── Hostname Waiting ────────────────────────────────────────────
|
||||
|
||||
pub(super) async fn wait_for_hostname(service_name: &str, max_secs: u64) -> Option<String> {
|
||||
pub(in crate::api::rpc) async fn wait_for_hostname(service_name: &str, max_secs: u64) -> Option<String> {
|
||||
for _ in 0..max_secs {
|
||||
if let Some(addr) = read_onion_address(service_name).await {
|
||||
return Some(addr);
|
||||
|
||||
Reference in New Issue
Block a user