mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 12:56:47 -06:00
Merge pull request #889 from kk376/fix/857-live-session-large-files
fix(collab-net): allow live sessions above 64 MiB and report accurate transport limits (#857)
This commit is contained in:
@@ -16,7 +16,7 @@ use figcraft_engine::live::{Event, Status};
|
||||
use web_time::Instant;
|
||||
|
||||
use crate::inbox::Inbox;
|
||||
use crate::join::{GuestConfig, Next, Outcome, Resuming, Seen, Taken, after, closed, hello, resolved, take, unresolved};
|
||||
use crate::join::{GuestConfig, Next, Outcome, Resuming, Seen, Taken, after, closed, hello, resolved, take, too_large, unresolved};
|
||||
use crate::throttle::{PRESENCE_INTERVAL, Throttle, earliest};
|
||||
|
||||
/// A WebSocket the driver opens, writes and closes; what it receives comes back through the
|
||||
@@ -114,6 +114,9 @@ impl<S: Socket> Driver<S> {
|
||||
/// A binary message from the host.
|
||||
pub(crate) fn message(&mut self, bytes: &[u8], now: Instant) {
|
||||
let Some(c) = self.conn.as_mut() else { return };
|
||||
if bytes.len() > self.cfg.limits.max_server_message {
|
||||
return self.end(too_large(&self.cfg.limits), now);
|
||||
}
|
||||
c.last_heard = now;
|
||||
let taken = match message::decode::<ServerMsg>(bytes, self.cfg.limits.max_server_message) {
|
||||
// The join code's answer: join the file it opens, on a new connection.
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::time::{Duration, Instant};
|
||||
use figcraft_collab::message::{self, ClientMsg, Presence, ServerMsg};
|
||||
use figcraft_collab::{Client, Limits, Outgoing, Server};
|
||||
use figcraft_doc::{Document, NodeId};
|
||||
use figcraft_engine::live::{Event, Status};
|
||||
use figcraft_engine::live::{Event, Status, TOO_LARGE_TO_JOIN};
|
||||
use tungstenite::protocol::WebSocket;
|
||||
use tungstenite::{Bytes, Message};
|
||||
|
||||
@@ -262,6 +262,15 @@ fn refusals_and_the_end_of_the_session_are_final() {
|
||||
assert!(d.done());
|
||||
assert!(matches!(statuses(&inbox.drain()).last(), Some(Status::Failed { reason }) if reason.starts_with("The host refused")));
|
||||
}
|
||||
// A message over the size limit: the file is too large for this app, not refused by the host.
|
||||
let inbox = Inbox::new(Arc::new(|| {}));
|
||||
let mut cfg = GuestConfig::new(invite(), "Ada".into());
|
||||
cfg.limits.max_server_message = 2 << 20;
|
||||
let mut d = Driver::start(Script::default(), cfg, inbox.clone(), t0);
|
||||
d.opened(t0);
|
||||
d.message(&vec![0; (2 << 20) + 1], t0);
|
||||
assert!(d.done());
|
||||
assert!(matches!(statuses(&inbox.drain()).last(), Some(Status::Failed { reason }) if *reason == TOO_LARGE_TO_JOIN.replace("{mb}", "2")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -26,7 +26,7 @@ use tungstenite::{Bytes, Message};
|
||||
use crate::inbox::Inbox;
|
||||
use crate::invite::{Endpoint, Invite};
|
||||
pub use crate::join::GuestConfig;
|
||||
use crate::join::{Next, Outcome, Resuming, Seen, Taken, after, closed, hello, resolved, take, unresolved};
|
||||
use crate::join::{Next, Outcome, Resuming, Seen, Taken, after, closed, hello, resolved, take, too_large, unresolved};
|
||||
use crate::manage;
|
||||
use crate::net::{Duplex, NetLimits, config, open, shutdown};
|
||||
use crate::throttle::{PRESENCE_INTERVAL, Throttle, earliest};
|
||||
@@ -367,7 +367,7 @@ fn read_loop(mut ws: WebSocket<Duplex>, ctx: &ReaderCtx) {
|
||||
Ok(Message::Close(f)) => break f.map_or_else(|| closed(0, ""), |f| closed(f.code.into(), &f.reason)),
|
||||
Ok(Message::Text(_)) => break Outcome::Refused("the host sent a text message".into()),
|
||||
Ok(_) => {}
|
||||
Err(tungstenite::Error::Capacity(_)) => break Outcome::Refused("the host sent a message over the size limit".into()),
|
||||
Err(tungstenite::Error::Capacity(_)) => break too_large(&ctx.cfg.limits),
|
||||
Err(e) => break Outcome::Retry(e.to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::time::Duration;
|
||||
|
||||
use figcraft_collab::Client;
|
||||
use figcraft_collab::message::{ClientMsg, Peer, Resume, ServerMsg};
|
||||
use figcraft_engine::live::{Event, Role, SessionInfo, Status};
|
||||
use figcraft_engine::live::{Event, Role, SessionInfo, Status, TOO_LARGE_TO_JOIN};
|
||||
|
||||
use crate::inbox::Inbox;
|
||||
use crate::invite::Invite;
|
||||
@@ -95,6 +95,12 @@ pub(crate) fn unresolved(cfg: &GuestConfig, outcome: Outcome) -> Outcome {
|
||||
})
|
||||
}
|
||||
|
||||
/// The host sent a message over this app's size limit (the file's welcome, usually): final, and
|
||||
/// not the host's doing.
|
||||
pub(crate) fn too_large(limits: &NetLimits) -> Outcome {
|
||||
Outcome::Failed(TOO_LARGE_TO_JOIN.replace("{mb}", &(limits.max_server_message >> 20).to_string()))
|
||||
}
|
||||
|
||||
/// The server's answer to the join code: the invite is whole now.
|
||||
pub(crate) fn resolved(cfg: &mut GuestConfig, file: String, token: String) -> Result<(), Outcome> {
|
||||
if !crate::invite::valid_id(&file, 1, 64) || !crate::invite::valid_id(&token, 16, 128) {
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::time::{Duration, Instant};
|
||||
use figcraft_collab::message::{self, ClientMsg, Hello, Presence, ServerMsg, Signal};
|
||||
use figcraft_collab::{Client, Limits};
|
||||
use figcraft_doc::{Document, NodeId};
|
||||
use figcraft_engine::live::{Event, PEOPLE, Status};
|
||||
use figcraft_engine::live::{Event, PEOPLE, Status, TOO_LARGE_TO_JOIN};
|
||||
use tungstenite::protocol::WebSocket;
|
||||
use tungstenite::{Bytes, Message};
|
||||
|
||||
@@ -642,3 +642,31 @@ fn the_host_removes_someone_and_they_cant_come_back() {
|
||||
host.stop();
|
||||
assert_eq!(host.connections(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_welcome_over_the_size_limit_says_the_file_is_too_large() {
|
||||
let mut doc = Document::new("B".repeat(1 << 20));
|
||||
doc.repair();
|
||||
let probe = Probe::new();
|
||||
let cfg = HostConfig {
|
||||
addr: SocketAddr::from(([127, 0, 0, 1], 0)),
|
||||
any_port_if_taken: false,
|
||||
file: FILE.into(),
|
||||
token: TOKEN.into(),
|
||||
doc,
|
||||
name: "Hana".into(),
|
||||
limits: fast_limits(),
|
||||
core: Limits::default(),
|
||||
};
|
||||
let mut host = Host::start(cfg, probe.inbox.clone()).unwrap();
|
||||
let invite = Invite::new(host.addr().to_string(), FILE, TOKEN);
|
||||
|
||||
let mut guest_cfg = quick(GuestConfig::new(invite, "Ada".into()));
|
||||
guest_cfg.limits = NetLimits { max_server_message: 1 << 20, ..fast_limits() };
|
||||
let mut gp = Probe::new();
|
||||
let _g = Guest::start(guest_cfg, gp.inbox.clone()).unwrap();
|
||||
let too_large = TOO_LARGE_TO_JOIN.replace("{mb}", "1");
|
||||
gp.wait("the file too large to join", |e| e.iter().any(|ev| matches!(ev, Event::Status(Status::Failed { reason }) if *reason == too_large)));
|
||||
assert!(!welcomed(&gp.events));
|
||||
host.stop();
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@ pub fn parse_color(s: &str) -> Option<(u8, u8, u8)> {
|
||||
Some((byte(0)?, byte(2)?, byte(4)?))
|
||||
}
|
||||
|
||||
/// Why a join failed when a message from the host is over this app's size limit (`{mb}`: the
|
||||
/// limit in MB). The UI translates it.
|
||||
pub const TOO_LARGE_TO_JOIN: &str = "This file is too large to join here (up to {mb} MB)";
|
||||
|
||||
/// The connection to a live session.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "state")]
|
||||
|
||||
@@ -1393,6 +1393,7 @@ name = "English"
|
||||
"This condition is edited as an expression" = "This condition is edited as an expression"
|
||||
"This file" = "This file"
|
||||
"This file has unsaved changes" = "This file has unsaved changes"
|
||||
"This file is too large to join here (up to {mb} MB)" = "This file is too large to join here (up to {mb} MB)"
|
||||
"This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them."
|
||||
"This file is on {server}, open only to you. Share it to let others in with a new code." = "This file is on {server}, open only to you. Share it to let others in with a new code."
|
||||
"This frame is new on the branch." = "This frame is new on the branch."
|
||||
|
||||
@@ -1394,6 +1394,7 @@ status = "draft — needs native review"
|
||||
"This condition is edited as an expression" = "Esta condición se edita como expresión"
|
||||
"This file" = "Este archivo"
|
||||
"This file has unsaved changes" = "Este archivo tiene cambios sin guardar"
|
||||
"This file is too large to join here (up to {mb} MB)" = "Este archivo es demasiado grande para unirte aquí (hasta {mb} MB)"
|
||||
"This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "Este archivo usa fuentes que no están instaladas en este equipo. Su texto se dibuja con una fuente de reserva hasta que las instales."
|
||||
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Este archivo está en {server}, abierto solo para ti. Compártelo para dejar entrar a otros con un código nuevo."
|
||||
"This frame is new on the branch." = "Este marco es nuevo en la rama."
|
||||
|
||||
@@ -135,6 +135,7 @@ fn dynamic_labels() -> Vec<&'static str> {
|
||||
v.extend(Group::ALL.iter().map(|g| g.label()));
|
||||
v.extend(GESTURES.iter().flat_map(|(_, label, mac, other)| [*label, *mac, *other]));
|
||||
v.extend(DATA_LABELS);
|
||||
v.push(figcraft_engine::live::TOO_LARGE_TO_JOIN);
|
||||
// The starter templates' names, descriptions and kinds.
|
||||
v.extend(figcraft_engine::templates::TEMPLATES.iter().flat_map(|t| [t.name, t.description, t.kind.label()]));
|
||||
v
|
||||
@@ -327,6 +328,14 @@ fn lookups_fall_back_to_english_and_fill_placeholders() {
|
||||
assert!(matches!(lookup(es, k), Some(x) if std::ptr::eq(x, t)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_live_failure_the_ui_knows_is_read_back_for_translation() {
|
||||
use figcraft_engine::live::TOO_LARGE_TO_JOIN;
|
||||
let reason = TOO_LARGE_TO_JOIN.replace("{mb}", "1024");
|
||||
assert!(matches!(crate::live::failure_text(&reason), std::borrow::Cow::Owned(t) if t == reason));
|
||||
assert!(matches!(crate::live::failure_text("Couldn't reach the server"), std::borrow::Cow::Borrowed(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_languages_pick_the_first_one_with_a_catalog() {
|
||||
let code = |pref: &str, system: Option<&str>| LANGS[resolve(pref, system)].code;
|
||||
|
||||
@@ -1510,7 +1510,7 @@ impl FigApp {
|
||||
// What happened in a live session (the network woke us for it).
|
||||
if self.session.live_poll() {
|
||||
if let Some(m) = self.session.message.take() {
|
||||
self.toast(m);
|
||||
self.toast(live::failure_text(&m));
|
||||
}
|
||||
self.background_done(&ctx);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
//! and it fades a few seconds after the last change (frames are asked for only until then).
|
||||
//! Spotlight: your avatar's menu asks everyone to follow you; a banner says so and stops it.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Shape, Stroke, Ui, pos2, vec2};
|
||||
use figcraft_engine::live::{Access, PEOPLE, Role, SessionId, Share, Signal, Status, Via, initials, parse_color, presence_page};
|
||||
use figcraft_engine::live::{Access, PEOPLE, Role, SessionId, Share, Signal, Status, TOO_LARGE_TO_JOIN, Via, initials, parse_color, presence_page};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::FigApp;
|
||||
@@ -595,7 +596,7 @@ pub fn share_popover(app: &mut FigApp, anchor: &egui::Response) {
|
||||
(Status::Reconnecting { attempt }, _) => {
|
||||
(trf("Connection lost, reconnecting (attempt {attempt})…", &[("attempt", attempt)]), t.text_secondary)
|
||||
}
|
||||
(Status::Failed { .. }, _) => (status.label(), t.danger),
|
||||
(Status::Failed { reason }, _) => (failure_text(reason).into_owned(), t.danger),
|
||||
_ => (tr(&status.label()).to_string(), t.text_secondary),
|
||||
};
|
||||
if !line.is_empty() {
|
||||
@@ -695,11 +696,18 @@ pub fn join_field(app: &mut FigApp, ui: &mut Ui, typed: &mut String, id: egui::I
|
||||
pub fn join_status(app: &FigApp, ui: &mut Ui, t: &Tokens, session: SessionId) {
|
||||
match app.session.live.get(session).map(|l| &l.status) {
|
||||
Some(Status::Connecting) => widgets::label(ui, tr("Joining…"), t.text_secondary),
|
||||
Some(Status::Failed { reason }) => widgets::label(ui, reason, t.danger),
|
||||
Some(Status::Failed { reason }) => widgets::label(ui, &failure_text(reason), t.danger),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a live session failed, in the UI language when it is a reason the UI knows (the transport's
|
||||
/// English otherwise).
|
||||
pub(crate) fn failure_text(reason: &str) -> Cow<'_, str> {
|
||||
let mb = TOO_LARGE_TO_JOIN.split_once("{mb}").and_then(|(before, after)| reason.strip_prefix(before)?.strip_suffix(after));
|
||||
mb.map_or(Cow::Borrowed(reason), |mb| Cow::Owned(trf(TOO_LARGE_TO_JOIN, &[("mb", &mb)])))
|
||||
}
|
||||
|
||||
/// Not in a session: your name, how to share (online or on the local network), start; or join
|
||||
/// one with a code. Everything else is under Advanced.
|
||||
fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
|
||||
Reference in New Issue
Block a user