From 7347a55ee15a71fa7a3aa4e032214f560317971e Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Thu, 23 Jul 2026 21:19:18 -0600 Subject: [PATCH] ADFA-4842: module management stops the server + blocks the whole UI (no live-rootfs corruption) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installing a module runs ./runrole in an app-side proot that modifies system packages/config. On a live system that collided with the running server over the shared rootfs (corruption risk). Content ops avoid a 2nd proot via the in-server engine; a runrole can't, so instead we quiesce the server around it. - InstallService: before the module queue, stop the server's services (pdsm stop, idempotent) so no runrole ever runs alongside a live server writing the same DBs/config. The server is brought back after the queue. - LibraryActivity: while the queue runs, block the ENTIRE library UI behind the full-screen (match_parent, clickable) boot gate — every tab (Library, Connect, Clone, Settings) and the nav bar, not just the cards — showing 'Modules are being installed. Please wait.' + the current module. When the queue finishes, drop the block and restart the server (guarded by canStartServer, so never a 2nd proot). Reuses existing strings (server_shutting_down, install_busy_modules). Needs on-device verification. Note: if the queue runs entirely while LibraryActivity is destroyed, the restart falls back to the ADFA-4837 'couldn't start - tap to retry' affordance. --- .../install/presentation/InstallService.java | 23 ++++++++- .../controller/redesign/LibraryActivity.java | 47 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java index 5b166123..4a2a64d6 100644 --- a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java +++ b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java @@ -610,7 +610,28 @@ private void resetExtractAndBootstrap(String downloadPath, String tarball) { // ------------------------------------------------------------ module queue private void runModuleQueue() { - installNextModule(); + // ADFA-4842: a runrole modifies system packages/config, so it must own the rootfs + // exclusively. Unlike content (which the running server handles in-process), we cannot + // avoid the runrole's own proot — so instead stop the server's SERVICES first (pdsm stop) + // so a runrole never runs alongside a live server writing the same DBs/config (the + // data-corruption risk). The app restarts the server after the queue (LibraryActivity's + // module-queue observer). Stopping is idempotent (no-op if the server is already down). + if (cancelled) return; + updateNotification(getString(R.string.server_shutting_down)); + log("[Modules] Stopping server services before runroles (exclusive rootfs)..."); + if (prootEngine == null) prootEngine = new PRootEngine(); + prootEngine.executeInContainer(this, debianRootfs.getAbsolutePath(), + "/usr/bin/env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin bash -lc '/usr/local/bin/pdsm stop'", + new PRootEngine.OutputListener() { + @Override public void onOutputLine(String line) { log("[Modules] pdsm stop: " + line); } + @Override public void onProcessExit(int exitCode) { installNextModule(); } + @Override public void onError(String error) { + // Proceed anyway: if the stop couldn't launch, the runroles still need to run; + // surface it in the log rather than hanging the queue. + log("[Modules] pdsm stop error (continuing): " + error); + installNextModule(); + } + }); } /** diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java index f71b4f97..492dae91 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -60,6 +60,7 @@ public class LibraryActivity extends AppCompatActivity implements ServerControll private boolean gateDismissed = false; private boolean closing = false; private boolean closedDone = false; + private boolean moduleBusy = false; // ADFA-4842: a module (runrole) queue is running // ADFA-4837: animated "…" on the boot status line so the long pre-pdsm silence doesn't look frozen. private final Handler ellipsisHandler = new Handler(Looper.getMainLooper()); @@ -172,6 +173,21 @@ public android.graphics.Typeface fetchFont(String fontFamily) { } }); + // ADFA-4842: while a module (runrole) queue runs, the server is stopped and the rootfs is + // being modified — block the WHOLE library UI (every tab + nav) behind the full-screen boot + // gate, show which module is installing, and bring the server back when the queue finishes. + org.iiab.controller.install.presentation.ModuleQueueRepository.get().state().observe(this, ms -> { + if (ms == null) return; + if (ms.isRunning()) { + if (!moduleBusy) { moduleBusy = true; showModuleBusy(); } + if (installDetail != null) installDetail.setText(ms.currentModule == null ? "" : ms.currentModule); + } else if (moduleBusy) { + moduleBusy = false; + hideModuleBusy(); + if (canStartServer()) startServer(); // the server was stopped for the runroles + } + }); + Handler main = new Handler(Looper.getMainLooper()); if (installing) { // A download is in progress: keep the gate and show live progress; dismissal @@ -219,6 +235,37 @@ private void hideInstallProgress() { if (installProgress != null) installProgress.setVisibility(View.GONE); } + // ADFA-4842: full-screen "system updating" block while a module (runrole) queue runs. The boot + // gate is match_parent + clickable and drawn over the nav content, so it blocks EVERY tab + // (Library, Connect, Clone, Settings) and the nav bar — not just the Library cards. + private void showModuleBusy() { + if (closing) return; + stopBootEllipsis(); + if (installProgress != null) { + installProgress.setVisibility(View.VISIBLE); + if (installStatus != null) installStatus.setText(getString(R.string.install_busy_modules)); + if (installBar != null) { installBar.setVisibility(View.VISIBLE); installBar.setIndeterminate(true); } + } + if (bootGate != null) { + bootGate.setVisibility(View.VISIBLE); + if (!reduceMotion()) { + bootGate.removeAllAnimatorListeners(); + bootGate.setRepeatCount(LottieDrawable.INFINITE); + bootGate.setMinAndMaxFrame("A_ENTRY_LOOP"); + bootGate.playAnimation(); + } + } + } + + private void hideModuleBusy() { + hideInstallProgress(); + if (bootGate != null) { + bootGate.removeAllAnimatorListeners(); + bootGate.cancelAnimation(); + bootGate.setVisibility(View.GONE); + } + } + private void onServerReady() { if (gateDismissed || bootGate == null) { return;