summaryrefslogtreecommitdiffstats
path: root/gui/src/net.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--gui/src/net.rs220
1 files changed, 220 insertions, 0 deletions
diff --git a/gui/src/net.rs b/gui/src/net.rs
new file mode 100644
index 0000000..d2bc7e4
--- /dev/null
+++ b/gui/src/net.rs
@@ -0,0 +1,220 @@
1//! Background network worker for the desktop frontend.
2//!
3//! The core is I/O-free, so the adapter owns the TLS stream. One blocking
4//! thread keeps the connection, forwards every server line, and wakes the UI
5//! through `egui::Context::request_repaint`. There is no async runtime: the
6//! protocol has one request in flight plus server pushes.
7
8use std::path::PathBuf;
9use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
10use std::thread;
11use std::time::Duration;
12
13use eframe::egui;
14use rusqlite::Connection;
15use zagan_bet::client::transport::{self, TlsStream, TransportError, TrustPrompt};
16use zagan_bet::client::{database, paths};
17
18/// How often the worker checks the socket for pushes when idle.
19const POLL: Duration = Duration::from_millis(8);
20/// How long a first-contact prompt may stay unanswered.
21const TRUST_TIMEOUT: Duration = Duration::from_secs(120);
22
23/// What the UI asks the worker to do.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Command {
26 Connect { addr: String },
27 Send(String),
28 Disconnect,
29 Stop,
30}
31
32/// Why a connection attempt failed. The UI picks a screen from this.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum Failure {
35 TrustDeclined,
36 KeyChanged { expected: String, observed: String },
37 VersionMismatch,
38 Timeout,
39 Other,
40}
41
42/// A connection that survived trust, version and auto-login.
43#[derive(Debug, Clone, PartialEq)]
44pub struct Connected {
45 pub addr: String,
46 pub user: Option<String>,
47 pub auth_error: Option<String>,
48 pub fingerprint: Option<String>,
49 pub server_version: String,
50 pub latest: String,
51 pub url: String,
52 pub elapsed_ms: f32,
53}
54
55/// What the worker tells the UI.
56pub enum Event {
57 /// First contact: the UI must answer through the sender.
58 Trust {
59 addr: String,
60 fingerprint: String,
61 reply: Sender<bool>,
62 },
63 Connected(Connected),
64 Failed {
65 addr: String,
66 failure: Failure,
67 message: String,
68 },
69 Line(String),
70 Closed,
71}
72
73fn classify(error: &TransportError) -> Failure {
74 match error {
75 TransportError::TrustDeclined => Failure::TrustDeclined,
76 TransportError::KeyChanged { expected, observed } => Failure::KeyChanged {
77 expected: expected.clone(),
78 observed: observed.clone(),
79 },
80 TransportError::VersionMismatch(_) => Failure::VersionMismatch,
81 TransportError::Timeout => Failure::Timeout,
82 _ => Failure::Other,
83 }
84}
85
86/// Handle held by the app. Dropping it stops the worker.
87pub struct Network {
88 commands: Sender<Command>,
89 events: Receiver<Event>,
90}
91
92impl Network {
93 pub fn spawn(ctx: egui::Context, data_dir: PathBuf, unsafe_mode: bool) -> Self {
94 let (commands, command_rx) = mpsc::channel();
95 let (event_tx, events) = mpsc::channel();
96 thread::spawn(move || worker(ctx, data_dir, unsafe_mode, command_rx, event_tx));
97 Self { commands, events }
98 }
99
100 pub fn send(&self, command: Command) {
101 let _ = self.commands.send(command);
102 }
103
104 pub fn try_recv(&self) -> Option<Event> {
105 self.events.try_recv().ok()
106 }
107}
108
109impl Drop for Network {
110 fn drop(&mut self) {
111 let _ = self.commands.send(Command::Stop);
112 }
113}
114
115fn worker(
116 ctx: egui::Context,
117 data_dir: PathBuf,
118 unsafe_mode: bool,
119 commands: Receiver<Command>,
120 events: Sender<Event>,
121) {
122 let db_path = paths::database_path(&data_dir);
123 let conn = match Connection::open(&db_path) {
124 Ok(conn) => {
125 let _ = conn.busy_timeout(Duration::from_secs(2));
126 database::init_db(&conn);
127 Some(conn)
128 }
129 Err(_) => None,
130 };
131
132 let mut stream: Option<TlsStream> = None;
133
134 loop {
135 match commands.recv_timeout(POLL) {
136 Ok(Command::Connect { addr }) => {
137 stream = None;
138 let Some(conn) = &conn else {
139 let _ = events.send(Event::Failed {
140 addr,
141 failure: Failure::Other,
142 message: format!("cannot open {}", db_path.display()),
143 });
144 ctx.request_repaint();
145 continue;
146 };
147 let mut trust = |prompt: &TrustPrompt| {
148 let (reply, decision) = mpsc::channel();
149 let _ = events.send(Event::Trust {
150 addr: prompt.addr.clone(),
151 fingerprint: prompt.fingerprint.clone(),
152 reply,
153 });
154 ctx.request_repaint();
155 decision.recv_timeout(TRUST_TIMEOUT).unwrap_or(false)
156 };
157 match transport::connect(&addr, conn, unsafe_mode, &mut trust) {
158 Ok(connected) => {
159 let fingerprint = database::get_fingerprint(conn, &addr);
160 let _ = events.send(Event::Connected(Connected {
161 addr: addr.clone(),
162 user: connected.user,
163 auth_error: connected.auth_error,
164 fingerprint,
165 server_version: connected.server_version,
166 latest: connected.latest,
167 url: connected.url,
168 elapsed_ms: connected.elapsed.as_secs_f64() as f32 * 1000.0,
169 }));
170 stream = Some(connected.stream);
171 }
172 Err(error) => {
173 let _ = events.send(Event::Failed {
174 addr,
175 failure: classify(&error),
176 message: error.to_string(),
177 });
178 }
179 }
180 ctx.request_repaint();
181 }
182 Ok(Command::Send(line)) => {
183 if let Some(open) = &mut stream {
184 if open.write_line(&line).is_err() {
185 stream = None;
186 let _ = events.send(Event::Closed);
187 }
188 }
189 ctx.request_repaint();
190 }
191 Ok(Command::Disconnect) => {
192 stream = None;
193 ctx.request_repaint();
194 }
195 Ok(Command::Stop) => break,
196 Err(RecvTimeoutError::Timeout) => {}
197 Err(RecvTimeoutError::Disconnected) => break,
198 }
199
200 // Drain pushes and replies without blocking the command channel.
201 if let Some(open) = &mut stream {
202 loop {
203 match open.read_available_line() {
204 Ok(Some(line)) => {
205 if events.send(Event::Line(line)).is_err() {
206 return;
207 }
208 }
209 Ok(None) => break,
210 Err(_) => {
211 stream = None;
212 let _ = events.send(Event::Closed);
213 break;
214 }
215 }
216 }
217 ctx.request_repaint();
218 }
219 }
220}