Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions crates/myownmesh/src/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ pub enum Request {
#[serde(default)]
purge: bool,
},
/// Forget **every** joined network at once — a `NetworkRemove{purge:true}`
/// for all of them: tear each out of the registry, `leave()` its driver, and
/// delete its signed governance state + roster from disk. Keeps this device's
/// identity. The daemon then exits (see [`schedule_daemon_exit`]) so every
/// layer reloads from the now-clean disk instead of a stale in-memory cache
/// that would re-persist ("resurrect") what was just removed; the GUI/service
/// brings a fresh daemon back up.
ForgetAllNetworks,
/// Factory reset — wipe this device's **entire** state directory
/// (`~/.myownmesh`, honouring `MYOWNMESH_HOME`): identity, config, and every
/// network's roster + governance state. The device becomes a brand-new
/// identity to every peer. Quiesces each network first (so nothing
/// re-persists mid-wipe), then removes the tree and exits so a fresh daemon
/// mints a new identity on empty state.
FactoryReset,
/// Update an already-joined network's config in place. Hot-
/// reloadable changes (topology / label / auto_approve / roster
/// path) are applied without dropping any peer; transport-level
Expand Down Expand Up @@ -1014,6 +1029,14 @@ async fn dispatch(state: &Arc<ControlState>, req: Request) -> Response {
info!(%network, purge, "control: network_remove");
network_remove(state, &network, purge).await
}
Request::ForgetAllNetworks => {
info!("control: forget_all_networks");
forget_all_networks(state).await
}
Request::FactoryReset => {
info!("control: factory_reset");
factory_reset(state).await
}
Request::NetworkUpdate { config } => {
info!(network = %config.network_id, config_id = %config.id, "control: network_update");
network_update(state, config).await
Expand Down Expand Up @@ -1814,6 +1837,72 @@ async fn network_remove(state: &Arc<ControlState>, key: &str, purge: bool) -> Re
}
}

/// Forget every joined network at once — the bulk `NetworkRemove{purge:true}`.
/// Each network is torn down live and its signed state + roster deleted from
/// disk; the device identity is kept. Snapshots the set first so removing as we
/// go can't skip an entry. Schedules a daemon exit ([`schedule_daemon_exit`]) so
/// every layer reloads clean around the wipe.
async fn forget_all_networks(state: &Arc<ControlState>) -> Response {
let mut forgotten = Vec::new();
for n in state.registry.summaries() {
// `network_remove` resolves either alias; the config id is stable.
let _ = network_remove(state, &n.config_id, true).await;
forgotten.push(n.config_id);
}
schedule_daemon_exit();
Response::ok(serde_json::json!({ "forgotten": forgotten, "restarting": true }))
}

/// Factory reset — return this device to a brand-new state. First quiesce every
/// network (tear it down + purge its files) so nothing re-persists mid-wipe,
/// then remove the whole state directory (identity, config, and any leftovers),
/// and finally exit so a fresh daemon mints a new identity on empty state. The
/// live control socket + log file descriptors stay valid until exit; the
/// supervising service, or the GUI's `ensure_daemon_running` on relaunch, brings
/// the daemon back. Best-effort per step — we always schedule the exit so a
/// partial failure still ends in a clean restart rather than a half-wiped daemon
/// re-persisting stale caches.
async fn factory_reset(state: &Arc<ControlState>) -> Response {
// Quiesce writers first: tearing each network down stops its engine driver
// from writing a roster/state file back out while we're deleting the tree.
for n in state.registry.summaries() {
let _ = network_remove(state, &n.config_id, true).await;
}
let dir = match myownmesh_core::dirs::data_dir() {
Ok(d) => d,
Err(e) => {
// Can't find the dir to wipe — still restart so we don't leave the
// caller hanging on a half-done reset.
schedule_daemon_exit();
return Response::err(format!("factory reset: resolve state dir: {e}"));
}
};
if let Err(e) = std::fs::remove_dir_all(&dir) {
// A missing dir already reads as reset; anything else is worth logging,
// but we still exit so caches can't resurrect what did get deleted.
if e.kind() != std::io::ErrorKind::NotFound {
warn!(dir = %dir.display(), "factory reset: remove_dir_all: {e:#}");
}
}
schedule_daemon_exit();
Response::ok(serde_json::json!({ "reset": true, "restarting": true }))
}

/// Exit the daemon shortly after the current response flushes, so a fresh
/// instance reloads from the now-clean disk. The reset commands use this: the
/// only reliable way to drop every in-memory cache — which would otherwise
/// re-persist and "resurrect" the state we just deleted — is a clean process
/// restart. The short delay lets the JSON response reach the client first; the
/// supervising service (Restart=always) or the GUI's `ensure_daemon_running` on
/// relaunch starts a fresh daemon.
fn schedule_daemon_exit() {
tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(400)).await;
info!("reset complete — exiting so a fresh daemon reloads clean state");
std::process::exit(0);
});
}

/// Reconnect a joined network in place — the non-destructive twin of
/// [`network_remove`] + [`network_add`]. Hands the live `JoinedNetwork` a
/// reconnect request (redial signaling + renegotiate ICE) without leaving the
Expand Down
7 changes: 7 additions & 0 deletions gui/src-tauri/src/control_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ pub enum Request {
NetworkRemove {
network: String,
},
/// Forget every joined network at once (purges each network's signed state +
/// roster; keeps the device identity). The daemon exits afterward so it
/// reloads clean — the GUI restarts the stack.
ForgetAllNetworks,
/// Wipe this device's entire state directory (identity, config, all
/// networks) and exit for a fresh start — a factory reset.
FactoryReset,
/// Atomic in-place edit of an already-joined network. Hot-applies
/// label / topology / auto-approve without dropping peers; restarts
/// transport only for signaling/STUN/TURN edits. Preserves the roster
Expand Down
43 changes: 43 additions & 0 deletions gui/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,46 @@ async fn mesh_network_remove(
unwrap_response(resp)
}

/// Danger Zone: forget every joined network at once (purges each network's
/// signed state + roster; keeps the device identity). The daemon exits after
/// responding so it reloads clean; the caller follows with `restart_app`.
#[tauri::command]
async fn mesh_forget_all_networks(state: State<'_, AppState>) -> Result<serde_json::Value, String> {
let resp = state
.client
.request(&Request::ForgetAllNetworks)
.await
.map_err(|e| e.to_string())?;
unwrap_response(resp)
}

/// Danger Zone: factory reset — wipe the entire state directory (identity,
/// config, all networks). The daemon exits so a fresh one mints a new identity;
/// the caller follows with `restart_app`.
#[tauri::command]
async fn mesh_factory_reset(state: State<'_, AppState>) -> Result<serde_json::Value, String> {
let resp = state
.client
.request(&Request::FactoryReset)
.await
.map_err(|e| e.to_string())?;
unwrap_response(resp)
}

/// Relaunch the whole app. The Danger Zone calls this right after a reset so
/// every layer restarts on the now-clean state — the daemon (which the reset
/// told to exit), the Tauri backend, and the webview — instead of any of them
/// serving a stale in-memory cache that would resurrect what was just wiped.
/// The short wait lets the daemon finish exiting so the relaunched app spawns a
/// fresh one via `ensure_daemon_running` rather than reconnecting to the old.
#[tauri::command]
async fn restart_app(app: AppHandle) -> Result<(), String> {
tokio::time::sleep(std::time::Duration::from_millis(1200)).await;
app.restart();
#[allow(unreachable_code)]
Ok(())
}

/// Atomic in-place network edit. The daemon hot-applies label / topology
/// / auto-approve and only restarts transport for signaling/STUN/TURN
/// changes — the roster survives either way. Replaces the GUI's previous
Expand Down Expand Up @@ -712,6 +752,9 @@ fn main() {
update_check,
update_apply,
update_set_prefs,
mesh_forget_all_networks,
mesh_factory_reset,
restart_app,
])
.setup(move |app| {
let handle = app.handle().clone();
Expand Down
20 changes: 20 additions & 0 deletions gui/src/mesh-client.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,24 @@ function createMeshClient() {
await refreshNetworks();
}

/** Danger Zone: forget every joined network at once, then reboot the whole
* stack. Purges each network's signed state + roster while keeping this
* device's identity. The daemon exits after the reset so it reloads clean,
* and `restart_app` relaunches the app on top of it so no layer keeps a
* stale cache that would resurrect what was wiped. `restart_app` never
* resolves (the app is replaced), so this call ends by relaunching. */
async function forgetAllNetworksAndRestart() {
await invoke("mesh_forget_all_networks");
await invoke("restart_app");
}

/** Danger Zone: factory reset — wipe this device's entire state (identity,
* config, every network) and reboot into a brand-new identity. */
async function factoryResetAndRestart() {
await invoke("mesh_factory_reset");
await invoke("restart_app");
}

/** Atomic in-place edit of an already-joined network. The daemon
* hot-applies label / topology / auto-approve and only restarts
* transport for signaling/STUN/TURN edits — the roster is preserved
Expand Down Expand Up @@ -717,6 +735,8 @@ function createMeshClient() {
configShow,
networkAdd,
networkRemove,
forgetAllNetworksAndRestart,
factoryResetAndRestart,
networkUpdate,
exportNetworkFile,

Expand Down
Loading
Loading