Many imorovments
Build and test / Desktop (Linux) (push) Failing after 1m8s
Build and test / Android (aarch64) (push) Failing after 2s
Build and test / Layer separation (push) Canceled after 23s
Traceability / Requirement traces (push) Failing after 59s

This commit is contained in:
2026-08-12 22:16:15 +02:00
parent fa12afed18
commit 4d78041d1d
24 changed files with 711 additions and 78 deletions
+121 -7
View File
@@ -106,6 +106,28 @@ where
});
}
// --- sign in with an app password -----------------------------------
{
let weak = window.as_weak();
let ctl = controller.clone();
window.on_launch_sign_in_direct(move |server, login, password| {
let Some(w) = weak.upgrade() else { return };
ctl.model
.borrow_mut()
.begin_direct_sign_in(server.to_string());
render(&w, &ctl);
let server = ctl.model.borrow().server_url.clone();
spawn_direct_login(
w.as_weak(),
ctl.clone(),
server,
login.to_string(),
password.to_string(),
);
});
}
// --- sign out ------------------------------------------------------
{
let weak = window.as_weak();
@@ -279,11 +301,18 @@ fn spawn_login(weak: slint::Weak<AppWindow>, ctl: Rc<LaunchController>, server:
};
step("thread started");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
// `enable_all()` also enables the signal driver, which wants to own
// process-wide signal handling and is not something a worker thread
// inside an Android app can safely claim — the flow needs only IO
// (reqwest) and time (the poll interval), so ask for just those.
let rt = match tokio::runtime::Builder::new_current_thread()
// Multi-thread, not current-thread: a current-thread runtime drives
// its reactor only while the thread sits inside `block_on`, and on
// Android that left reqwest's connection future never polled — the
// await never resolved, so the worker neither failed nor returned.
// A multi-thread runtime owns worker threads that poll the reactor
// regardless. One worker is plenty for a single login flow.
//
// Only IO and time are enabled; `enable_all()` would also start the
// signal driver, which wants process-wide signal handling that an
// Android app's runtime already owns.
let rt = match tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_io()
.enable_time()
.build()
@@ -313,6 +342,85 @@ fn spawn_login(weak: slint::Weak<AppWindow>, ctl: Rc<LaunchController>, server:
poll_channel(weak, ctl, rx);
}
/// TRACES: FR-NC-1
/// Verify an app password the user supplied, then keep it.
///
/// No browser and no polling: one authenticated request establishes both that
/// the credential works and what the account's canonical user id is, which is
/// what the DAV paths are built from. Reuses the same channel and drain loop as
/// the browser flow, so success and failure land in the UI identically.
fn spawn_direct_login(
weak: slint::Weak<AppWindow>,
ctl: Rc<LaunchController>,
server: String,
login: String,
password: String,
) {
let (tx, rx) = std::sync::mpsc::channel::<LoginMessage>();
std::thread::spawn(move || {
let panic_tx = tx.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
// Multi-thread for the reason the browser flow is: a current-thread
// runtime left reqwest's future unpolled on Android.
let rt = match tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_io()
.enable_time()
.build()
{
Ok(rt) => rt,
Err(e) => {
let _ = tx.send(LoginMessage::Failed(format!("tokio runtime: {e}")));
return;
}
};
rt.block_on(async {
let client = match dr_sync_nextcloud::http_client("DarkRoom") {
Ok(c) => c,
Err(e) => {
let _ = tx.send(LoginMessage::Failed(format!("http client: {e}")));
return;
}
};
let creds = dr_sync_nextcloud::AppCredentials {
server,
login_name: login,
app_password: password,
};
// The credential is only worth storing if it actually works,
// and this is the cheapest request that proves it. A 401 comes
// back as an error here rather than as a puzzling failure on
// the first listing.
match fetch_user_id(&client, &creds).await {
Ok(user_id) => {
let _ = tx.send(LoginMessage::Success(Box::new((creds, user_id))));
}
Err(e) => {
let _ = tx.send(LoginMessage::Failed(format!(
"could not sign in with that app password: {e}"
)));
}
}
});
}));
if let Err(panic) = result {
let detail = panic
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| panic.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "panicked with a non-string payload".to_string());
let _ = panic_tx.send(LoginMessage::Failed(format!("internal error: {detail}")));
}
});
poll_channel(weak, ctl, rx);
}
/// The body of the login flow, split out so the worker above can wrap it in
/// `catch_unwind` without a deeply nested closure.
fn run_login_flow(
@@ -487,8 +595,14 @@ fn spawn_folder_list(weak: slint::Weak<AppWindow>, ctl: Rc<LaunchController>, pa
let user_id = session.user_id.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
// Multi-thread for the same reason as the login worker: a
// current-thread runtime left reqwest's connection future unpolled on
// Android, so the await never resolved and the thread stopped without
// failing or returning.
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_io()
.enable_time()
.build();
let Ok(rt) = rt else {
let _ = tx.send(Err("runtime".into()));