Build 30: Tor up in seconds — probe bridges, keep consensus cache, pre-warm at start; borderless window frame, vector sidebar mark, Wayland app id, v2 icon set
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="600.000000pt" height="601.000000pt" viewBox="0 0 600.000000 601.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<g transform="translate(0.000000,601.000000) scale(0.050000,-0.050000)"
|
||||
fill="#ffffff" stroke="none">
|
||||
<path d="M195 11784 c-515 -1551 98 -2966 1520 -3514 171 -66 171 -72 2 -72
|
||||
-415 0 -893 215 -1273 572 -178 167 -181 163 -77 -83 478 -1130 1770 -1734
|
||||
2963 -1384 283 83 309 101 420 292 376 648 1038 1116 1763 1245 l143 26 100
|
||||
202 c887 1780 -911 3344 -3076 2675 l-170 -52 220 -14 c480 -32 818 -114 1118
|
||||
-273 258 -137 548 -414 624 -597 l32 -76 -87 76 c-435 375 -938 524 -1557 461
|
||||
-273 -28 -340 -39 -740 -120 -893 -182 -1449 24 -1756 650 l-98 200 -71 -214z"/>
|
||||
<path d="M9720 10053 c0 -146 -256 -556 -441 -705 -381 -308 -766 -380 -1559
|
||||
-290 -1209 137 -2026 -255 -2519 -1208 -78 -151 -82 -156 -72 -77 18 140 128
|
||||
450 210 592 43 74 73 135 67 135 -25 0 -397 -184 -512 -253 -966 -580 -1594
|
||||
-1674 -1594 -2777 0 -104 -4 -190 -10 -190 -5 0 -65 43 -132 95 -650 503
|
||||
-1118 775 -1606 935 -290 95 -302 94 -242 -15 52 -95 166 -372 191 -465 9 -33
|
||||
34 -121 56 -195 48 -161 120 -508 194 -935 118 -684 437 -1371 795 -1715 185
|
||||
-178 324 -239 594 -262 144 -13 150 -15 147 -63 -1 -27 -14 -174 -28 -326 -83
|
||||
-902 258 -1568 1037 -2024 139 -81 161 -80 127 4 -37 96 -85 369 -96 546 l-10
|
||||
170 46 -60 c328 -431 869 -753 1497 -891 351 -77 1285 -67 1426 15 9 5 -104
|
||||
50 -250 98 -649 217 -1341 668 -1680 1096 -81 102 -88 147 -11 78 157 -142
|
||||
653 -406 925 -493 783 -249 1542 -242 2332 20 l274 91 106 -83 c298 -236 798
|
||||
-328 1259 -231 197 42 196 38 32 169 -156 124 -344 356 -443 546 l-60 116 70
|
||||
52 c544 408 783 906 627 1300 l-41 102 170 138 c442 355 584 624 813 1537 186
|
||||
742 257 952 452 1328 l118 227 -145 -14 c-693 -68 -1425 -411 -1729 -810 -99
|
||||
-130 -112 -127 -165 44 -54 175 -217 511 -308 636 -64 88 -70 91 -224 117
|
||||
-314 52 -637 184 -956 390 -260 168 -509 436 -280 302 127 -74 302 -160 430
|
||||
-211 97 -38 146 -55 348 -118 335 -105 983 -129 1280 -47 961 264 1554 1048
|
||||
1729 2286 28 199 45 685 23 677 -9 -4 -73 -77 -141 -163 -650 -810 -1594
|
||||
-1147 -2451 -873 -251 80 -246 76 -171 121 395 240 638 767 578 1253 -25 209
|
||||
-77 395 -77 278z m-682 -4962 c306 -158 601 -1396 416 -1747 -118 -224 -283
|
||||
-345 -526 -387 -455 -78 -577 227 -456 1143 96 725 320 1118 566 991z m-3115
|
||||
-156 c371 -172 699 -815 799 -1565 34 -251 19 -307 -108 -422 -575 -519 -1624
|
||||
-162 -1931 657 -265 709 593 1630 1240 1330z m5261 -120 c20 -377 -135 -912
|
||||
-290 -995 -70 -38 -72 -35 -157 230 l-64 200 -24 -90 c-50 -192 -144 -340
|
||||
-215 -340 -111 0 -316 804 -228 893 53 53 200 -130 226 -283 l14 -80 36 105
|
||||
c100 293 222 333 332 109 l54 -110 35 105 c89 260 271 427 281 256z m-8491
|
||||
-284 l75 -230 52 160 c89 272 210 336 295 154 126 -268 210 -757 142 -825 -44
|
||||
-44 -163 120 -247 340 -12 33 -19 24 -39 -50 -89 -330 -286 -346 -374 -30
|
||||
l-22 80 -35 -125 c-54 -196 -223 -428 -275 -377 -37 37 -29 448 11 608 70 276
|
||||
219 524 314 524 17 0 56 -87 103 -229z m4164 -2698 c137 -311 883 -373 1408
|
||||
-116 162 78 171 64 42 -61 -317 -305 -854 -408 -1271 -244 -223 87 -516 338
|
||||
-516 442 0 53 140 267 212 324 l58 46 14 -152 c8 -84 31 -191 53 -239z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -167,12 +167,28 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
|
||||
/// Draw custom desktop window frame content.
|
||||
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||
// Paint the whole window with the theme background first: the frame
|
||||
// margin ring is otherwise transparent, which X11 without a
|
||||
// compositor renders as a black border (and a black strip under the
|
||||
// sidebar in light/yellow themes).
|
||||
// Paint the window area inside the frame margin with the theme
|
||||
// background first: surface gaps are otherwise transparent, which X11
|
||||
// without a compositor renders as black (strip under the sidebar in
|
||||
// light/yellow themes). The margin ring itself must STAY transparent —
|
||||
// painting it gives the window a visible border under a compositor.
|
||||
let fill_rect = if is_fullscreen {
|
||||
ui.max_rect()
|
||||
} else {
|
||||
ui.max_rect().shrink(Content::WINDOW_FRAME_MARGIN)
|
||||
};
|
||||
let fill_rounding = if is_fullscreen {
|
||||
CornerRadius::ZERO
|
||||
} else {
|
||||
CornerRadius {
|
||||
nw: 8,
|
||||
ne: 8,
|
||||
sw: 0,
|
||||
se: 0,
|
||||
}
|
||||
};
|
||||
ui.painter()
|
||||
.rect_filled(ui.max_rect(), CornerRadius::ZERO, Colors::fill());
|
||||
.rect_filled(fill_rect, fill_rounding, Colors::fill());
|
||||
let content_bg_rect = {
|
||||
let mut r = ui.max_rect();
|
||||
if !is_fullscreen {
|
||||
|
||||
@@ -2688,7 +2688,7 @@ pub fn widgets_logo(ui: &mut egui::Ui) {
|
||||
/// Tinted goblin mark at a given size.
|
||||
pub fn widgets_logo_sized(ui: &mut egui::Ui, size: f32) {
|
||||
let (rect, _) = ui.allocate_exact_size(Vec2::splat(size), Sense::hover());
|
||||
let img = egui::Image::new(egui::include_image!("../../../../img/goblin-logo2-256.png"))
|
||||
let img = egui::Image::new(egui::include_image!("../../../../img/goblin-logo2.svg"))
|
||||
.tint(theme::tokens().text)
|
||||
.fit_to_exact_size(Vec2::splat(size));
|
||||
img.paint_at(ui, rect);
|
||||
|
||||
@@ -316,7 +316,7 @@ pub fn qr_code(ui: &mut Ui, text: &str, size: f32) {
|
||||
ui.painter()
|
||||
.rect_filled(b_rect, CornerRadius::same((backing * 0.18) as u8), plate);
|
||||
let m_rect = egui::Rect::from_center_size(rect.center(), Vec2::splat(backing * 0.72));
|
||||
egui::Image::new(egui::include_image!("../../../../img/goblin-logo2-256.png"))
|
||||
egui::Image::new(egui::include_image!("../../../../img/goblin-logo2.svg"))
|
||||
.tint(ink)
|
||||
.fit_to_exact_size(m_rect.size())
|
||||
.paint_at(ui, m_rect);
|
||||
|
||||
@@ -694,7 +694,7 @@ impl View {
|
||||
pub fn app_logo_name_version(ui: &mut egui::Ui) {
|
||||
ui.add_space(-1.0);
|
||||
// Goblin mark (white master, tinted for the current theme).
|
||||
let logo = egui::include_image!("../../../img/goblin-logo2-256.png");
|
||||
let logo = egui::include_image!("../../../img/goblin-logo2.svg");
|
||||
// Show application logo and name.
|
||||
ui.scope(|ui| {
|
||||
ui.set_opacity(0.9);
|
||||
|
||||
@@ -117,6 +117,8 @@ pub fn start(options: NativeOptions, app_creator: eframe::AppCreator) -> eframe:
|
||||
if AppConfig::autostart_node() {
|
||||
Node::start();
|
||||
}
|
||||
// Pre-warm the Tor client so price/NIP-05/nostr are ready at first use.
|
||||
tor::Tor::warm_up();
|
||||
// Launch graphical interface.
|
||||
eframe::run_native("Goblin", options, app_creator)
|
||||
}
|
||||
|
||||
@@ -87,6 +87,9 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
|
||||
let is_mac = os == egui::os::OperatingSystem::Mac;
|
||||
let is_win = os == egui::os::OperatingSystem::Windows;
|
||||
viewport = viewport
|
||||
// Wayland taskbars resolve the icon through the .desktop file whose
|
||||
// name matches this app id (see linux/Goblin.AppDir/goblin.desktop).
|
||||
.with_app_id("goblin")
|
||||
.with_fullsize_content_view(true)
|
||||
.with_window_level(egui::WindowLevel::Normal)
|
||||
.with_title_shown(is_win)
|
||||
|
||||
@@ -27,7 +27,7 @@ use parking_lot::RwLock;
|
||||
use safelog::DisplayRedacted;
|
||||
use sha2::Sha512;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream, ToSocketAddrs};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -172,6 +172,81 @@ impl Tor {
|
||||
(config, bridges, max_two_bridges)
|
||||
}
|
||||
|
||||
/// Endpoint (host, port) a bridge dials first, for a quick reachability probe.
|
||||
/// Webtunnel and obfs4 need a working TCP path to this endpoint before anything
|
||||
/// else can happen; snowflake is CDN-fronted, so it gets no probe (None).
|
||||
fn bridge_probe_addr(bridge: &TorBridge) -> Option<(String, u16)> {
|
||||
let line = bridge.connection_line();
|
||||
match bridge {
|
||||
TorBridge::Webtunnel(_, _) => {
|
||||
let url = line
|
||||
.split_whitespace()
|
||||
.find_map(|t| t.strip_prefix("url="))?;
|
||||
let rest = url
|
||||
.strip_prefix("https://")
|
||||
.or_else(|| url.strip_prefix("http://"))?;
|
||||
let host_port = rest.split('/').next()?;
|
||||
let mut parts = host_port.splitn(2, ':');
|
||||
let host = parts.next()?.to_string();
|
||||
let port = parts.next().and_then(|p| p.parse().ok()).unwrap_or(443);
|
||||
Some((host, port))
|
||||
}
|
||||
TorBridge::Obfs4(_, _) => {
|
||||
let addr = line.split_whitespace().nth(1)?;
|
||||
let mut parts = addr.rsplitn(2, ':');
|
||||
let port = parts.next()?.parse().ok()?;
|
||||
let host = parts
|
||||
.next()?
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
Some((host, port))
|
||||
}
|
||||
TorBridge::Snowflake(_, _) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Order bridges live-endpoint-first via a parallel TCP probe. A dead bridge
|
||||
/// otherwise costs a full bootstrap timeout (60s) per attempt, which reads as
|
||||
/// the wallet being stuck on "connecting…" while the list is ground through.
|
||||
fn sort_bridges_by_reachability(bridges: Vec<TorBridge>) -> Vec<TorBridge> {
|
||||
if bridges.len() < 2 {
|
||||
return bridges;
|
||||
}
|
||||
let probes: Vec<(TorBridge, thread::JoinHandle<bool>)> = bridges
|
||||
.into_iter()
|
||||
.map(|b| {
|
||||
let addr = Self::bridge_probe_addr(&b);
|
||||
let handle = thread::spawn(move || match addr {
|
||||
// No probeable endpoint: assume reachable.
|
||||
None => true,
|
||||
Some((host, port)) => (host.as_str(), port)
|
||||
.to_socket_addrs()
|
||||
.ok()
|
||||
.and_then(|mut addrs| addrs.next())
|
||||
.map(|a| {
|
||||
TcpStream::connect_timeout(&a, Duration::from_millis(4000)).is_ok()
|
||||
})
|
||||
.unwrap_or(false),
|
||||
});
|
||||
(b, handle)
|
||||
})
|
||||
.collect();
|
||||
let mut alive = vec![];
|
||||
let mut dead = vec![];
|
||||
for (b, handle) in probes {
|
||||
if handle.join().unwrap_or(true) {
|
||||
alive.push(b);
|
||||
} else {
|
||||
dead.push(b);
|
||||
}
|
||||
}
|
||||
// Probes can lie on hostile networks — keep dead ones as a last resort
|
||||
// rather than dropping them.
|
||||
alive.extend(dead);
|
||||
alive
|
||||
}
|
||||
|
||||
/// Build bootstrapped client from provided config.
|
||||
fn build_client_bootstrap(config: TorClientConfig) -> Option<TorClient<TokioNativeTlsRuntime>> {
|
||||
let client_res = TorClient::with_runtime(TOR_STATE.runtime.clone())
|
||||
@@ -190,8 +265,10 @@ impl Tor {
|
||||
.runtime()
|
||||
.spawn(async move {
|
||||
let task = c.bootstrap();
|
||||
// Bootstrap client with 60s timeout.
|
||||
if tokio::time::timeout(Duration::from_millis(60000), task)
|
||||
// Bootstrap client with 120s timeout: a cold-cache directory
|
||||
// consensus over a webtunnel bridge regularly needs more than
|
||||
// 60s, and timing it out throws the progress away.
|
||||
if tokio::time::timeout(Duration::from_millis(120000), task)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -222,10 +299,11 @@ impl Tor {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Cleanup keys, state and cache.
|
||||
// Cleanup onion service keys only. State and cache are KEPT: the cache
|
||||
// holds the directory consensus, and wiping it forced a full re-download
|
||||
// over the (slow) bridge on every app start — minutes of "connecting…"
|
||||
// where a warm cache bootstraps in seconds.
|
||||
fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default();
|
||||
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
|
||||
fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
|
||||
TOR_STATE.client_launching.store(true, Ordering::Relaxed);
|
||||
// Get initial bridges.
|
||||
let initial_bridges = if let Some(b) = TorConfig::get_bridge() {
|
||||
@@ -239,7 +317,7 @@ impl Tor {
|
||||
bridge
|
||||
})
|
||||
.collect::<Vec<TorBridge>>();
|
||||
Some(bridges)
|
||||
Some(Self::sort_bridges_by_reachability(bridges))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -271,9 +349,8 @@ impl Tor {
|
||||
TOR_STATE.client_config.write().replace((c, config.clone()));
|
||||
break;
|
||||
} else {
|
||||
// Cleanup state and cache.
|
||||
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
|
||||
fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
|
||||
// Keep state and cache between attempts so directory consensus
|
||||
// progress accumulates instead of restarting from zero.
|
||||
// Check unused bridges to check another part.
|
||||
if !unused_bridges.is_empty() {
|
||||
config_bridges = Self::build_config(Some(unused_bridges));
|
||||
@@ -286,7 +363,8 @@ impl Tor {
|
||||
.iter()
|
||||
.map(|b| TorBridge::Webtunnel(TorConfig::webtunnel_path(), b.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
config_bridges = Self::build_config(Some(add_bridges));
|
||||
config_bridges =
|
||||
Self::build_config(Some(Self::sort_bridges_by_reachability(add_bridges)));
|
||||
continue;
|
||||
} else if TorConfig::get_bridge().is_some() {
|
||||
// Launch without bridges if all attempts failed.
|
||||
@@ -388,6 +466,15 @@ impl Tor {
|
||||
r_client_config.clone()
|
||||
}
|
||||
|
||||
/// Pre-warm the Tor client in the background so it is already bootstrapped
|
||||
/// by the time the first consumer (price feed, NIP-05, nostr relays) needs
|
||||
/// it — otherwise nothing launches it until a wallet finishes opening.
|
||||
pub fn warm_up() {
|
||||
thread::spawn(|| {
|
||||
let _ = Self::isolated_client_blocking();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get an isolated Tor client for outgoing connections (e.g. Nostr relays),
|
||||
/// launching the embedded client first when needed. Blocking: bootstrap can
|
||||
/// take up to a minute, call from a blocking-friendly context.
|
||||
@@ -550,10 +637,9 @@ impl Tor {
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
*w_services = services.clone();
|
||||
}
|
||||
// Cleanup keys, state and cache.
|
||||
// Cleanup onion service keys (services re-save them on relaunch). State
|
||||
// and cache are kept so the restart reuses the directory consensus.
|
||||
fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default();
|
||||
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
|
||||
fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
|
||||
{
|
||||
let mut w_client = TOR_STATE.client_config.write();
|
||||
*w_client = None;
|
||||
|
||||