From 619e4c90a2c76eeaa63969d1f3a37f32812df3ad Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 30 Jan 2024 18:32:07 +0300 Subject: [PATCH] Throw and handle exception from sample 'appIdFor' --- .../kotlin/dogcat/DogcatException.kt | 3 + src/commonMain/kotlin/dogcat/Shell.kt | 3 +- src/nativeMain/kotlin/AdbShell.kt | 88 +++++++++++++------ src/nativeMain/kotlin/Main.kt | 54 ++++++------ src/nativeMain/kotlin/ui/AppPresenter.kt | 55 +++++------- .../kotlin/ui/logLines/LogLinesPresenter.kt | 24 ++--- 6 files changed, 119 insertions(+), 108 deletions(-) create mode 100644 src/commonMain/kotlin/dogcat/DogcatException.kt diff --git a/src/commonMain/kotlin/dogcat/DogcatException.kt b/src/commonMain/kotlin/dogcat/DogcatException.kt new file mode 100644 index 0000000..eefd941 --- /dev/null +++ b/src/commonMain/kotlin/dogcat/DogcatException.kt @@ -0,0 +1,3 @@ +package dogcat + +class DogcatException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/src/commonMain/kotlin/dogcat/Shell.kt b/src/commonMain/kotlin/dogcat/Shell.kt index 9ad641f..b8deadc 100644 --- a/src/commonMain/kotlin/dogcat/Shell.kt +++ b/src/commonMain/kotlin/dogcat/Shell.kt @@ -6,7 +6,7 @@ interface Shell { fun lines(minLogLevel: String, userId: String) : Flow - suspend fun userIdFor(packageName: String): String + suspend fun appIdFor(packageName: String): String suspend fun currentEmulatorName(): String? @@ -20,3 +20,4 @@ interface Shell { suspend fun isShellAvailable(): Boolean } + diff --git a/src/nativeMain/kotlin/AdbShell.kt b/src/nativeMain/kotlin/AdbShell.kt index 60d63c4..e14a008 100644 --- a/src/nativeMain/kotlin/AdbShell.kt +++ b/src/nativeMain/kotlin/AdbShell.kt @@ -4,7 +4,8 @@ import com.kgit2.kommand.exception.KommandException import com.kgit2.kommand.process.Child import com.kgit2.kommand.process.Command import com.kgit2.kommand.process.Stdio -import dogcat.DogcatConfig +import com.kgit2.kommand.process.Stdio.Pipe +import dogcat.DogcatException import dogcat.Shell import kotlinx.coroutines.* import kotlinx.coroutines.channels.ClosedReceiveChannelException @@ -41,7 +42,7 @@ class AdbShell( .args( listOf("logcat", "-v", "brief", userId, minLogLevel) ) - .stdout(Stdio.Pipe) + .stdout(Pipe) .spawn() } catch (e: KommandException) { @@ -126,28 +127,44 @@ class AdbShell( } } - override suspend fun userIdFor(packageName: String) = withContext(dispatcherIo) { - val UID_CONTEXT = """Packages:\R\s+Package\s+\[$packageName]\s+\(.*\):\R\s+(?:appId|userId)=(\d*)""".toRegex() - val output = withTimeout(COMMAND_TIMEOUT_MILLIS) { - Command("adb") - .args( - listOf("shell", "dumpsys", "package") - ) - .arg(packageName) - .stdout(Stdio.Pipe) - .output() - } + override suspend fun appIdFor(packageName: String): String { + val appIdContext = + """Packages:\R\s+Package\s+\[$packageName]\s+\(.*\):\R\s+(?:appId|userId)=(\d*)""".toRegex() + + val appId = try { + withContext(dispatcherIo) { + val commandOutput = withTimeout(COMMAND_TIMEOUT_MILLIS) { + // Looks like despite its claims, Kommand still doesn't support timeouts + Command("adb") + .args( + listOf("shell", "dumpsys", "package") + ) + .arg(packageName) + .stdout(Pipe) + .output() + } - val userId = output.stdout?.let { - val match = UID_CONTEXT.find(it) - match?.let { - val (userId) = it.destructured - userId + val appId = commandOutput.stdout?.let { + val match = appIdContext.find(it) + + match?.let { + val (id) = it.destructured + id + } + } + + appId } + } catch (e: KommandException) { + throw DogcatException("Couldn't launch ADB command", e) + + } catch (e: TimeoutCancellationException) { + throw DogcatException("Running ADB timed out", e) } - userId ?: throw RuntimeException("UserId not found!") + return appId + ?: throw DogcatException("App ID is not found for the package '$packageName'. Package is not installed on device.") } override suspend fun currentEmulatorName() = withContext(Dispatchers.IO) { @@ -155,7 +172,7 @@ class AdbShell( .args( listOf("emu", "avd", "name") ) - .stdout(Stdio.Pipe) + .stdout(Pipe) .output() .stdout ?.lines() @@ -172,7 +189,7 @@ class AdbShell( .args( listOf("shell", "dumpsys", "activity", "activities") ) - .stdout(Stdio.Pipe) + .stdout(Pipe) .spawn() val stdoutReader = out.bufferedStdout()!!// getChildStdout()!! @@ -197,14 +214,27 @@ class AdbShell( } override suspend fun clearSource(): Boolean { - val childStatus = withContext(dispatcherIo) { - Command("adb") - .args( - listOf("logcat", "-c") - ) - .status() + val childStatus = try { + withContext(dispatcherIo) { + withTimeout(3000) { + Command("adb") + .args( + listOf("logcat", "-c") + ) + .status() + } + } + } catch (e: KommandException) { + //log + + return false + } catch (e: TimeoutCancellationException) { + + //or throw? + return false } + Logger.d("${context()} Exit code for 'adb logcat -c': ${childStatus}") return childStatus == 0 @@ -240,7 +270,7 @@ class AdbShell( .args( listOf("emu", "avd", "status") ) - .stdout(Stdio.Pipe) + .stdout(Pipe) .output() .stdout ?.lines() @@ -264,7 +294,7 @@ class AdbShell( .args( listOf("version") ) - .stdout(Stdio.Pipe) + .stdout(Pipe) .status() } catch (e: KommandException) { //whoa exception and not code if command not found diff --git a/src/nativeMain/kotlin/Main.kt b/src/nativeMain/kotlin/Main.kt index a84306f..e4638b5 100644 --- a/src/nativeMain/kotlin/Main.kt +++ b/src/nativeMain/kotlin/Main.kt @@ -2,10 +2,10 @@ import di.AppModule import kotlinx.coroutines.* import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import logger.Logger import logger.context -import platform.posix.signal import userInput.Arguments import userInput.Keymap import userInput.Keymap.Actions @@ -16,34 +16,38 @@ fun main(args: Array) { val ui = newSingleThreadContext("UI") - val handler = CoroutineExceptionHandler { _, t -> - Logger.d("!!!!!!11111111 CATCH! ${t.message}") - //do we need to just terminate the app and write message to stdout on exit rather than logging? + val handler = CoroutineExceptionHandler { c, t -> + Logger.d("CATCH! ${t.message}") + println("${t.message}") } runBlocking(ui) { - val appModule = AppModule(ui) - - with(appModule) { - Logger.set(fileLogger) - - input.start() - appPresenter.start() - - input - .keypresses - .filter { - Keymap.bindings[it] == Actions.Quit - } - .onEach { - Logger.d("${context()} Cancel scope") - - //make sure to cancel last leaking ADB - appPresenter.stop() - coroutineContext.cancelChildren() - } - .launchIn(this@runBlocking) + //he key takeaway is that if you call launch on a custom CoroutineScope, any CoroutineExceptionHandler provided + // directly to the CoroutineScope constructor or to launch will be executed when an exception is thrown within the launched coroutine. + val appJob = CoroutineScope(ui).launch(handler) { + val appModule = AppModule(ui) + + with(appModule) { + Logger.set(fileLogger) + + input.start() + appPresenter.start() + + input + .keypresses + .filter { + Keymap.bindings[it] == Actions.Quit + } + .onEach { + Logger.d("${context()} Cancel scope") + + appPresenter.stop() + coroutineContext.cancelChildren() + } + .launchIn(this@launch) + } } + appJob.join() } ui.close() diff --git a/src/nativeMain/kotlin/ui/AppPresenter.kt b/src/nativeMain/kotlin/ui/AppPresenter.kt index 92da361..0d0dbbf 100644 --- a/src/nativeMain/kotlin/ui/AppPresenter.kt +++ b/src/nativeMain/kotlin/ui/AppPresenter.kt @@ -9,68 +9,68 @@ import dogcat.state.PublicState import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import logger.Logger import logger.context import ui.logLines.LogLinesPresenter import ui.status.StatusPresenter import userInput.Arguments -import userInput.HasHifecycle import userInput.Input import userInput.Keymap import userInput.Keymap.Actions.* import kotlin.coroutines.coroutineContext -@OptIn(ExperimentalForeignApi::class) class AppPresenter( private val dogcat: Dogcat, private val appStateFlow: AppStateFlow, private val input: Input, private val logLinesPresenter: LogLinesPresenter, private val statusPresenter: StatusPresenter, -) : HasHifecycle { +) : HasLifecycle { private val view = AppView() - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun start() { - view.start() + when { + Arguments.packageName != null -> dogcat(Start.PickAppPackage(Arguments.packageName!!)) + Arguments.current == true -> dogcat(Start.PickForegroundApp) + else -> dogcat(Start.PickAllApps) + } - val s = CoroutineScope(coroutineContext) + view.start() - s.launch { + val scope = CoroutineScope(coroutineContext) + scope.launch { collectDogcatEvents() } - s.launch { + scope.launch { collectKeypresses() } - when { - Arguments.packageName != null -> dogcat(Start.PickApp(Arguments.packageName!!)) - Arguments.current == true -> dogcat(Start.PickForegroundApp) - else -> dogcat(Start.All) - } - logLinesPresenter.start() - //statusPresenter.start() + statusPresenter.start() } override suspend fun stop() { + dogcat(Stop) + logLinesPresenter.stop() statusPresenter.stop() view.stop() } - @OptIn(ExperimentalCoroutinesApi::class) private suspend fun collectDogcatEvents() { dogcat .state - .filterIsInstance() + .filterIsInstance() .collect { println( - "Either ADB is not found in your PATH or it's found but no emulator is running ") - + "Either ADB is not found in your PATH or it's found but no emulator is running " + ) } } @@ -82,19 +82,6 @@ class AppPresenter( Autoscroll -> { appStateFlow.autoscroll(!appStateFlow.state.value.autoscroll) } - Quit -> { // catch control-c - //dogcat(Command.Stop) - //coroutineContext.cancelChildren() - //currentCoroutineContext().cancelChildren() - //scope.coroutineContext.cancelChildren() -- only this works - Logger.d("{${context()} ++++++ cancelled ${coroutineContext}'s job") - //endwin() - - //pad.terminate() - //pad2.terminate() - //resetty() - //exit(0) - } ClearLogs -> { dogcat(ClearLogSource) @@ -109,7 +96,7 @@ class AppPresenter( dogcat(ResetFilter(ByPackage::class)) } else { Logger.d("${context()} !SelectAppByPackage") - dogcat(Start.PickApp(f.first!!.packageName)) + dogcat(Start.PickAppPackage(f.first!!.packageName)) appStateFlow.filterByPackage(f.first, true) } } diff --git a/src/nativeMain/kotlin/ui/logLines/LogLinesPresenter.kt b/src/nativeMain/kotlin/ui/logLines/LogLinesPresenter.kt index 6eb2ccb..30e72a6 100644 --- a/src/nativeMain/kotlin/ui/logLines/LogLinesPresenter.kt +++ b/src/nativeMain/kotlin/ui/logLines/LogLinesPresenter.kt @@ -11,20 +11,16 @@ import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import logger.context -import ncurses.curs_set -import ncurses.wmove -import userInput.HasHifecycle +import ui.HasLifecycle import userInput.Keymap -import windowed import kotlin.coroutines.coroutineContext -import kotlin.time.Duration.Companion.milliseconds class LogLinesPresenter( private val dogcat: Dogcat, private val appStateFlow: AppStateFlow, private val input: Input, private val uiDispatcher: CoroutineDispatcher -) : HasHifecycle { +) : HasLifecycle { //views can come and go, when input disappears private lateinit var view: LogLinesView @@ -55,17 +51,7 @@ class LogLinesPresenter( .state .flatMapLatest { when (it) { - is WaitingInput -> { - Logger.d("${context()} Waiting for log lines...\r") - val waiting = "--------- log lines are empty, let's wait -- waiting" - - withContext(uiDispatcher) { - view.processLogLine(IndexedValue(0, Unparseable(waiting))) - } - emptyFlow() - } - - is CapturingInput -> { + is Active -> { Logger.d("${context()} Capturing input...") i = 0 //make sure no capturing happens after clearing @@ -91,7 +77,7 @@ class LogLinesPresenter( it.lines//.windowed(500.milliseconds)//.dropWhile { (it.value as LogLine).message == "" } } - InputCleared -> { + Inactive -> { Logger.d("${context()} Cleared Logcat and re-started\r") /* withContext(ui) { @@ -107,7 +93,7 @@ class LogLinesPresenter( emptyFlow() } - Stopped -> { + Terminated -> { Logger.d("${context()} No more reading lines, terminated\r") emptyFlow() }