From 0efb69ba071ccfb969c8f4c646f18b0292293323 Mon Sep 17 00:00:00 2001 From: GlitchedCode922 Date: Wed, 21 Oct 2026 13:56:32 +0000 Subject: [PATCH 1/2] Add runit integration in the app --- Android/app/src/main/assets/chkrunit.sh | 88 ++++++ .../ui/navigation/DroidspacesNavigation.kt | 21 ++ .../app/ui/screen/ContainerDetailsScreen.kt | 4 +- .../droidspaces/app/ui/screen/RunitScreen.kt | 67 ++++ .../app/util/ContainerRunitManager.kt | 287 ++++++++++++++++++ .../app/util/ContainerUsersManager.kt | 5 +- Android/app/src/main/res/values/strings.xml | 9 +- CONTRIBUTING.md | 5 +- 8 files changed, 478 insertions(+), 8 deletions(-) create mode 100644 Android/app/src/main/assets/chkrunit.sh create mode 100644 Android/app/src/main/java/com/droidspaces/app/ui/screen/RunitScreen.kt create mode 100644 Android/app/src/main/java/com/droidspaces/app/util/ContainerRunitManager.kt diff --git a/Android/app/src/main/assets/chkrunit.sh b/Android/app/src/main/assets/chkrunit.sh new file mode 100644 index 000000000..3a6a7628c --- /dev/null +++ b/Android/app/src/main/assets/chkrunit.sh @@ -0,0 +1,88 @@ +#!/bin/sh +# chkrunit.sh - runit service query tool + +ACTIVE_DIR="" +for dir in /etc/service /service /var/service; do + if [ -d "$dir" ]; then + ACTIVE_DIR="$dir" + break + fi +done + +SV_DIR="" +for dir in /etc/sv /etc/runit/sv /usr/local/etc/sv; do + if [ -d "$dir" ]; then + SV_DIR="$dir" + break + fi +done + +_all_running() { + [ -n "$ACTIVE_DIR" ] || return 0 + for path in "$ACTIVE_DIR"/*; do + [ -e "$path" ] || [ -L "$path" ] || continue + [ -d "$path" ] || continue + name=$(basename "$path") + status=$(sv status "$path" 2>/dev/null) || status="" + + case "$status" in + run:*) + printf '%s|%s\n' "$name" "$status" + ;; + esac + done +} + +_all_enabled() { + [ -n "$ACTIVE_DIR" ] || return 0 + for path in "$ACTIVE_DIR"/*; do + [ -e "$path" ] || [ -L "$path" ] || continue + [ -d "$path" ] || continue + name=$(basename "$path") + status=$([ -f "$path/down" ] && printf "run-on-boot: disabled" || printf "run-on-boot: enabled" ) + + printf '%s|%s\n' "$name" "$status" + done +} + +_all_disabled() { + [ -n "$ACTIVE_DIR" ] || return 0 + [ -n "$SV_DIR" ] || return 0 + for path in "$SV_DIR"/*; do + [ -e "$path" ] || [ -L "$path" ] || continue + [ -d "$path" ] || continue + name=$(basename "$path") + [ -e "$ACTIVE_DIR/$name" ] && continue + status=$([ -f "$path/down" ] && printf "run-on-boot: disabled" || printf "run-on-boot: enabled" ) + + printf '%s|%s\n' "$name" "$status" + done +} + +_all_services() { + [ -n "$ACTIVE_DIR" ] || return 0 + [ -n "$SV_DIR" ] || return 0 + for path in "$SV_DIR"/*; do + [ -e "$path" ] || [ -L "$path" ] || continue + [ -d "$path" ] || continue + name=$(basename "$path") + status=$([ -e "$ACTIVE_DIR/$name" ] && printf "enabled" || printf "disabled" ) + + printf '%s|%s\n' "$name" "$status" + done +} + +case "$1" in + --running) + _all_running + ;; + --enabled) + _all_enabled + ;; + --disabled) + _all_disabled + ;; + --all) + _all_services + ;; +esac diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt b/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt index 81587cd48..3d71d7416 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt @@ -41,6 +41,7 @@ import com.droidspaces.app.ui.screen.UnitDetailScreen import com.droidspaces.app.ui.screen.OverrideEditorScreen import com.droidspaces.app.ui.screen.ProcdScreen import com.droidspaces.app.ui.screen.OpenRCScreen +import com.droidspaces.app.ui.screen.RunitScreen import com.droidspaces.app.ui.screen.ContainerTerminalScreen import com.droidspaces.app.ui.viewmodel.ContainerInstallationViewModel import com.droidspaces.app.ui.viewmodel.ContainerViewModel @@ -121,6 +122,9 @@ sealed class Screen(val route: String) { data object OpenRC : Screen("openrc/{containerName}") { fun createRoute(containerName: String) = "openrc/${Uri.encode(containerName)}" } + data object Runit : Screen("runit/{containerName}") { + fun createRoute(containerName: String) = "runit/${Uri.encode(containerName)}" + } data object Terminal : Screen("terminal/{containerName}") { fun createRoute(containerName: String) = "terminal/${Uri.encode(containerName)}" @@ -670,6 +674,8 @@ fun DroidspacesNavigation( navController.navigate(Screen.Procd.createRoute(containerName)) com.droidspaces.app.ui.screen.InitSystem.OPENRC -> navController.navigate(Screen.OpenRC.createRoute(containerName)) + com.droidspaces.app.ui.screen.InitSystem.RUNIT -> + navController.navigate(Screen.Runit.createRoute(containerName)) } }, onNavigateToTerminal = { @@ -785,6 +791,21 @@ fun DroidspacesNavigation( ) } + composable( + route = Screen.Runit.route, + arguments = listOf( + navArgument("containerName") { type = NavType.StringType } + ), + enterTransition = defaultEnterTransition, + exitTransition = defaultExitTransition + ) { backStackEntry -> + val containerName = backStackEntry.arguments?.getString("containerName") ?: "" + RunitScreen( + containerName = containerName, + onNavigateBack = { navController.popBackStack() } + ) + } + composable( route = Screen.Terminal.route, arguments = listOf( diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerDetailsScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerDetailsScreen.kt index 94a221821..d4fbbec4b 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerDetailsScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerDetailsScreen.kt @@ -94,6 +94,7 @@ fun ContainerDetailsScreen( "systemd" -> InitSystemCardState.Available(InitSystem.SYSTEMD) "procd" -> InitSystemCardState.Available(InitSystem.PROCD) "openrc" -> InitSystemCardState.Available(InitSystem.OPENRC) + "runit" -> InitSystemCardState.Available(InitSystem.RUNIT) else -> InitSystemCardState.NotAvailable } } @@ -279,7 +280,7 @@ fun ContainerDetailsScreen( /** * Which init system is available in the container. */ -enum class InitSystem { SYSTEMD, PROCD, OPENRC } +enum class InitSystem { SYSTEMD, PROCD, OPENRC, RUNIT } /** * Init system card state - sealed for type safety and stability @@ -601,6 +602,7 @@ private fun PremiumInitSystemCard( InitSystem.SYSTEMD -> context.getString(R.string.systemd) InitSystem.PROCD -> context.getString(R.string.openwrt) InitSystem.OPENRC -> context.getString(R.string.openrc) + InitSystem.RUNIT -> context.getString(R.string.runit) } else -> context.getString(R.string.init_system) } diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/RunitScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/RunitScreen.kt new file mode 100644 index 000000000..873d919d1 --- /dev/null +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/RunitScreen.kt @@ -0,0 +1,67 @@ +package com.droidspaces.app.ui.screen + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.PowerSettingsNew +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import com.droidspaces.app.R +import com.droidspaces.app.util.ContainerRunitManager + +private fun ContainerRunitManager.CommandResult.toInit() = InitCommandResult(isSuccess, output, error) + +private fun ContainerRunitManager.ServiceInfo.toRow(containerName: String): InitServiceRow { + val uiStatus = when (status) { + ContainerRunitManager.ServiceStatus.ENABLED_RUNNING -> InitServiceUiStatus.ENABLED_RUNNING + ContainerRunitManager.ServiceStatus.ENABLED_STOPPED -> InitServiceUiStatus.ENABLED_STOPPED + ContainerRunitManager.ServiceStatus.DISABLED_STOPPED -> InitServiceUiStatus.DISABLED_STOPPED + ContainerRunitManager.ServiceStatus.ABNORMAL -> InitServiceUiStatus.ABNORMAL + } + return InitServiceRow( + name = name, + description = description, + isRunning = isRunning, + isEnabled = isEnabled, + isMasked = false, + isStatic = false, + status = uiStatus, + startStop = { (if (isRunning) ContainerRunitManager.stopService(containerName, name) else ContainerRunitManager.startService(containerName, name)).toInit() }, + enableDisable = { (if (isEnabled) ContainerRunitManager.disableService(containerName, name) else ContainerRunitManager.enableService(containerName, name)).toInit() }, + unmask = null, + menu = buildList { + if (isRunning) add(InitServiceMenuAction.Command(R.string.restart_service, Icons.Default.Refresh) { ContainerRunitManager.restartService(containerName, name).toInit() }) + add(InitServiceMenuAction.Command(R.string.toggle_run_on_boot, Icons.Rounded.PowerSettingsNew) { ContainerRunitManager.toggleDefaultState(containerName, name).toInit() }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RunitScreen( + containerName: String, + onNavigateBack: () -> Unit +) { + val context = LocalContext.current + LaunchedEffect(Unit) { ContainerRunitManager.initialize(context) } + + val filters = listOf( + InitServiceFilterChip("RUNNING", R.string.running, statusColorFor(InitServiceUiStatus.ENABLED_RUNNING), R.string.no_running_services) { it.isRunning && it.isEnabled }, + InitServiceFilterChip("ENABLED", R.string.enabled_legend, statusColorFor(InitServiceUiStatus.ENABLED_STOPPED), R.string.no_enabled_services) { it.isEnabled && !it.isRunning }, + InitServiceFilterChip("DISABLED", R.string.disabled_legend, statusColorFor(InitServiceUiStatus.DISABLED_STOPPED), R.string.no_disabled_services) { !it.isEnabled && !it.isRunning }, + InitServiceFilterChip("ABNORMAL", R.string.abnormal_legend, statusColorFor(InitServiceUiStatus.ABNORMAL), R.string.no_abnormal_services) { it.isRunning && !it.isEnabled }, + InitServiceFilterChip("ALL", R.string.all_legend, null, R.string.no_services_found) { true }, + ) + + InitServiceScreen( + containerName = containerName, + titleRes = R.string.runit_services, + onNavigateBack = onNavigateBack, + isAvailable = { cn -> ContainerRunitManager.isRunitAvailable(cn) }, + fetchRows = { cn -> ContainerRunitManager.getAllServices(cn).map { it.toRow(cn) } }, + filters = filters, + defaultFilterId = "RUNNING", + ) +} diff --git a/Android/app/src/main/java/com/droidspaces/app/util/ContainerRunitManager.kt b/Android/app/src/main/java/com/droidspaces/app/util/ContainerRunitManager.kt new file mode 100644 index 000000000..bbab6bb3f --- /dev/null +++ b/Android/app/src/main/java/com/droidspaces/app/util/ContainerRunitManager.kt @@ -0,0 +1,287 @@ +package com.droidspaces.app.util + +import android.content.Context +import android.util.Base64 +import android.util.Log +import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +object ContainerRunitManager { + private const val TAG = "ContainerRunitManager" + private const val COMMAND_TIMEOUT_MS = 10_000L + + private var scriptBase64: String? = null + + enum class ServiceStatus { + ENABLED_RUNNING, + ENABLED_STOPPED, + DISABLED_STOPPED, + ABNORMAL + } + + data class ServiceInfo( + val name: String, + val description: String, + val status: ServiceStatus, + val isEnabled: Boolean, + val isRunning: Boolean + ) + + enum class ServiceFilter { + RUNNING, + ENABLED, + DISABLED, + ABNORMAL, + ALL + } + + data class CommandResult( + val exitCode: Int, + val output: List, + val error: List + ) { + val isSuccess: Boolean get() = exitCode == 0 + } + + fun initialize(context: Context) { + if (scriptBase64 == null) { + try { + val script = context.assets.open("chkrunit.sh").bufferedReader().readText() + scriptBase64 = Base64.encodeToString(script.toByteArray(), Base64.NO_WRAP) + } catch (e: Exception) { + Log.e(TAG, "Failed to load chkrunit.sh from assets", e) + } + } + } + + private suspend fun runScript(containerName: String, flag: String): Pair> = + withContext(Dispatchers.IO) { + val b64 = scriptBase64 ?: return@withContext Pair(false, emptyList()) + try { + val cmd = "${Constants.DROIDSPACES_BINARY_PATH} --name=${ContainerCommandBuilder.quote(containerName)} run 'echo $b64 | base64 -d | sh -s -- $flag'" + val result = Shell.cmd(cmd).exec() + Pair(result.isSuccess, result.out) + } catch (e: Exception) { + Log.e(TAG, "Error running script", e) + Pair(false, emptyList()) + } + } + + suspend fun executeRunitCommand( + containerName: String, + command: String + ): CommandResult = withContext(Dispatchers.IO) { + val fullCmd = "${Constants.DROIDSPACES_BINARY_PATH} --name=${ContainerCommandBuilder.quote(containerName)} run '$command 2>&1'" + val executor = Executors.newSingleThreadExecutor() + try { + val future = executor.submit { Shell.cmd(fullCmd).exec() } + try { + val result = future.get(COMMAND_TIMEOUT_MS, TimeUnit.MILLISECONDS) + CommandResult(exitCode = result.code, output = result.out, error = result.err) + } catch (e: TimeoutException) { + future.cancel(true) + Log.e(TAG, "Command timed out: $command") + CommandResult( + exitCode = 124, + output = listOf("Command timed out after ${COMMAND_TIMEOUT_MS / 1000} seconds"), + error = emptyList() + ) + } + } catch (e: Exception) { + Log.e(TAG, "Error executing: $command", e) + CommandResult(exitCode = 1, output = emptyList(), error = listOf(e.message ?: "Unknown error")) + } finally { + executor.shutdownNow() + } + } + + suspend fun isRunitAvailable(containerName: String): Boolean = withContext(Dispatchers.IO) { + try { + val result = Shell.cmd( + "${Constants.DROIDSPACES_BINARY_PATH} --name=${ContainerCommandBuilder.quote(containerName)} run 'command -v sv'" + ).exec() + result.isSuccess && result.out.any { it.trim().isNotEmpty() } + } catch (e: Exception) { + false + } + } + + suspend fun getAllServices(containerName: String): List = coroutineScope { + try { + val runningDeferred = async { runScript(containerName, "--running") } + val enabledDeferred = async { runScript(containerName, "--enabled") } + val disabledDeferred = async { runScript(containerName, "--disabled") } + + val (runningSuccess, runningLines) = runningDeferred.await() + val (enabledSuccess, enabledLines) = enabledDeferred.await() + val (disabledSuccess, disabledLines) = disabledDeferred.await() + + val runningServices = mutableMapOf() + if (runningSuccess) { + runningLines.forEach { line -> + val parts = line.split("|", limit = 2) + if (parts.isNotEmpty() && parts[0].isNotBlank()) { + runningServices[parts[0].trim()] = parts.getOrElse(1) { "" }.trim() + } + } + } + + val enabledSet = mutableMapOf() + if (enabledSuccess) { + enabledLines.forEach { line -> + val parts = line.split("|", limit = 2) + if (parts.isNotEmpty() && parts[0].isNotBlank()) { + enabledSet[parts[0].trim()] = parts.getOrElse(1) { "" }.trim() + } + } + } + + val disabledSet = mutableMapOf() + if (disabledSuccess) { + disabledLines.forEach { line -> + val parts = line.split("|", limit = 2) + if (parts.isNotEmpty() && parts[0].isNotBlank()) { + disabledSet[parts[0].trim()] = parts.getOrElse(1) { "" }.trim() + } + } + } + + val allServices = mutableMapOf() + + enabledSet.forEach { (name, enabledDesc) -> + val isRunning = runningServices.containsKey(name) + val runningDesc = runningServices[name].orEmpty() + val desc = listOf(enabledDesc, runningDesc) + .filter { it.isNotBlank() } + .distinct() + .joinToString(" | ") + allServices[name] = ServiceInfo( + name = name, + description = desc, + status = if (isRunning) ServiceStatus.ENABLED_RUNNING else ServiceStatus.ENABLED_STOPPED, + isEnabled = true, + isRunning = isRunning + ) + } + + disabledSet.forEach { (name, desc) -> + val isRunning = runningServices.containsKey(name) + allServices[name] = ServiceInfo( + name = name, + description = desc, + status = if (isRunning) ServiceStatus.ABNORMAL else ServiceStatus.DISABLED_STOPPED, + isEnabled = false, + isRunning = isRunning + ) + } + + // Any running services not in enabled/disabled lists + runningServices.forEach { (name, desc) -> + if (!allServices.containsKey(name)) { + allServices[name] = ServiceInfo( + name = name, + description = desc, + status = ServiceStatus.ABNORMAL, + isEnabled = false, + isRunning = true + ) + } + } + + allServices.values.sortedBy { it.name } + } catch (e: Exception) { + Log.e(TAG, "Error fetching services", e) + emptyList() + } + } + + fun filterServices(services: List, filter: ServiceFilter): List { + return when (filter) { + ServiceFilter.ALL -> services + ServiceFilter.RUNNING -> services.filter { it.isRunning && it.isEnabled } + ServiceFilter.ENABLED -> services.filter { it.isEnabled && !it.isRunning } + ServiceFilter.DISABLED -> services.filter { !it.isEnabled && !it.isRunning } + ServiceFilter.ABNORMAL -> services.filter { it.isRunning && !it.isEnabled } + }.sortedWith(compareByDescending { it.isRunning }.thenBy { it.name }) + } + + private suspend fun runSV(containerName: String, serviceName: String, buildCommand: (String) -> String): CommandResult { + if (!ServiceManagerBase.isSafeServiceName(serviceName)) { + return CommandResult(exitCode = 2, output = emptyList(), error = listOf("Invalid service name: $serviceName")) + } + return executeRunitCommand(containerName, buildCommand(serviceName)) + } + + suspend fun startService(containerName: String, serviceName: String) = + runSV(containerName, serviceName) { "sv up $it" } + + suspend fun stopService(containerName: String, serviceName: String) = + runSV(containerName, serviceName) { "sv down $it" } + + suspend fun restartService(containerName: String, serviceName: String) = + runSV(containerName, serviceName) { "sv restart $it" } + + suspend fun enableService(containerName: String, serviceName: String) = + runSV(containerName, serviceName) { service -> + """ + ACTIVE_DIR="" + for dir in /etc/service /service /var/service; do + if [ -d "${'$'}dir" ]; then + ACTIVE_DIR="${'$'}dir" + break + fi + done + + SV_DIR="" + for dir in /etc/sv /etc/runit/sv /usr/local/etc/sv; do + if [ -d "${'$'}dir" ]; then + SV_DIR="${'$'}dir" + break + fi + done + + ln -s "${'$'}SV_DIR/$service" "${'$'}ACTIVE_DIR/$service" + """.trimIndent() + } + + suspend fun disableService(containerName: String, serviceName: String) = + runSV(containerName, serviceName) { service -> + """ + ACTIVE_DIR="" + for dir in /etc/service /service /var/service; do + if [ -d "${'$'}dir" ]; then + ACTIVE_DIR="${'$'}dir" + break + fi + done + + sv down $service + rm "${'$'}ACTIVE_DIR/$service" + """.trimIndent() + } + + suspend fun toggleDefaultState(containerName: String, serviceName: String) = + runSV(containerName, serviceName) { service -> + """ + SV_DIR="" + for dir in /etc/sv /etc/runit/sv /usr/local/etc/sv; do + if [ -d "${'$'}dir" ]; then + SV_DIR="${'$'}dir" + break + fi + done + + if [ -f ${'$'}SV_DIR/$service/down ]; then + rm ${'$'}SV_DIR/$service/down + else + touch ${'$'}SV_DIR/$service/down + fi + """.trimIndent() + } +} diff --git a/Android/app/src/main/java/com/droidspaces/app/util/ContainerUsersManager.kt b/Android/app/src/main/java/com/droidspaces/app/util/ContainerUsersManager.kt index 484b17a34..923b12f57 100644 --- a/Android/app/src/main/java/com/droidspaces/app/util/ContainerUsersManager.kt +++ b/Android/app/src/main/java/com/droidspaces/app/util/ContainerUsersManager.kt @@ -54,14 +54,15 @@ object ContainerUsersManager { if command -v systemctl >/dev/null 2>&1; then echo systemd elif [ -f /etc/openwrt_release ] && { [ -x /sbin/procd ] || [ -x /sbin/ubusd ] || command -v ubus >/dev/null 2>&1 || command -v service >/dev/null 2>&1; }; then echo procd elif command -v rc-service >/dev/null 2>&1; then echo openrc + elif command -v sv >/dev/null 2>&1; then echo runit else echo none; fi awk -F: '$3 >= 1000 && $3 < 65534 && $1 !~ /^(nixbld)/ {print $1}' /etc/passwd 2>/dev/null """.trimIndent() - private val INIT_SYSTEMS = setOf("systemd", "procd", "openrc", "none") + private val INIT_SYSTEMS = setOf("systemd", "procd", "openrc", "runit", "none") /** - * The container's init system ("systemd", "procd", "openrc" or "none") and its + * The container's init system ("systemd", "procd", "openrc", "runit" or "none") and its * regular users, from one `run`. Null if the container could not be entered. */ suspend fun probe(containerName: String): Pair>? = withContext(Dispatchers.IO) { diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml index ee3cccb8a..a131b594a 100644 --- a/Android/app/src/main/res/values/strings.xml +++ b/Android/app/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ Talks to the host Linux kernel directly. No hypervisor, no virtual machine layer. Unkillable by design Supports native init.rc integration and 2-layer userspace daemons. Completely unkillable. - systemd / OpenWrt procd / OpenRC as PID 1 + systemd / OpenWrt procd / OpenRC / runit as PID 1 Full init system support. Real service management, not a simulated environment. Complete isolation Private PID tree, mount table, hostname, IPC, and cgroup hierarchy per container. @@ -445,9 +445,11 @@ OpenWrt OpenRC Services OpenRC + Runit Services + runit Init System - Systemd/OpenWrt/OpenRC Not Available - This container does not have systemd, OpenWrt/procd, or OpenRC installed + Systemd/OpenWrt/OpenRC/runit Not Available + This container does not have systemd, OpenWrt/procd, runit, or OpenRC installed Fetching services... Fetching distros from the rootfs repo... @@ -463,6 +465,7 @@ Enable Disable Restart service + Toggle run-on-boot Reload service Mask service Inspect unit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00e78ef1a..56644118e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -417,7 +417,7 @@ boundary. ### Android: init system managers and container introspection -All three init managers share the same shape. The container name is always quoted through +All four init managers share the same shape. The container name is always quoted through `ContainerCommandBuilder`, and the service name is always allow-listed through `ServiceManagerBase`. @@ -426,6 +426,7 @@ All three init managers share the same shape. The container name is always quote | `ContainerSystemdManager` | `util/ContainerSystemdManager.kt` | systemd units. Also owns unit inspection and drop-in override read, write, delete. The base64 streaming in `setOverrideConf` is the pattern to copy for arbitrary text | | `ContainerOpenRCManager` | `util/ContainerOpenRCManager.kt` | OpenRC services | | `ContainerProcdManager` | `util/ContainerProcdManager.kt` | procd services. The only manager that also allow-lists the action | +| `ContainerRunitManager` | `util/ContainerRunitManager.kt` | runit services | | `ContainerProcessManager` | `util/ContainerProcessManager.kt` | Process list and kill inside a container | | `ContainerUsersManager` | `util/ContainerUsersManager.kt` | Container user list, cached | | `ContainerOSInfoManager` | `util/ContainerOSInfoManager.kt` | Distro name, version, icon, and live usage. `OSInfo.ramLabel(context)` is the one formatter for the RAM text, limit aware | @@ -446,7 +447,7 @@ validation. Prefer the wrappers. | `ContainerUsageViewModel` | `ui/viewmodel/ContainerUsageViewModel.kt` | Live usage polling | | `SystemStatsViewModel` | `ui/viewmodel/SystemStatsViewModel.kt` | Per-container OS info | | `RootfsRepoViewModel` | `ui/viewmodel/RootfsRepoViewModel.kt` | Repo listing and per-asset download state | -| `InitServiceViewModel` | `ui/viewmodel/InitServiceViewModel.kt` | Init-system service list, filter and search, shared by systemd, OpenRC and procd | +| `InitServiceViewModel` | `ui/viewmodel/InitServiceViewModel.kt` | Init-system service list, filter and search, shared by systemd, OpenRC, procd and runit | ### Android: terminal From 142e9f630175c4c304b38f3c5bb02461eb55ae66 Mon Sep 17 00:00:00 2001 From: GlitchedCode922 Date: Wed, 21 Oct 2026 17:22:27 +0300 Subject: [PATCH 2/2] Replace "run-on-boot" with "run-at-boot" --- Android/app/src/main/assets/chkrunit.sh | 4 ++-- .../main/java/com/droidspaces/app/ui/screen/RunitScreen.kt | 2 +- Android/app/src/main/res/values/strings.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Android/app/src/main/assets/chkrunit.sh b/Android/app/src/main/assets/chkrunit.sh index 3a6a7628c..2d5ad8c70 100644 --- a/Android/app/src/main/assets/chkrunit.sh +++ b/Android/app/src/main/assets/chkrunit.sh @@ -39,7 +39,7 @@ _all_enabled() { [ -e "$path" ] || [ -L "$path" ] || continue [ -d "$path" ] || continue name=$(basename "$path") - status=$([ -f "$path/down" ] && printf "run-on-boot: disabled" || printf "run-on-boot: enabled" ) + status=$([ -f "$path/down" ] && printf "run-at-boot: disabled" || printf "run-at-boot: enabled" ) printf '%s|%s\n' "$name" "$status" done @@ -53,7 +53,7 @@ _all_disabled() { [ -d "$path" ] || continue name=$(basename "$path") [ -e "$ACTIVE_DIR/$name" ] && continue - status=$([ -f "$path/down" ] && printf "run-on-boot: disabled" || printf "run-on-boot: enabled" ) + status=$([ -f "$path/down" ] && printf "run-at-boot: disabled" || printf "run-at-boot: enabled" ) printf '%s|%s\n' "$name" "$status" done diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/RunitScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/RunitScreen.kt index 873d919d1..fe89cf387 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/RunitScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/RunitScreen.kt @@ -33,7 +33,7 @@ private fun ContainerRunitManager.ServiceInfo.toRow(containerName: String): Init unmask = null, menu = buildList { if (isRunning) add(InitServiceMenuAction.Command(R.string.restart_service, Icons.Default.Refresh) { ContainerRunitManager.restartService(containerName, name).toInit() }) - add(InitServiceMenuAction.Command(R.string.toggle_run_on_boot, Icons.Rounded.PowerSettingsNew) { ContainerRunitManager.toggleDefaultState(containerName, name).toInit() }) + add(InitServiceMenuAction.Command(R.string.toggle_run_at_boot, Icons.Rounded.PowerSettingsNew) { ContainerRunitManager.toggleDefaultState(containerName, name).toInit() }) } ) } diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml index a131b594a..f65348883 100644 --- a/Android/app/src/main/res/values/strings.xml +++ b/Android/app/src/main/res/values/strings.xml @@ -465,7 +465,7 @@ Enable Disable Restart service - Toggle run-on-boot + Toggle run-at-boot Reload service Mask service Inspect unit