Separate CLI concerns a bit

This commit is contained in:
Adam Gastineau 2025-08-09 21:29:44 -07:00
parent 97c388ff93
commit 14b62eceac
3 changed files with 407 additions and 402 deletions

View File

@ -1,37 +1,19 @@
package com.penumbraos.cli
import android.annotation.SuppressLint
import android.util.Log
import com.penumbraos.appprocessmocks.Common
import com.penumbraos.appprocessmocks.MockContext
import com.penumbraos.bridge.IBridge
import com.penumbraos.bridge.ISettingsCallback
import com.penumbraos.bridge.ISettingsProvider
import com.penumbraos.bridge.external.connectToBridge
import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.system.exitProcess
private const val TAG = "CLI"
/**
* CLI program for PenumbraOS Settings
*
* This program connects to the bridge settings service and provides command-line access
* to system settings and module actions like eSIM operations.
*
* Usage:
* pin settings list
* pin settings system audio.volume 75
* pin settings esim getProfiles
* pin settings esim enableProfile --iccid 89012345678901234567
* Main entry point for PenumbraOS CLI
*
* Handles command routing to different subsystems
*/
@SuppressLint("DiscouragedPrivateApi", "PrivateApi")
class Entrypoint {
companion object {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var bridge: IBridge? = null
private var settingsProvider: ISettingsProvider? = null
@JvmStatic
fun main(args: Array<String>) {
@ -46,7 +28,6 @@ class Entrypoint {
scope.cancel()
}
}
exitProcess(0)
}
@ -57,7 +38,10 @@ class Entrypoint {
}
when (args[0].lowercase()) {
"settings" -> handleSettingsCommand(args.drop(1).toTypedArray())
"settings" -> {
val settingsCommand = SettingsCommand(scope)
settingsCommand.execute(args.drop(1).toTypedArray())
}
"help", "--help", "-h" -> showRootHelp()
else -> {
println("Unknown command '${args[0]}'")
@ -67,50 +51,6 @@ class Entrypoint {
}
}
private suspend fun handleSettingsCommand(args: Array<String>) {
if (!connectToServices()) {
println("Failed to connect to settings service")
throw RuntimeException("Service connection failed")
}
if (args.isEmpty()) {
showSettingsHelp()
return
}
when (args[0].lowercase()) {
"list" -> listAvailableOptions()
"help", "--help", "-h" -> showSettingsHelp()
"system" -> handleSystemSetting(args.drop(1).toList())
else -> handleAppAction(args.toList())
}
}
private suspend fun connectToServices(): Boolean {
return try {
val classLoader = ClassLoader.getSystemClassLoader()
val thread = Common.initialize(classLoader)
val context =
MockContext.createWithAppContext(classLoader, thread, "com.android.settings")
Log.i(TAG, "Connecting to bridge service...")
bridge = connectToBridge(TAG, context)
val settingsProviderBinder = bridge?.settingsProvider
if (settingsProviderBinder != null) {
settingsProvider = ISettingsProvider.Stub.asInterface(settingsProviderBinder)
Log.i(TAG, "Connected to settings service")
true
} else {
Log.e(TAG, "Settings provider not available")
false
}
} catch (e: Exception) {
Log.e(TAG, "Failed to connect to bridge", e)
false
}
}
private fun showRootHelp() {
println(
"""
@ -133,340 +73,5 @@ class Entrypoint {
""".trimIndent()
)
}
private fun showSettingsHelp() {
println(
"""
PenumbraOS Settings CLI
Usage:
penumbra settings list - List all available modules and actions
penumbra settings system <setting> [value] - Get or set system setting
penumbra settings <module> <action> [params...] - Execute module action
penumbra settings help - Show this help
Use 'penumbra settings list' to see all available system settings and module actions.
Examples:
penumbra settings list
penumbra settings system audio.volume 75
penumbra settings esim getProfiles
penumbra settings esim enableProfile --iccid 89012345678901234567
""".trimIndent()
)
}
private fun listAvailableOptions() {
val provider = settingsProvider ?: run {
println("Settings service not available")
return
}
println("Available Settings Modules and Actions:")
println()
try {
// Get and display system settings dynamically
val systemSettings = provider.availableSystemSettings
if (systemSettings.isNotEmpty()) {
println("system:")
systemSettings.forEach { setting ->
val readOnlyIndicator = if (setting.readOnly) " (read-only)" else ""
println(" ${setting.key}${readOnlyIndicator} - ${setting.description}")
}
println()
}
// Get and display app actions dynamically
val allActions = provider.allAvailableActions
if (allActions.isNotEmpty()) {
allActions.forEach { appInfo ->
println("${appInfo.appId.lowercase()}:")
appInfo.actions.forEach { action ->
val paramText = if (action.parameters.isNotEmpty()) {
" " + action.parameters.joinToString(" ") { "--${it.name}" }
} else ""
println(" ${action.key}$paramText - ${action.description}")
}
println()
}
} else {
println("No app actions registered")
}
} catch (e: Exception) {
println("Failed to list available options: ${e.message}")
Log.e(TAG, "Error listing available options", e)
}
}
private fun handleSystemSetting(args: List<String>) {
val provider = settingsProvider ?: run {
println("Settings service not available")
return
}
if (args.isEmpty()) {
println("Error: System setting name required")
try {
val availableSettings = provider.availableSystemSettings
if (availableSettings.isNotEmpty()) {
println("Available system settings:")
availableSettings.forEach { setting ->
val readOnlyIndicator = if (setting.readOnly) " (read-only)" else ""
println(" ${setting.key}${readOnlyIndicator} - ${setting.description}")
}
} else {
println("No system settings available")
}
} catch (e: Exception) {
println("Failed to get available system settings: ${e.message}")
}
return
}
val settingKey = args[0]
if (args.size == 1) {
// No args, it's a get
try {
val systemSettings = provider.systemSettings
val value = systemSettings?.get(settingKey)
if (value != null) {
println("$settingKey: $value")
} else {
println("Setting \"$settingKey\" not found")
}
} catch (e: Exception) {
println("Failed to get setting: ${e.message}")
Log.e(TAG, "Error getting system setting", e)
}
} else {
// More args, it's a set
val value = args[1]
try {
provider.updateSystemSetting(settingKey, value)
println("Successfully set")
} catch (e: Exception) {
println("Failed to set setting: ${e.message}")
Log.e(TAG, "Error setting system setting", e)
}
}
}
private suspend fun handleAppAction(args: List<String>) {
val provider = settingsProvider ?: run {
println("Settings service not available")
return
}
if (args.size < 2) {
println("Error: Module and action required")
println("Use 'pin settings list' to see available options")
return
}
val module = args[0].lowercase()
val action = args[1]
val params = parseParameters(args.drop(2))
// Validate module exists
try {
val registeredApps = provider.registeredApps
if (module !in registeredApps) {
println(
"Unknown module '$module'. Available modules: ${
registeredApps.joinToString(
", "
)
}"
)
return
}
// Validate action exists for this module
val availableActions = provider.getAppActions(module)
val actionExists = availableActions.any { it.key == action }
if (!actionExists) {
println("Unknown action '$action' for module '$module'")
if (availableActions.isNotEmpty()) {
println("Available actions for $module:")
availableActions.forEach { actionDef ->
val paramText = if (actionDef.parameters.isNotEmpty()) {
" " + actionDef.parameters.joinToString(" ") { "--${it.name}" }
} else ""
println(" ${actionDef.key}$paramText - ${actionDef.description}")
}
}
return
}
} catch (e: Exception) {
println("Failed to validate module/action: ${e.message}")
return
}
println("Executing $module.$action...")
if (params.isNotEmpty()) {
println("Parameters: ${params.map { "${it.key}=${it.value}" }.joinToString(", ")}")
}
try {
val aidlParams = params.mapValues { it.value as Any }.toMutableMap()
val result = executeActionSync(provider, module, action, aidlParams)
if (result.success) {
println("Action completed successfully")
if (!result.message.isNullOrEmpty()) {
println("Message: ${result.message}")
}
if (result.data.isNotEmpty()) {
println()
formatData(result.data)
}
} else {
println("Action failed: ${result.message ?: "Unknown error"}")
}
} catch (e: Exception) {
println("Failed to execute action: ${e.message}")
Log.e(TAG, "Error executing action", e)
}
}
private data class ActionResult(
val success: Boolean,
val message: String?,
val data: Map<String, Any> = emptyMap()
)
private suspend fun executeActionSync(
provider: ISettingsProvider,
appId: String,
action: String,
params: Map<String, Any>
): ActionResult = suspendCancellableCoroutine { continuation ->
val callback = object : ISettingsCallback.Stub() {
override fun onSettingChanged(
appId: String,
category: String,
key: String,
value: String
) {
}
override fun onSettingsRegistered(appId: String, category: String) {}
override fun onError(message: String) {
if (continuation.isActive) {
continuation.resume(ActionResult(success = false, message = message))
}
}
override fun onActionResult(
appId: String,
action: String,
success: Boolean,
message: String,
data: Map<*, *>
) {
if (continuation.isActive) {
@Suppress("UNCHECKED_CAST")
val convertedData = data as? Map<String, Any> ?: emptyMap()
continuation.resume(
ActionResult(
success = success,
message = message,
data = convertedData
)
)
}
}
}
val timeoutJob = scope.launch {
delay(30000)
if (continuation.isActive) {
continuation.resume(ActionResult(success = false, message = "Action timed out"))
}
}
continuation.invokeOnCancellation {
timeoutJob.cancel()
}
try {
provider.executeActionWithCallback(appId, action, params, callback)
} catch (e: Exception) {
timeoutJob.cancel()
if (continuation.isActive) {
continuation.resume(
ActionResult(
success = false,
message = "Failed to execute: ${e.message}"
)
)
}
}
}
private fun formatData(data: Any?, indent: Int = 0) {
val prefix = " ".repeat(indent)
when (data) {
is Map<*, *> -> {
data.forEach { (key, value) ->
print("${prefix}${key}: ")
when (value) {
is Map<*, *>, is List<*> -> {
println()
formatData(value, indent + 1)
}
null -> println("(null)")
else -> println(value)
}
}
}
is List<*> -> {
if (data.isEmpty()) {
println("${prefix}(empty)")
} else {
data.forEachIndexed { index, item ->
print("${prefix}[$index]: ")
when (item) {
is Map<*, *>, is List<*> -> {
println()
formatData(item, indent + 1)
}
null -> println("(null)")
else -> println(item)
}
}
}
}
else -> println("${prefix}$data")
}
}
private fun parseParameters(args: List<String>): Map<String, String> {
val params = mutableMapOf<String, String>()
var i = 0
while (i < args.size) {
val arg = args[i]
if (arg.startsWith("--")) {
val paramName = arg.substring(2)
if (i + 1 < args.size && !args[i + 1].startsWith("--")) {
params[paramName] = args[i + 1]
i += 2
} else {
println("Warning: Parameter $arg missing value")
i++
}
} else {
println("Warning: Unexpected argument $arg (expected --param format)")
i++
}
}
return params
}
}
}

View File

@ -0,0 +1,41 @@
package com.penumbraos.cli
object OutputFormatter {
fun formatData(data: Any?, indent: Int = 0) {
val prefix = " ".repeat(indent)
when (data) {
is Map<*, *> -> {
data.forEach { (key, value) ->
print("${prefix}${key}: ")
when (value) {
is Map<*, *>, is List<*> -> {
println()
formatData(value, indent + 1)
}
null -> println("(null)")
else -> println(value)
}
}
}
is List<*> -> {
if (data.isEmpty()) {
println("${prefix}(empty)")
} else {
data.forEachIndexed { index, item ->
print("${prefix}[$index]: ")
when (item) {
is Map<*, *>, is List<*> -> {
println()
formatData(item, indent + 1)
}
null -> println("(null)")
else -> println(item)
}
}
}
}
else -> println("${prefix}$data")
}
}
}

View File

@ -0,0 +1,359 @@
package com.penumbraos.cli
import android.annotation.SuppressLint
import android.util.Log
import com.penumbraos.appprocessmocks.Common
import com.penumbraos.appprocessmocks.MockContext
import com.penumbraos.bridge.IBridge
import com.penumbraos.bridge.ISettingsCallback
import com.penumbraos.bridge.ISettingsProvider
import com.penumbraos.bridge.external.connectToBridge
import kotlinx.coroutines.*
import kotlin.coroutines.resume
private const val TAG = "SettingsCommand"
@SuppressLint("DiscouragedPrivateApi", "PrivateApi")
class SettingsCommand(private val scope: CoroutineScope) {
private var bridge: IBridge? = null
private var settingsProvider: ISettingsProvider? = null
data class ActionResult(
val success: Boolean,
val message: String?,
val data: Map<String, Any> = emptyMap()
)
suspend fun execute(args: Array<String>) {
if (!connectToServices()) {
println("Failed to connect to settings service")
throw RuntimeException("Service connection failed")
}
if (args.isEmpty()) {
showHelp()
return
}
when (args[0].lowercase()) {
"list" -> listAvailableOptions()
"help", "--help", "-h" -> showHelp()
"system" -> handleSystemSetting(args.drop(1).toList())
else -> handleAppAction(args.toList())
}
}
private suspend fun connectToServices(): Boolean {
return try {
val classLoader = ClassLoader.getSystemClassLoader()
val thread = Common.initialize(classLoader)
val context =
MockContext.createWithAppContext(classLoader, thread, "com.android.settings")
Log.i(TAG, "Connecting to bridge service...")
bridge = connectToBridge(TAG, context)
val settingsProviderBinder = bridge?.settingsProvider
if (settingsProviderBinder != null) {
settingsProvider = ISettingsProvider.Stub.asInterface(settingsProviderBinder)
Log.i(TAG, "Connected to settings service")
true
} else {
Log.e(TAG, "Settings provider not available")
false
}
} catch (e: Exception) {
Log.e(TAG, "Failed to connect to bridge", e)
false
}
}
private fun showHelp() {
println(
"""
PenumbraOS Settings CLI
Usage:
penumbra settings list - List all available modules and actions
penumbra settings system <setting> [value] - Get or set system setting
penumbra settings <module> <action> [params...] - Execute module action
penumbra settings help - Show this help
Use 'penumbra settings list' to see all available system settings and module actions.
Examples:
penumbra settings list
penumbra settings system audio.volume 75
penumbra settings esim getProfiles
penumbra settings esim enableProfile --iccid 89012345678901234567
""".trimIndent()
)
}
private fun listAvailableOptions() {
val provider = settingsProvider ?: run {
println("Settings service not available")
return
}
println("Available Settings Modules and Actions:")
println()
try {
// Get and display system settings dynamically
val systemSettings = provider.availableSystemSettings
if (systemSettings.isNotEmpty()) {
println("system:")
systemSettings.forEach { setting ->
val readOnlyIndicator = if (setting.readOnly) " (read-only)" else ""
println(" ${setting.key}${readOnlyIndicator} - ${setting.description}")
}
println()
}
// Get and display app actions dynamically
val allActions = provider.allAvailableActions
if (allActions.isNotEmpty()) {
allActions.forEach { appInfo ->
println("${appInfo.appId.lowercase()}:")
appInfo.actions.forEach { action ->
val paramText = if (action.parameters.isNotEmpty()) {
" " + action.parameters.joinToString(" ") { "--${it.name}" }
} else ""
println(" ${action.key}$paramText - ${action.description}")
}
println()
}
} else {
println("No app actions registered")
}
} catch (e: Exception) {
println("Failed to list available options: ${e.message}")
Log.e(TAG, "Error listing available options", e)
}
}
private fun handleSystemSetting(args: List<String>) {
val provider = settingsProvider ?: run {
println("Settings service not available")
return
}
if (args.isEmpty()) {
println("Error: System setting name required")
try {
val availableSettings = provider.availableSystemSettings
if (availableSettings.isNotEmpty()) {
println("Available system settings:")
availableSettings.forEach { setting ->
val readOnlyIndicator = if (setting.readOnly) " (read-only)" else ""
println(" ${setting.key}${readOnlyIndicator} - ${setting.description}")
}
} else {
println("No system settings available")
}
} catch (e: Exception) {
println("Failed to get available system settings: ${e.message}")
}
return
}
val settingKey = args[0]
if (args.size == 1) {
// No args, it's a get
try {
val systemSettings = provider.systemSettings
val value = systemSettings?.get(settingKey)
if (value != null) {
println("$settingKey: $value")
} else {
println("Setting \"$settingKey\" not found")
}
} catch (e: Exception) {
println("Failed to get setting: ${e.message}")
Log.e(TAG, "Error getting system setting", e)
}
} else {
// More args, it's a set
val value = args[1]
try {
provider.updateSystemSetting(settingKey, value)
println("Successfully set")
} catch (e: Exception) {
println("Failed to set setting: ${e.message}")
Log.e(TAG, "Error setting system setting", e)
}
}
}
private suspend fun handleAppAction(args: List<String>) {
val provider = settingsProvider ?: run {
println("Settings service not available")
return
}
if (args.size < 2) {
println("Error: Module and action required")
println("Use 'penumbra settings list' to see available options")
return
}
val module = args[0].lowercase()
val action = args[1]
val params = parseParameters(args.drop(2))
// Validate module exists
try {
val registeredApps = provider.registeredApps
if (module !in registeredApps) {
println(
"Unknown module '$module'. Available modules: ${
registeredApps.joinToString(", ")
}"
)
return
}
// Validate action exists for this module
val availableActions = provider.getAppActions(module)
val actionExists = availableActions.any { it.key == action }
if (!actionExists) {
println("Unknown action '$action' for module '$module'")
if (availableActions.isNotEmpty()) {
println("Available actions for $module:")
availableActions.forEach { actionDef ->
val paramText = if (actionDef.parameters.isNotEmpty()) {
" " + actionDef.parameters.joinToString(" ") { "--${it.name}" }
} else ""
println(" ${actionDef.key}$paramText - ${actionDef.description}")
}
}
return
}
} catch (e: Exception) {
println("Failed to validate module/action: ${e.message}")
return
}
println("Executing $module.$action...")
if (params.isNotEmpty()) {
println("Parameters: ${params.map { "${it.key}=${it.value}" }.joinToString(", ")}")
}
try {
val aidlParams = params.mapValues { it.value as Any }.toMutableMap()
val result = executeActionSync(provider, module, action, aidlParams)
if (result.success) {
println("Action completed successfully")
if (!result.message.isNullOrEmpty()) {
println("Message: ${result.message}")
}
if (result.data.isNotEmpty()) {
println()
OutputFormatter.formatData(result.data)
}
} else {
println("Action failed: ${result.message ?: "Unknown error"}")
}
} catch (e: Exception) {
println("Failed to execute action: ${e.message}")
Log.e(TAG, "Error executing action", e)
}
}
private suspend fun executeActionSync(
provider: ISettingsProvider,
appId: String,
action: String,
params: Map<String, Any>
): ActionResult = suspendCancellableCoroutine { continuation ->
val callback = object : ISettingsCallback.Stub() {
override fun onSettingChanged(
appId: String,
category: String,
key: String,
value: String
) {
}
override fun onSettingsRegistered(appId: String, category: String) {}
override fun onError(message: String) {
if (continuation.isActive) {
continuation.resume(ActionResult(success = false, message = message))
}
}
override fun onActionResult(
appId: String,
action: String,
success: Boolean,
message: String,
data: Map<*, *>
) {
if (continuation.isActive) {
@Suppress("UNCHECKED_CAST")
val convertedData = data as? Map<String, Any> ?: emptyMap()
continuation.resume(
ActionResult(
success = success,
message = message,
data = convertedData
)
)
}
}
}
val timeoutJob = scope.launch {
delay(30000)
if (continuation.isActive) {
continuation.resume(ActionResult(success = false, message = "Action timed out"))
}
}
continuation.invokeOnCancellation {
timeoutJob.cancel()
}
try {
provider.executeActionWithCallback(appId, action, params, callback)
} catch (e: Exception) {
timeoutJob.cancel()
if (continuation.isActive) {
continuation.resume(
ActionResult(
success = false,
message = "Failed to execute: ${e.message}"
)
)
}
}
}
private fun parseParameters(args: List<String>): Map<String, String> {
val params = mutableMapOf<String, String>()
var i = 0
while (i < args.size) {
val arg = args[i]
if (arg.startsWith("--")) {
val paramName = arg.substring(2)
if (i + 1 < args.size && !args[i + 1].startsWith("--")) {
params[paramName] = args[i + 1]
i += 2
} else {
println("Warning: Parameter $arg missing value")
i++
}
} else {
println("Warning: Unexpected argument $arg (expected --param format)")
i++
}
}
return params
}
}