diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 07c41b93..fc7d17de 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -67,7 +67,7 @@ jobs: - name: Cargo test run: | cd rust - nix develop --command cargo test --workspace --all-targets --all-features --exclude client + nix develop --command cargo test --workspace --all-targets --all-features --exclude client --exclude backend-ebpf-test rust-build: name: Rust Build @@ -115,3 +115,8 @@ jobs: --parallel \ --build-cache \ -Dorg.gradle.jvmargs=-Xmx4G + - name: Upload Detekt results to GitHub + uses: github/codeql-action/upload-sarif@v2 + if: success() || failure() + with: + sarif_file: frontend/app/build/reports/detekt/detekt.sarif diff --git a/Deliverables/sprint-10/feature-board.jpg b/Deliverables/sprint-10/feature-board.jpg new file mode 100644 index 00000000..559e0524 Binary files /dev/null and b/Deliverables/sprint-10/feature-board.jpg differ diff --git a/Deliverables/sprint-10/feature-board.tsv b/Deliverables/sprint-10/feature-board.tsv new file mode 100644 index 00000000..561d9abf --- /dev/null +++ b/Deliverables/sprint-10/feature-board.tsv @@ -0,0 +1,76 @@ +Title URL Assignees Status Estimated size Real size +Refactor: SIGQUIT https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/186 der-whity Awaiting Review 1 +Daemon: config SIGQUIT https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/174 der-whity Awaiting Review 1 +Collector: for information resulting from SIGQUIT calls https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/175 Mr-Kanister Awaiting Review 1 +Frontend: SIGQUIT https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/176 luca-dot-sh Awaiting Review 2 +Uprobe: Trace JNI symbols https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/182 Sprint Backlog 3 +Uprobe: expand client library https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/183 Sprint Backlog 3 +Prototype for overlay mode https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/178 luca-dot-sh In Progress 3 +Visualize JNI Reference Metrics https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/156 luca-dot-sh Awaiting Review 3 +Integration Testing 2 https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/166 fhilgers, Mr-Kanister Awaiting Review 5 +In memory testing https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/163 ffranzgitHub, fhilgers Awaiting Review 2 +Testing Ebpf Programs https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/161 fhilgers Awaiting Review 3 +Aggregate Data Points in Background for Efficient Processing https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/158 BenediktZinn, Mr-Kanister In Progress 2 +Integration Testing https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/165 Mr-Kanister Feature Archive 3 2 +UI: Search bar to filter out App/Process https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/141 luca-dot-sh Feature Archive 2 2 +Ebpf: SIGQUIT https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/173 der-whity Feature Archive 2 2 +Uprobe Analysis: Finding Symbols from shared libraries https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/116 Mr-Kanister Feature Archive 2 2 +Refactor: Collection of events in Daemon https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/123 fhilgers Feature Archive 2 5 +Refactor: Configuration API https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/124 ffranzgitHub Feature Archive 2 2 +Uprobe Analysis: Frontend Show Symbols https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/121 luca-dot-sh Feature Archive 2 3 +Refactoring pIDs to uint32 https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/149 Mr-Kanister Feature Archive 2 1 +Uprobe Analysis: Collect Uprobe events https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/119 BenediktZinn, Mr-Kanister Feature Archive 1 3 +Uprobe Analysis: Finding Symbols from Dex/Oat https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/117 BenediktZinn, Mr-Kanister Feature Archive 5 8 +Uprobe Analysis: Setup ebpf uprobes https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/118 der-whity Feature Archive 3 2 +Uprobe Analysis: Config https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/120 der-whity, Mr-Kanister Feature Archive 2 2 +Refactoring ebpf Programs https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/151 der-whity Feature Archive 1 1 +EPIC: uprobe https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/19 In Progress 8 +Uprobe Analysis: Frontend Show Uprobe Events https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/122 luca-dot-sh Sprint Backlog 2 +EPIC: Create a Databank in the Backend https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/155 fhilgers In Progress +Actor Refactor https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/164 In Progress 1 +Mocking IO in userspace daemon https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/162 ffranzgitHub, fhilgers In Progress 3 +Gradle refactoring https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/140 fhilgers Feature Archive 5 +CI Rework https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/139 fhilgers Feature Archive 1 +Uprobe Analysis: Setup https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/115 fhilgers Feature Archive 2 5 +Define metric for the visualisation screen https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/46 luca-dot-sh Feature Archive 3 3 +Refactoring: Frontend https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/130 luca-dot-sh Feature Archive 2 2 +Unix Domain Socket: Ebpf https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/111 der-whity Feature Archive 2 2 +Unix Domain Socket: Frontend https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/114 luca-dot-sh Feature Archive 3 3 +Unix Domain Socket: Configuration https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/112 ffranzgitHub, Mr-Kanister Feature Archive 2 2 +Unix Domain Socket: Daemon Collector https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/113 BenediktZinn, ffranzgitHub, fhilgers Feature Archive 2 2 +Identify Running State https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/75 luca-dot-sh Feature Archive 3 2 +Plaintext architecture document https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/34 fhilgers, Mr-Kanister Feature Archive 3 3 +EPIC: analyze unix domain sockets https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/80 BenediktZinn, der-whity, ffranzgitHub, fhilgers, luca-dot-sh, Mr-Kanister Feature Archive 5 -1 +internal: implement client library and export to kotlin for load and list programs https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/60 BenediktZinn, fhilgers Feature Archive 2 1 +Display running processes in UI https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/73 luca-dot-sh Feature Archive 2 2 +User eBPF programm Selection https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/41 ffranzgitHub, fhilgers, Mr-Kanister Feature Archive 5 -1 +internal: define kotlin interface for frontend loading and listing programs https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/58 fhilgers Feature Archive 1 1 +internal: implement loading/unloading of ebpf functions in daemon https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/77 der-whity, ffranzgitHub, Mr-Kanister Feature Archive 2 3 +Retrieve running processes List https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/72 Mr-Kanister Feature Archive 3 3 +Home Screen and Navigation Drawer https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/43 luca-dot-sh Feature Archive 2 3 +EBPF Program extension https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/54 der-whity Feature Archive 3 3 +Bugfix: Manage Sbom generation through nix https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/55 Mr-Kanister Feature Archive 1 1 +Communcation between Android side and Rust side https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/42 fhilgers Feature Archive 5 5 +scope(ebpf) unix domain socket traffic analysis (research) https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/22 fhilgers Feature Archive 5 3 +Create a prototype for the visualisation screen https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/47 luca-dot-sh Feature Archive 3 3 +internal: implement frontend load and list programs https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/62 luca-dot-sh Feature Archive 2 2 +internal: implement test cli client load and list programs https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/59 BenediktZinn, ffranzgitHub, Mr-Kanister Feature Archive 3 2 +Preparation of CI https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/6 fhilgers Feature Archive 3 3 +scope(ui) find timeseries visualization library (research) https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/21 clabrous, luca-dot-sh Feature Archive 2 2 +Generation of sboms doesn't include kotlin https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/39 Feature Archive 1 1 +Manage Sbom generation through nix https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/30 Mr-Kanister Feature Archive 2 1 +scope(daemon) get information about android processes to list/find/search them (research) https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/20 der-whity, ffranzgitHub Feature Archive 3 1 +License and Copyright Agreement https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/18 fhilgers Feature Archive 2 2 +Preperation of Kotlin https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/7 clabrous, luca-dot-sh Feature Archive 3 3 +Docker Container https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/11 fhilgers Feature Archive 3 3 +scope(build) aarch64 als target https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/23 fhilgers Feature Archive 1 1 +scope(build) android 13 instead of 15 https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/24 fhilgers Feature Archive 1 1 +Team Decision https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/8 BenediktZinn, clabrous, der-whity, ffranzgitHub, fhilgers, luca-dot-sh, Mr-Kanister Feature Archive 1 1 +architecture document https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/10 fhilgers Feature Archive 3 5 +bill of materials https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/9 BenediktZinn, der-whity, Mr-Kanister Feature Archive 3 3 +Brain Storming Architecture https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/3 Feature Archive 3 1 +Distinguish between System vs. User Applications https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/74 Product Backlog 3 +UI Filter for System and User Applications https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/76 Product Backlog 2 +Display Installed Applications in UI https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/25 Product Backlog -1 +Brain Storming eBPF Use Cases https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/1 Product Backlog 2 +Create kprobe to Track Blocking vfs_write Call https://github.com/amosproj/amos2024ws03-android-zero-instrumentation/issues/88 der-whity Feature Archive 3 3 \ No newline at end of file diff --git a/Deliverables/sprint-10/imp-squared-backlog.jpg b/Deliverables/sprint-10/imp-squared-backlog.jpg new file mode 100644 index 00000000..e14329db Binary files /dev/null and b/Deliverables/sprint-10/imp-squared-backlog.jpg differ diff --git a/Deliverables/sprint-10/imp-squared-backlog.tsv b/Deliverables/sprint-10/imp-squared-backlog.tsv new file mode 100644 index 00000000..a5bd018c --- /dev/null +++ b/Deliverables/sprint-10/imp-squared-backlog.tsv @@ -0,0 +1,33 @@ +Title Assignees Status +Note: All of the Items have a more detailed description pls. click them to see. (this is not an item) Todo +Team: Get the team to get better with show and tell Todo +Team: increase resilance (hard to do) Todo +Team: Assist in creating team processes (Sinatra Doctrine, this is ongoing) In Progress +Team: Completing Task Quicker (earlier PR) In Progress +Tech/Team: Deliverables In Progress +Tech: Streamline testing proces In Progress +Tech: Establish more testing In Progress +Tech: Add a percentage of refactoring items to each sprint In Progress +Tech: Split Backlog Items (PR and Creation) Done +Team: Fixing a PO Dev meeting Done +Agile: Completing all items we set out Done +Team: Rotating the deliverable creation Done +Team: IP contact Done +Tech: Repository coordination Done +Team: Load balancing inside the dev team Done +Team: Making sure all tasks in a sprint are being finished Done +Team collaboration (between dev and po) Done +Gain Independence from the IP Done +Lifting the collective mood Done +Creating a Template for backlog item suggestions by the IP Done +Solving dissatisfaction among the team Done +Focus on IP (and PO relations) Done +Ensuring that everyone has work to do Done +Backlog Item creation procedure Done +Helping Coordinate the team meeting Done +Helping to set up the first IP meeting Done +Supporting team in understanding the amos workflow (e.g importance of the team meeting) Done +Creating Sub-Teams to streamline development Done +Establishing a continuouse release cycle with the team Done +Improving time management in the Team meeting Done +Ensuring a continuous backlog items flow Done \ No newline at end of file diff --git a/Deliverables/sprint-10/planning-documents.pdf b/Deliverables/sprint-10/planning-documents.pdf new file mode 100644 index 00000000..aa6d06ba Binary files /dev/null and b/Deliverables/sprint-10/planning-documents.pdf differ diff --git a/frontend/README.md b/frontend/README.md index c73cda63..27e1839d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -26,4 +26,14 @@ To automatically update all dependencies, run ### Format and check formatting Format `./gradlew combinedFormat`
-Check: `./gradlew ktfmtCheck` \ No newline at end of file +Check: `./gradlew ktfmtCheck` + +## Troubleshooting +### The frontend crashes +Make sure the backend is running or that you are running a mocked version. +If you are running a release, check for MethodNotFoundException etc., these errors are most likely +caused by R8/ProGuard removing used classes. +Quickfix: Use the debug build type. + +### The backend and frontend crashes +Delete the local configuration to make sure it does not contain outdated entries. \ No newline at end of file diff --git a/frontend/app/build.gradle.kts b/frontend/app/build.gradle.kts index 9cf999ae..beb47521 100644 --- a/frontend/app/build.gradle.kts +++ b/frontend/app/build.gradle.kts @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.compose.compiler) alias(libs.plugins.org.cyclonedx.bom) + alias(libs.plugins.detekt) } android { @@ -44,7 +45,7 @@ android { buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -92,6 +93,11 @@ dependencies { debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) implementation(libs.accompanist.drawablepainter) + implementation(libs.flowredux.jvm) + implementation(libs.flowredux.compose) + + implementation(libs.arrow.core) + implementation(libs.arrow.fx.coroutines) implementation(project(":client")) @@ -105,6 +111,9 @@ dependencies { implementation(libs.vico.compose.m2) implementation(libs.vico.compose.m3) implementation(libs.vico.core) + implementation(libs.arrow.core) + implementation(libs.arrow.fx.coroutines) + detektPlugins(libs.detekt.compose.rules) } tasks.cyclonedxBom { @@ -118,3 +127,15 @@ tasks.cyclonedxBom { setIncludeLicenseText(true) setIncludeMetadataResolution(true) } + +detekt { + config = files("detekt.yml") + buildUponDefaultConfig = true + parallel = true + ignoreFailures = true +} + +tasks.detekt { + reports.xml.required.set(true) + reports.html.required.set(true) +} diff --git a/frontend/app/detekt.yml b/frontend/app/detekt.yml new file mode 100644 index 00000000..8731a91c --- /dev/null +++ b/frontend/app/detekt.yml @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2024 Luca Bretting +# +# SPDX-License-Identifier: MIT + +naming: + FunctionNaming: + active: true + ignoreAnnotated: + - Composable + +TwitterCompose: + CompositionLocalAllowlist: + active: true + # You can optionally define a list of CompositionLocals that are allowed here + # allowedCompositionLocals: LocalSomething,LocalSomethingElse + CompositionLocalNaming: + active: true + ContentEmitterReturningValues: + active: true + # You can optionally add your own composables here + # contentEmitters: MyComposable,MyOtherComposable + ModifierComposable: + active: true + ModifierMissing: + active: true + ModifierReused: + active: true + ModifierWithoutDefault: + active: true + MultipleEmitters: + active: true + # You can optionally add your own composables here + # contentEmitters: MyComposable,MyOtherComposable + MutableParams: + active: true + ComposableNaming: + active: true + # You can optionally disable the checks in this rule for regex matches against the composable name (e.g. molecule presenters) + # allowedComposableFunctionNames: .*Presenter,.*MoleculePresenter + ComposableParamOrder: + active: true + PreviewNaming: + active: true + PreviewPublic: + active: true + # You can optionally disable that only previews with @PreviewParameter are flagged + # previewPublicOnlyIfParams: false + RememberMissing: + active: true + UnstableCollections: + active: true + ViewModelForwarding: + active: true + ViewModelInjection: + active: true \ No newline at end of file diff --git a/frontend/app/proguard-rules.pro b/frontend/app/proguard-rules.pro index 8ab8d872..143742a1 100644 --- a/frontend/app/proguard-rules.pro +++ b/frontend/app/proguard-rules.pro @@ -22,4 +22,7 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile +-keep class com.sun.jna.** { *; } +-dontwarn java.awt.** +-keep class uniffi.** { *; } \ No newline at end of file diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ZiofaApplication.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ZiofaApplication.kt index d8430ac9..b5c49135 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ZiofaApplication.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ZiofaApplication.kt @@ -7,8 +7,7 @@ package de.amosproj3.ziofa import android.app.Application import android.content.Context import android.content.pm.PackageManager -import de.amosproj3.ziofa.api.configuration.BackendConfigurationAccess -import de.amosproj3.ziofa.api.configuration.LocalConfigurationAccess +import de.amosproj3.ziofa.api.configuration.ConfigurationAccess import de.amosproj3.ziofa.api.configuration.SymbolsAccess import de.amosproj3.ziofa.api.events.DataStreamProvider import de.amosproj3.ziofa.api.processes.RunningComponentsAccess @@ -31,7 +30,6 @@ import org.koin.core.context.startKoin import org.koin.core.module.Module import org.koin.core.module.dsl.viewModel import org.koin.core.parameter.parametersOf -import org.koin.dsl.binds import org.koin.dsl.module import timber.log.Timber @@ -64,27 +62,22 @@ class ZiofaApplication : Application() { single { RunningComponentsProvider(clientFactory = get(), packageInformationProvider = get()) } - single { ConfigurationManager(clientFactory = get()) } binds - arrayOf(BackendConfigurationAccess::class, LocalConfigurationAccess::class) + single { ConfigurationManager(clientFactory = get()) } factory { (scope: CoroutineScope) -> DataStreamManager(get(), scope) } single { UProbeManager(get()) } } private fun Module.createViewModelFactories() { viewModel { (pids: List) -> - ConfigurationViewModel( - backendConfigurationAccess = get(), - localConfigurationAccess = get(), - pids = pids, - ) + ConfigurationViewModel(configurationAccess = get(), pids = pids) } viewModel { ResetViewModel(get()) } viewModel { ProcessesViewModel(runningComponentsProvider = get()) } viewModel { VisualizationViewModel( - backendConfigurationAccess = get(), - dataStreamProviderFactory = { get { parametersOf(it) } }, + configurationAccess = get(), runningComponentsAccess = get(), + dataStreamProviderFactory = { get { parametersOf(it) } }, ) } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/BackendConfigurationAccess.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/BackendConfigurationAccess.kt deleted file mode 100644 index 8d168abb..00000000 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/BackendConfigurationAccess.kt +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Luca Bretting -// -// SPDX-License-Identifier: MIT - -package de.amosproj3.ziofa.api.configuration - -import kotlinx.coroutines.flow.StateFlow - -interface BackendConfigurationAccess { - - /** Only emits updates from the backend that are actually confirmed to be active */ - val backendConfiguration: StateFlow - - /** Clear the backend configuration. */ - fun reset() -} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationAccess.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationAccess.kt new file mode 100644 index 00000000..444476a8 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationAccess.kt @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.api.configuration + +import kotlinx.coroutines.flow.StateFlow + +interface ConfigurationAccess { + val configurationState: StateFlow + + suspend fun performAction(action: ConfigurationAction) +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationAction.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationAction.kt new file mode 100644 index 00000000..646ec6f5 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationAction.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.api.configuration + +import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions + +/** Possible interaction with the configuration */ +sealed class ConfigurationAction { + /** Overwrite the backend configuration with the in-memory configuration */ + data object Synchronize : ConfigurationAction() + + /** [enable] or disable a [backendFeature] for given [pids]. */ + data class ChangeFeature( + val backendFeature: BackendFeatureOptions, + val enable: Boolean, + val pids: Set, + ) : ConfigurationAction() + + /** Reset the backend configuration to an empty configuration. */ + data object Reset : ConfigurationAction() +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationState.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationState.kt new file mode 100644 index 00000000..2fbf4d7f --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationState.kt @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2025 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.api.configuration + +import de.amosproj3.ziofa.client.Client +import de.amosproj3.ziofa.client.ClientFactory +import de.amosproj3.ziofa.client.Configuration + +/** State of the configuration */ +sealed class ConfigurationState { + /** Initial state */ + data class Uninitialized(val clientFactory: ClientFactory) : ConfigurationState() + + /** In-memory configuration and remote configuration are equivalent */ + data class Synchronized(val client: Client, val configuration: Configuration) : + ConfigurationState() + + /** + * In-memory configuration and remote configuration differ (i.e. there are unsynchronized local + * changes) + */ + data class Different( + val client: Client, + val localConfiguration: Configuration, + val backendConfiguration: Configuration, + ) : ConfigurationState() + + /** An error has occured during communication with the backend */ + data class Error(val error: Throwable) : ConfigurationState() +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationUpdate.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationUpdate.kt deleted file mode 100644 index 65e1a939..00000000 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/ConfigurationUpdate.kt +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Felix Hilgers -// SPDX-FileCopyrightText: 2024 Luca Bretting -// -// SPDX-License-Identifier: MIT - -package de.amosproj3.ziofa.api.configuration - -import de.amosproj3.ziofa.client.Configuration - -sealed class ConfigurationUpdate { - data class Valid(val configuration: Configuration) : ConfigurationUpdate() - - data class Invalid(val error: Throwable) : ConfigurationUpdate() - - data object Unknown : ConfigurationUpdate() -} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/LocalConfigurationAccess.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/LocalConfigurationAccess.kt deleted file mode 100644 index b167e98d..00000000 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/api/configuration/LocalConfigurationAccess.kt +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Luca Bretting -// SPDX-FileCopyrightText: 2024 Robin Seidl -// -// SPDX-License-Identifier: MIT - -package de.amosproj3.ziofa.api.configuration - -import de.amosproj3.ziofa.client.JniReferencesConfig -import de.amosproj3.ziofa.client.SysSendmsgConfig -import de.amosproj3.ziofa.client.UprobeConfig -import de.amosproj3.ziofa.client.VfsWriteConfig -import kotlinx.coroutines.flow.Flow - -interface LocalConfigurationAccess { - - /** - * Emits updates both unconfirmed changes and confirmed changes (these override the unconfirmed) - */ - val localConfiguration: Flow - - /** - * Change the local configuration of a feature. If the feature is PID dependent, this function - * will enable or disable it ==> for the respective PIDs <== depending on [enable]. - * - * @param enable Whether to enable or disable the feature. - * @param vfsWriteFeature A [VfsWriteConfig] update to apply or null if this should not be - * changed. - * @param sendMessageFeature A [SysSendmsgConfig] update to apply or null if this should not be - * changed. - * @param uprobesFeature The Uprobe config to apply or null if this should not be changed. - */ - fun changeFeatureConfiguration( - enable: Boolean, - vfsWriteFeature: VfsWriteConfig? = null, - sendMessageFeature: SysSendmsgConfig? = null, - uprobesFeature: List? = listOf(), - jniReferencesFeature: JniReferencesConfig? = null, - ) - - /** Submit the local configuration to the backend. */ - fun submitConfiguration() -} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/events/BackendEvent.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/events/BackendEvent.kt deleted file mode 100644 index a11ba326..00000000 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/api/events/BackendEvent.kt +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Luca Bretting -// -// SPDX-License-Identifier: MIT - -package de.amosproj3.ziofa.api.events - -sealed class BackendEvent( - val fileDescriptor: ULong, - val processId: UInt, - val startTimestamp: ULong, - val durationOrSize: ULong, -) { - - data class VfsWriteEvent( - val fd: ULong, - val pid: UInt, - val size: ULong, - val timestampMillis: ULong, // unix time - ) : BackendEvent(fd, pid, timestampMillis, size) - - data class SendMessageEvent( - val fd: ULong, - val pid: UInt, - val tid: UInt, - val beginTimestamp: ULong, - val durationNanos: ULong, - ) : BackendEvent(fd, pid, beginTimestamp, durationNanos) -} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/events/DataStreamProvider.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/events/DataStreamProvider.kt index 20069bb2..cf465e4e 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/api/events/DataStreamProvider.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/api/events/DataStreamProvider.kt @@ -4,12 +4,16 @@ package de.amosproj3.ziofa.api.events +import de.amosproj3.ziofa.client.Event import kotlinx.coroutines.flow.Flow interface DataStreamProvider { - suspend fun counter(ebpfProgramName: String): Flow - suspend fun vfsWriteEvents(pids: List?): Flow + fun vfsWriteEvents(pids: List?): Flow - suspend fun sendMessageEvents(pids: List?): Flow + fun sendMessageEvents(pids: List?): Flow + + fun jniReferenceEvents(pids: List?): Flow + + fun sigquitEvents(pids: List?): Flow } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/processes/RunningComponent.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/processes/RunningComponent.kt index 0edb9101..eb124e82 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/api/processes/RunningComponent.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/api/processes/RunningComponent.kt @@ -8,8 +8,8 @@ import de.amosproj3.ziofa.client.Process sealed class RunningComponent(val pids: List) { data class StandaloneProcess(val process: Process) : - RunningComponent(pids = listOf(process.pid.toUInt())) + RunningComponent(pids = listOf(process.pid)) data class Application(val packageInfo: InstalledPackageInfo, val processList: List) : - RunningComponent(pids = processList.map { it.pid.toUInt() }) + RunningComponent(pids = processList.map { it.pid }) } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/bl/configuration/ConfigDiffHelpers.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/bl/configuration/ConfigDiffHelpers.kt index a68a5153..65496fe4 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/bl/configuration/ConfigDiffHelpers.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/bl/configuration/ConfigDiffHelpers.kt @@ -4,10 +4,88 @@ package de.amosproj3.ziofa.bl.configuration +import de.amosproj3.ziofa.api.configuration.ConfigurationAction +import de.amosproj3.ziofa.client.Configuration import de.amosproj3.ziofa.client.JniReferencesConfig import de.amosproj3.ziofa.client.SysSendmsgConfig +import de.amosproj3.ziofa.client.SysSigquitConfig import de.amosproj3.ziofa.client.UprobeConfig import de.amosproj3.ziofa.client.VfsWriteConfig +import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions +import de.amosproj3.ziofa.ui.shared.DURATION_THRESHOLD + +@Suppress("CyclomaticComplexMethod", "LongMethod") +fun Configuration.applyChange(action: ConfigurationAction.ChangeFeature): Configuration { + + val feature = action.backendFeature + val enable = action.enable + val pids = action.pids + + return when (feature) { + is BackendFeatureOptions.VfsWriteOption -> + this.copy( + vfsWrite = + this.vfsWrite?.updatePIDs( + pidsToAdd = + if (enable) pids.associateWith { DURATION_THRESHOLD }.entries + else setOf(), + pidsToRemove = + if (!enable) pids.associateWith { DURATION_THRESHOLD }.entries + else setOf(), + ) + ) + + is BackendFeatureOptions.SendMessageOption -> + this.copy( + sysSendmsg = + this.sysSendmsg?.updatePIDs( + pidsToAdd = + if (enable) pids.associateWith { DURATION_THRESHOLD }.entries + else setOf(), + pidsToRemove = + if (!enable) pids.associateWith { DURATION_THRESHOLD }.entries + else setOf(), + ) + ) + + is BackendFeatureOptions.JniReferencesOption -> + this.copy( + jniReferences = + this.jniReferences?.updatePIDs( + pidsToAdd = if (enable) pids else setOf(), + pidsToRemove = if (!enable) pids else setOf(), + ) + ) + + is BackendFeatureOptions.SigquitOption -> + this.copy( + sysSigquit = + this.sysSigquit?.updatePIDs( + pidsToAdd = if (enable) pids else setOf(), + pidsToRemove = if (!enable) pids else setOf(), + ) + ) + + is BackendFeatureOptions.UprobeOption -> { + val uprobeUpdate = + pids.map { + UprobeConfig( + fnName = feature.method, + target = feature.odexFilePath, + offset = feature.offset, + pid = it, + ) + } + this.copy( + uprobes = + this.uprobes.updateUProbes( + pidsToAdd = if (enable) uprobeUpdate else listOf(), + pidsToRemove = if (!enable) uprobeUpdate else listOf(), + ) + ) + } + } +} fun VfsWriteConfig?.updatePIDs( pidsToAdd: Set> = setOf(), @@ -44,9 +122,17 @@ fun List?.updateUProbes( } fun JniReferencesConfig?.updatePIDs( - pidsToAdd: List = listOf(), - pidsToRemove: List = listOf(), + pidsToAdd: Set = setOf(), + pidsToRemove: Set = setOf(), ): JniReferencesConfig { val config = this ?: JniReferencesConfig(listOf()) return config.copy(pids = config.pids.plus(pidsToAdd).minus(pidsToRemove.toSet())) } + +fun SysSigquitConfig?.updatePIDs( + pidsToAdd: Set = setOf(), + pidsToRemove: Set = setOf(), +): SysSigquitConfig { + val config = this ?: SysSigquitConfig(listOf()) + return config.copy(pids = config.pids.plus(pidsToAdd).minus(pidsToRemove)) +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/bl/configuration/ConfigurationManager.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/bl/configuration/ConfigurationManager.kt index d1c173f3..0d6f7d4a 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/bl/configuration/ConfigurationManager.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/bl/configuration/ConfigurationManager.kt @@ -6,174 +6,146 @@ package de.amosproj3.ziofa.bl.configuration -import de.amosproj3.ziofa.api.configuration.BackendConfigurationAccess -import de.amosproj3.ziofa.api.configuration.ConfigurationUpdate -import de.amosproj3.ziofa.api.configuration.LocalConfigurationAccess +import arrow.core.Either +import com.freeletics.flowredux.dsl.ExecutionPolicy +import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import com.freeletics.flowredux.dsl.State +import de.amosproj3.ziofa.api.configuration.ConfigurationAccess +import de.amosproj3.ziofa.api.configuration.ConfigurationAction +import de.amosproj3.ziofa.api.configuration.ConfigurationState import de.amosproj3.ziofa.client.Client import de.amosproj3.ziofa.client.ClientFactory import de.amosproj3.ziofa.client.Configuration -import de.amosproj3.ziofa.client.JniReferencesConfig -import de.amosproj3.ziofa.client.SysSendmsgConfig -import de.amosproj3.ziofa.client.UprobeConfig -import de.amosproj3.ziofa.client.VfsWriteConfig +import de.amosproj3.ziofa.ui.configuration.utils.EMPTY_CONFIGURATION import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import timber.log.Timber -class ConfigurationManager(val clientFactory: ClientFactory) : - BackendConfigurationAccess, LocalConfigurationAccess { - - private val coroutineScope = CoroutineScope(Dispatchers.IO) - private var client: Client? = null - - override val backendConfiguration: MutableStateFlow = - MutableStateFlow(ConfigurationUpdate.Unknown) - - private val _localConfiguration = MutableStateFlow(null) - - override val localConfiguration = - _localConfiguration - .onEach { Timber.i("local configuration updated $it") } - .map { it ?: ConfigurationUpdate.Unknown } +/** + * This class is organized as a state machine with 4 states in [ConfigurationState]. Starting in + * state [ConfigurationState.Uninitialized], once the configuration is initially retrieved from the + * backend, we transition to [ConfigurationState.Synchronized]. If the user makes a change, the + * state will change to [ConfigurationState.Different] until the user submits his configuration, + * which will change it back to [ConfigurationState.Synchronized]. Any errors in communication with + * the backend will transition to [ConfigurationState.Error] from any state. + * + * @param clientFactory the client factory for backend communication + */ +@OptIn(ExperimentalCoroutinesApi::class) +class ConfigurationManager(clientFactory: ClientFactory) : + FlowReduxStateMachine( + initialState = ConfigurationState.Uninitialized(clientFactory = clientFactory) + ), + ConfigurationAccess { + + private val _configurationState = + MutableStateFlow(ConfigurationState.Uninitialized(clientFactory)) + + override val configurationState = _configurationState.asStateFlow() + + override suspend fun performAction(action: ConfigurationAction) { + this.dispatch(action) + } init { - coroutineScope.launch { - try { - client = clientFactory.connect() - initializeConfigurations() - } catch (e: Exception) { - backendConfiguration.update { ConfigurationUpdate.Invalid(e) } - } - } + startStateMachine() + startUpdatingConfigurationState() } - override fun changeFeatureConfiguration( - enable: Boolean, - vfsWriteFeature: VfsWriteConfig?, - sendMessageFeature: SysSendmsgConfig?, - uprobesFeature: List?, - jniReferencesFeature: JniReferencesConfig?, - ) { - _localConfiguration.update { prev -> - Timber.e("changeFeatureConfigurationForPIDs.prev $prev") - Timber.e( - "changeFeatureConfigurationForPIDs() $vfsWriteFeature, $sendMessageFeature, $uprobesFeature, $jniReferencesFeature" - ) - // the configuration shall not be changed from the UI if there is none received from - // backend - if (prev != null && prev is ConfigurationUpdate.Valid) { - val previousConfiguration = prev.configuration - previousConfiguration - .copy( - vfsWrite = - vfsWriteFeature?.let { requestedChanges -> - previousConfiguration.vfsWrite.updatePIDs( - pidsToAdd = - if (enable) requestedChanges.entries.entries else setOf(), - pidsToRemove = - if (!enable) requestedChanges.entries.entries else setOf(), - ) - } ?: previousConfiguration.vfsWrite, - sysSendmsg = - sendMessageFeature?.let { requestedChanges -> - previousConfiguration.sysSendmsg.updatePIDs( - pidsToAdd = - if (enable) requestedChanges.entries.entries else setOf(), - pidsToRemove = - if (!enable) requestedChanges.entries.entries else setOf(), - ) - } ?: previousConfiguration.sysSendmsg, - uprobes = - uprobesFeature.let { requestedChanges -> - if (requestedChanges == null) - return@let previousConfiguration.uprobes - previousConfiguration.uprobes.updateUProbes( - pidsToAdd = if (enable) requestedChanges else listOf(), - pidsToRemove = if (!enable) requestedChanges else listOf(), - ) - }, - jniReferences = - jniReferencesFeature?.let { requestedChanges -> - previousConfiguration.jniReferences.updatePIDs( - pidsToAdd = if (enable) requestedChanges.pids else listOf(), - pidsToRemove = if (!enable) requestedChanges.pids else listOf(), - ) - } ?: previousConfiguration.jniReferences, - ) - .also { Timber.i("new local configuration = $it") } - .let { ConfigurationUpdate.Valid(it) } - } else return@update prev - } - } + private fun startStateMachine() { + spec { + inState { + onEnter { state -> + val client = state.snapshot.clientFactory.connect() + val configuration = client.initializeConfiguration() + state.override { configuration.synchronizedOrErrorState(client) } + } + } - override fun submitConfiguration() { - coroutineScope.launch { - sendLocalToBackend() - updateBothConfigurations( - getFromBackend() - ) // "emulates" callback of changed configuration until + inState { + on(executionPolicy = ExecutionPolicy.ORDERED) { + action, + state -> + state.applyChangeAndTransitionToDifferent(action) + } + + on { _, state -> + val updatedConfig = + state.snapshot.client.updateBackendConfiguration(EMPTY_CONFIGURATION) + state.override { updatedConfig.synchronizedOrErrorState(client) } + } + } + inState { + on { _, state -> + val currentState = state.snapshot + val updatedConfig = + currentState.client.updateBackendConfiguration( + currentState.localConfiguration + ) + state.override { updatedConfig.synchronizedOrErrorState(client) } + } + + on { action, state -> + state.override { + val newLocalConfig = this.localConfiguration.applyChange(action) + if (newLocalConfig == this.backendConfiguration) + ConfigurationState.Synchronized( + client = this.client, + configuration = this.backendConfiguration, + ) + else this.copy(localConfiguration = newLocalConfig) + } + } + + on { _, state -> + val updatedConfig = + state.snapshot.client.updateBackendConfiguration(EMPTY_CONFIGURATION) + state.override { updatedConfig.synchronizedOrErrorState(client) } + } + } } } - override fun reset() { - runBlocking { - client?.setConfiguration(Configuration(null, null, listOf(), null)) - updateBothConfigurations(getFromBackend()) + /** + * Start updating the internal [MutableStateFlow] based on the current state of the state + * machine. + */ + private fun startUpdatingConfigurationState() { + CoroutineScope(Dispatchers.IO).launch { + this@ConfigurationManager.state.collect { _configurationState.value = it } } } - private suspend fun initializeConfigurations() { - val initializedConfiguration = - try { - ConfigurationUpdate.Valid(client!!.getConfiguration()) - } catch (e: Exception) { - getOrCreateInitialConfiguration() - } - updateBothConfigurations(initializedConfiguration) - } - - // TODO this should be handled on the backend - private suspend fun getOrCreateInitialConfiguration(): ConfigurationUpdate { - return try { - // the config may not be initialized, we should try initializing it - client!!.setConfiguration( - Configuration( - vfsWrite = null, - sysSendmsg = null, - uprobes = listOf(), - jniReferences = null, - ) + fun State.applyChangeAndTransitionToDifferent( + action: ConfigurationAction.ChangeFeature + ) = + this.override { + ConfigurationState.Different( + client = this.client, + localConfiguration = this.configuration.applyChange(action), + backendConfiguration = this.configuration, ) - ConfigurationUpdate.Valid(client!!.getConfiguration()) - } catch (e: Exception) { - return ConfigurationUpdate.Invalid(e) } - } - private suspend fun sendLocalToBackend() { - _localConfiguration.value?.let { - if (it is ConfigurationUpdate.Valid) client?.setConfiguration(it.configuration) - } ?: Timber.e("unsubmittedConfiguration == null -> this should never happen") - } + private fun Either.synchronizedOrErrorState(client: Client) = + this.fold( + ifLeft = { ConfigurationState.Error(it) }, + ifRight = { ConfigurationState.Synchronized(client, it) }, + ) - private suspend fun getFromBackend(): ConfigurationUpdate { - return try { - (client?.getConfiguration()?.let { ConfigurationUpdate.Valid(it) } - ?: ConfigurationUpdate.Unknown) - .also { Timber.i("Received config $it") } - } catch (e: Exception) { - ConfigurationUpdate.Invalid(e) + private suspend fun Client.updateBackendConfiguration(configuration: Configuration) = + Either.catch { + this.setConfiguration(configuration) + this.getConfiguration() } - } - private fun updateBothConfigurations(configurationUpdate: ConfigurationUpdate) { - backendConfiguration.value = configurationUpdate - _localConfiguration.value = configurationUpdate - } + private suspend fun Client.initializeConfiguration() = + try { + Either.Right(this.getConfiguration()) + } catch (e: Exception) { + this.updateBackendConfiguration(EMPTY_CONFIGURATION) + } } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/bl/events/DataStreamManager.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/bl/events/DataStreamManager.kt index c0483ba6..e404a21e 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/bl/events/DataStreamManager.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/bl/events/DataStreamManager.kt @@ -5,7 +5,6 @@ package de.amosproj3.ziofa.bl.events -import de.amosproj3.ziofa.api.events.BackendEvent import de.amosproj3.ziofa.api.events.DataStreamProvider import de.amosproj3.ziofa.client.ClientFactory import de.amosproj3.ziofa.client.Event @@ -14,10 +13,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.shareIn -import timber.log.Timber class DataStreamManager(private val clientFactory: ClientFactory, coroutineScope: CoroutineScope) : DataStreamProvider { @@ -26,48 +23,25 @@ class DataStreamManager(private val clientFactory: ClientFactory, coroutineScope flow { clientFactory.connect().initStream().collect { emit(it) } } .shareIn(coroutineScope, SharingStarted.Lazily) - override suspend fun counter(ebpfProgramName: String): Flow { - return clientFactory - .connect() - .also { - try { - it.load() - // default wifi interface on android, now configurable - it.attach("wlan0") - it.startCollecting() - } catch (e: Exception) { - Timber.e(e.stackTraceToString()) - } - } - .serverCount() - } - - override suspend fun vfsWriteEvents(pids: List?): Flow = + override fun vfsWriteEvents(pids: List?): Flow = dataFlow .mapNotNull { it as? Event.VfsWrite } .filter { it.pid.isGlobalRequestedOrPidConfigured(pids) } - .map { - BackendEvent.VfsWriteEvent( - fd = it.fp, - pid = it.pid, - size = it.bytesWritten, - timestampMillis = it.beginTimeStamp, - ) - } - override suspend fun sendMessageEvents(pids: List?): Flow = + override fun sendMessageEvents(pids: List?): Flow = dataFlow .mapNotNull { it as? Event.SysSendmsg } .filter { it.pid.isGlobalRequestedOrPidConfigured(pids) } - .map { - BackendEvent.SendMessageEvent( - fd = it.fd, - pid = it.pid, - tid = it.tid, - beginTimestamp = it.beginTimeStamp, - durationNanos = it.durationNanoSecs, - ) - } + + override fun jniReferenceEvents(pids: List?): Flow = + dataFlow + .mapNotNull { it as? Event.JniReferences } + .filter { it.pid.isGlobalRequestedOrPidConfigured(pids) } + + override fun sigquitEvents(pids: List?): Flow = + dataFlow + .mapNotNull { it as? Event.SysSigquit } + .filter { it.pid.isGlobalRequestedOrPidConfigured(pids) } private fun UInt.isGlobalRequestedOrPidConfigured(pids: List?) = pids?.contains(this) ?: true diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/ZiofaApp.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/ZiofaApp.kt index 4cb76681..b3d55d10 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/ZiofaApp.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/ZiofaApp.kt @@ -34,7 +34,19 @@ import de.amosproj3.ziofa.ui.visualization.VisualizationScreen val GLOBAL_CONFIGURATION_ROUTE = "${Routes.IndividualConfiguration.name}?displayName=${Uri.encode("all processes")}?pids=-1" +val PIDS_ARG = + navArgument("pids") { + type = NavType.StringType + nullable = true + } +val DISPLAY_NAME_ARG = + navArgument("displayName") { + type = NavType.StringType + nullable = true + } + /** Main application composable. All calls to [NavController] should happen here. */ +@Suppress("ModifierMissing, LongMethod") // Top level composable @Composable fun ZIOFAApp() { val navController = rememberNavController() @@ -56,16 +68,16 @@ fun ZIOFAApp() { } screenWithDefaultAnimations(Routes.Reset.name) { ResetScreen( - Modifier.padding(innerPadding), afterResetConfirmed = { navController.popBackStack() }, + modifier = Modifier.padding(innerPadding), ) } screenWithDefaultAnimations(Routes.Configuration.name) { ConfigurationMenu( - Modifier.padding(innerPadding), toProcesses = { navController.navigate(Routes.Processes.name) }, toGlobalConfiguration = { navController.navigate(GLOBAL_CONFIGURATION_ROUTE) }, toReset = { navController.navigate(Routes.Reset.name) }, + modifier = Modifier.padding(innerPadding), ) } screenWithDefaultAnimations(Routes.Visualize.name) { @@ -77,28 +89,17 @@ fun ZIOFAApp() { screenWithDefaultAnimations(Routes.Processes.name) { ProcessesScreen( Modifier.padding(innerPadding), - onClickEdit = { - navController.navigate(it.toConfigurationScreenRouteForComponent()) + onClickEdit = { component -> + navController.navigate(component.toConfigurationScreenRouteForComponent()) }, ) } parameterizedScreen( "${Routes.IndividualConfiguration.name}?displayName={displayName}?pids={pids}", - arguments = - listOf( - navArgument("displayName") { - type = NavType.StringType - nullable = true - }, - navArgument("pids") { - type = NavType.StringType - nullable = true - }, - ), + arguments = listOf(DISPLAY_NAME_ARG, PIDS_ARG), ) { ConfigurationScreen( Modifier.padding(innerPadding), - onBack = { navController.popBackStack() }, pids = it.arguments?.getString("pids")?.deserializePIDs()?.validPIDsOrNull(), onAddUprobeSelected = { navController.navigate(it.arguments.copyToSymbolsRoute()) @@ -108,24 +109,17 @@ fun ZIOFAApp() { parameterizedScreen( "${Routes.Symbols.name}?displayName={displayName}?pids={pids}", - arguments = - listOf( - navArgument("displayName") { - type = NavType.StringType - nullable = true - }, - navArgument("pids") { - type = NavType.StringType - nullable = true - }, - ), + arguments = listOf(DISPLAY_NAME_ARG, PIDS_ARG), ) { SymbolsScreen( modifier = Modifier.padding(innerPadding), onSymbolsSubmitted = { navController.popBackStack() }, pids = - it.arguments?.getString("pids")?.deserializePIDs()?.validPIDsOrNull() - ?: listOf(), + it.arguments + ?.getString("pids") + ?.deserializePIDs() + ?.validPIDsOrNull() + .orEmpty(), ) } } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/about/AboutScreen.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/about/AboutScreen.kt index 08719fcf..3b6fb14b 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/about/AboutScreen.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/about/AboutScreen.kt @@ -12,8 +12,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp const val PRODUCT_MISSION = - "ZIOFA (Zero Instrumentation Observability for Android) aims to implement observability use cases relevant to performance specified by our industry partner using eBPF. Examples include tracing long-running blocking calls, leaking JNI indirect references or signals like SIGKILL sent to processes, all without instrumenting the observed application itself.\n" + - "The eBPF programs are loaded and unloaded using a backend daemon running as root that will collect metrics and send them to a client. For displaying these metrics to the user, we are implementing an on-device UI that can display visualizations for these use cases and allow for configuration of the enabled use cases, but using a decoupled Client SDK so that future work may easily make the data accessible the external processing." + "ZIOFA (Zero Instrumentation Observability for Android) aims to implement observability use " + + "cases relevant to performance specified by our industry partner using eBPF. " + + "Examples include tracing long-running blocking calls, leaking JNI indirect" + + "references or signals like SIGKILL sent to processes, all without instrumenting" + + " the observed application itself.\n" + + "The eBPF programs are loaded and unloaded using a backend daemon running as root that " + + "will collect metrics and send them to a client. For displaying these metrics to " + + "the user, we are implementing an on-device UI that can display visualizations for" + + " these use cases and allow for configuration of the enabled use cases, but using a " + + "decoupled Client SDK so that future work may easily make the data accessible the " + + "external processing." /** * Screen containing information about the project. Might delete later if we need space for another diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/ConfigurationScreen.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/ConfigurationScreen.kt index 1915d908..a482cb37 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/ConfigurationScreen.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/ConfigurationScreen.kt @@ -27,6 +27,7 @@ import de.amosproj3.ziofa.ui.configuration.composables.SectionTitleRow import de.amosproj3.ziofa.ui.configuration.composables.SubmitFab import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions import de.amosproj3.ziofa.ui.configuration.data.ConfigurationScreenState +import de.amosproj3.ziofa.ui.configuration.data.FeatureType import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -35,7 +36,6 @@ import org.koin.core.parameter.parametersOf @Composable fun ConfigurationScreen( modifier: Modifier = Modifier, - onBack: () -> Unit = {}, onAddUprobeSelected: () -> Unit = {}, pids: List? = listOf(), ) { @@ -50,21 +50,26 @@ fun ConfigurationScreen( Column(Modifier.fillMaxWidth()) { // Render list of options - SectionTitleRow("IO Observability Features") + SectionTitleRow(FeatureType.IO.displayName) EbpfIOFeatureOptions( - options = - state.options.filter { it !is BackendFeatureOptions.UprobeOption }, + options = state.options.filter { it.featureType == FeatureType.IO }, + onOptionChanged = { option, newState -> + viewModel.optionChanged(option, newState) + }, + ) + + SectionTitleRow(FeatureType.SIGNALS.displayName) + EbpfIOFeatureOptions( + options = state.options.filter { it.featureType == FeatureType.SIGNALS }, onOptionChanged = { option, newState -> viewModel.optionChanged(option, newState) }, ) - SectionTitleRow("Uprobes") + SectionTitleRow(FeatureType.UPROBES.displayName) EbpfUprobeFeatureOptions( options = - state.options.mapNotNull { - if (it is BackendFeatureOptions.UprobeOption) it else null - }, + state.options.mapNotNull { it as? BackendFeatureOptions.UprobeOption }, onOptionDeleted = { option -> viewModel.optionChanged(option, active = false) }, diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/ConfigurationViewModel.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/ConfigurationViewModel.kt index 44f9308f..0534907a 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/ConfigurationViewModel.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/ConfigurationViewModel.kt @@ -7,15 +7,13 @@ package de.amosproj3.ziofa.ui.configuration import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import de.amosproj3.ziofa.api.configuration.BackendConfigurationAccess -import de.amosproj3.ziofa.api.configuration.ConfigurationUpdate -import de.amosproj3.ziofa.api.configuration.LocalConfigurationAccess +import de.amosproj3.ziofa.api.configuration.ConfigurationAccess +import de.amosproj3.ziofa.api.configuration.ConfigurationAction +import de.amosproj3.ziofa.api.configuration.ConfigurationState import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions import de.amosproj3.ziofa.ui.configuration.data.ConfigurationScreenState -import de.amosproj3.ziofa.ui.shared.setInLocalConfiguration import de.amosproj3.ziofa.ui.shared.toUIOptionsForPids import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -23,52 +21,53 @@ import kotlinx.coroutines.launch import timber.log.Timber class ConfigurationViewModel( - private val localConfigurationAccess: LocalConfigurationAccess, - private val backendConfigurationAccess: BackendConfigurationAccess, + private val configurationAccess: ConfigurationAccess, private val pids: List, ) : ViewModel() { val configurationScreenState = - localConfigurationAccess.localConfiguration + configurationAccess.configurationState .onEach { Timber.i("Update from UI: $it") } .map { it.toConfigurationScreenState(pids) } .stateIn(viewModelScope, SharingStarted.Eagerly, ConfigurationScreenState.Loading) val changed = - combine( - localConfigurationAccess.localConfiguration, - backendConfigurationAccess.backendConfiguration, - ) { local, backend -> - local != backend - } + configurationAccess.configurationState + .map { it is ConfigurationState.Different } .stateIn(viewModelScope, SharingStarted.Lazily, false) fun optionChanged(option: BackendFeatureOptions, active: Boolean) { if (configurationScreenState.value is ConfigurationScreenState.Valid) { - option.setInLocalConfiguration( - localConfigurationAccess = localConfigurationAccess, - pids = pids.toSet(), - active = active, - ) + val change = + ConfigurationAction.ChangeFeature( + backendFeature = option, + enable = active, + pids = pids.toSet(), + ) + viewModelScope.launch { configurationAccess.performAction(change) } } } /** Submit the configuration changes to the backend. */ fun configurationSubmitted() { - viewModelScope.launch { localConfigurationAccess.submitConfiguration() } + viewModelScope.launch { configurationAccess.performAction(ConfigurationAction.Synchronize) } } - private fun ConfigurationUpdate.toConfigurationScreenState( + private fun ConfigurationState.toConfigurationScreenState( relevantPids: List ): ConfigurationScreenState = when (this) { - is ConfigurationUpdate.Valid -> { - ConfigurationScreenState.Valid(this.toUIOptionsForPids(relevantPids)) - } + is ConfigurationState.Synchronized -> + ConfigurationScreenState.Valid(this.configuration.toUIOptionsForPids(relevantPids)) - is ConfigurationUpdate.Invalid -> + is ConfigurationState.Different -> + ConfigurationScreenState.Valid( + this.localConfiguration.toUIOptionsForPids(relevantPids) + ) + + is ConfigurationState.Error -> ConfigurationScreenState.Invalid(this.error.stackTraceToString()) - is ConfigurationUpdate.Unknown -> ConfigurationScreenState.Loading + is ConfigurationState.Uninitialized -> ConfigurationScreenState.Loading } } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/EbpfIOFeatureOptions.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/EbpfIOFeatureOptions.kt index ae6171bc..2186e8dc 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/EbpfIOFeatureOptions.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/EbpfIOFeatureOptions.kt @@ -23,8 +23,9 @@ import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions fun EbpfIOFeatureOptions( options: List, onOptionChanged: (BackendFeatureOptions, Boolean) -> Unit, + modifier: Modifier = Modifier, ) { - LazyColumn(modifier = Modifier.padding(horizontal = 20.dp, vertical = 15.dp).fillMaxWidth()) { + LazyColumn(modifier = modifier.padding(horizontal = 20.dp, vertical = 15.dp).fillMaxWidth()) { items(options) { option -> Row( modifier = Modifier.fillMaxWidth(), diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/EbpfUprobeFeatureOptions.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/EbpfUprobeFeatureOptions.kt index 95abf808..685eb01a 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/EbpfUprobeFeatureOptions.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/EbpfUprobeFeatureOptions.kt @@ -28,8 +28,9 @@ fun EbpfUprobeFeatureOptions( options: List, onOptionDeleted: (BackendFeatureOptions.UprobeOption) -> Unit, onAddUprobeSelected: () -> Unit, + modifier: Modifier = Modifier, ) { - LazyColumn(modifier = Modifier.padding(horizontal = 20.dp, vertical = 15.dp).fillMaxSize()) { + LazyColumn(modifier = modifier.padding(horizontal = 20.dp, vertical = 15.dp).fillMaxSize()) { items(options) { option -> Row( modifier = Modifier.fillMaxWidth(), diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/ErrorScreen.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/ErrorScreen.kt index fd9ce2f9..c1044d6d 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/ErrorScreen.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/ErrorScreen.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import kotlin.system.exitProcess +const val TITLE_TEXT_SIZE = 25f + @Preview(device = Devices.AUTOMOTIVE_1024p) @Composable fun ErrorScreen(error: String = "No error message available") { @@ -36,7 +38,7 @@ fun ErrorScreen(error: String = "No error message available") { Text( text = "Error while communicating with backend", color = Color.Red, - fontSize = TextUnit(25f, TextUnitType.Sp), + fontSize = TextUnit(TITLE_TEXT_SIZE, TextUnitType.Sp), ) Text(text = error) } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/SectionTitleRow.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/SectionTitleRow.kt index 5ba4279c..b65d25fb 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/SectionTitleRow.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/composables/SectionTitleRow.kt @@ -15,8 +15,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @Composable -fun SectionTitleRow(title: String) { - Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.padding(bottom = 10.dp)) { +fun SectionTitleRow(title: String, modifier: Modifier = Modifier) { + Row(horizontalArrangement = Arrangement.Center, modifier = modifier.padding(bottom = 10.dp)) { Text(title, fontWeight = FontWeight.Bold) } HorizontalDivider(thickness = 3.dp) diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/data/EBpfProgramOption.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/data/BackendFeatureOptions.kt similarity index 50% rename from frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/data/EBpfProgramOption.kt rename to frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/data/BackendFeatureOptions.kt index f014f205..311a5ca4 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/data/EBpfProgramOption.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/data/BackendFeatureOptions.kt @@ -5,15 +5,28 @@ package de.amosproj3.ziofa.ui.configuration.data -sealed class BackendFeatureOptions(val featureName: String, val active: Boolean) { +enum class FeatureType(val displayName: String) { + IO("IO Observability Features"), + SIGNALS("Linux Signals"), + UPROBES("Uprobes"), +} + +sealed class BackendFeatureOptions( + val featureName: String, + val featureType: FeatureType, + val active: Boolean, +) { data class VfsWriteOption(val enabled: Boolean, val pids: Set) : - BackendFeatureOptions("VFS Write Analysis", enabled) + BackendFeatureOptions("VFS Write Analysis", FeatureType.IO, enabled) data class SendMessageOption(val enabled: Boolean, val pids: Set) : - BackendFeatureOptions("Unix Domain Socket Analysis", enabled) + BackendFeatureOptions("Unix Domain Socket Analysis", FeatureType.IO, enabled) data class JniReferencesOption(val enabled: Boolean, val pids: Set) : - BackendFeatureOptions("Local & Global Indirect JNI References", enabled) + BackendFeatureOptions("Local & Global Indirect JNI References", FeatureType.IO, enabled) + + data class SigquitOption(val enabled: Boolean, val pids: Set) : + BackendFeatureOptions("SIGQUIT", FeatureType.SIGNALS, enabled) data class UprobeOption( val method: String, @@ -21,5 +34,5 @@ sealed class BackendFeatureOptions(val featureName: String, val active: Boolean) val pids: Set, val offset: ULong, val odexFilePath: String, - ) : BackendFeatureOptions(method, enabled) + ) : BackendFeatureOptions(method, FeatureType.UPROBES, enabled) } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/utils/Constants.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/utils/Constants.kt index 6988c389..71928156 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/utils/Constants.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/configuration/utils/Constants.kt @@ -3,3 +3,7 @@ // SPDX-License-Identifier: MIT package de.amosproj3.ziofa.ui.configuration.utils + +import de.amosproj3.ziofa.client.Configuration + +val EMPTY_CONFIGURATION = Configuration(null, null, listOf(), null, null) diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/ConfigurationMenu.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/ConfigurationMenu.kt index 4f1b8fd9..99158f15 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/ConfigurationMenu.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/ConfigurationMenu.kt @@ -14,16 +14,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import de.amosproj3.ziofa.ui.navigation.composables.MenuOptionData import de.amosproj3.ziofa.ui.navigation.composables.MenuOptions import de.amosproj3.ziofa.ui.navigation.data.Emoji +import de.amosproj3.ziofa.ui.navigation.data.MenuOptionData @Composable fun ConfigurationMenu( - modifier: Modifier = Modifier, toProcesses: () -> Unit, toGlobalConfiguration: () -> Unit, toReset: () -> Unit, + modifier: Modifier = Modifier, ) { Box( diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/HomeScreen.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/HomeScreen.kt index 724af328..f2561e4e 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/HomeScreen.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/HomeScreen.kt @@ -16,9 +16,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import de.amosproj3.ziofa.ui.navigation.composables.MenuOptionData import de.amosproj3.ziofa.ui.navigation.composables.MenuOptions import de.amosproj3.ziofa.ui.navigation.data.Emoji +import de.amosproj3.ziofa.ui.navigation.data.MenuOptionData /** Static home screen for navigation */ @Composable diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/composables/MenuOptions.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/composables/MenuOptions.kt index cdb44434..0cba11ee 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/composables/MenuOptions.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/composables/MenuOptions.kt @@ -22,11 +22,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp +import de.amosproj3.ziofa.ui.navigation.data.MenuOptionData -data class MenuOptionData(val title: String, val logoEmoji: String, val onClick: () -> Unit) +private const val CARD_EMOJI_SIZE = 120f +private const val CARD_TITLE_TEXT_SIZE = 40f @Composable -fun MenuOptions(modifier: Modifier = Modifier, menuOptions: List) { +fun MenuOptions(menuOptions: List, modifier: Modifier = Modifier) { Row( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly, @@ -65,10 +67,10 @@ fun MenuCardWithIcon( ) { Text( emoji, - fontSize = TextUnit(120f, TextUnitType.Sp), + fontSize = TextUnit(CARD_EMOJI_SIZE, TextUnitType.Sp), modifier = Modifier.padding(bottom = 20.dp), ) - Text(text, fontSize = TextUnit(40f, TextUnitType.Sp)) + Text(text, fontSize = TextUnit(CARD_TITLE_TEXT_SIZE, TextUnitType.Sp)) } } } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/composables/ZiofaTopBar.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/composables/ZiofaTopBar.kt index 7bfd3006..e493c9a9 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/composables/ZiofaTopBar.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/composables/ZiofaTopBar.kt @@ -26,13 +26,18 @@ import androidx.compose.ui.unit.dp /** Top bar containing the app name and AMOS text */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ZiofaTopBar(screenName: String, showBackButton: Boolean = true, onBack: () -> Unit = {}) { +fun ZiofaTopBar( + screenName: String, + modifier: Modifier = Modifier, + showBackButton: Boolean = true, + onBack: () -> Unit = {}, +) { TopAppBar( { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), ) { Row(verticalAlignment = Alignment.CenterVertically) { if (showBackButton) { diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/data/MenuOptionData.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/data/MenuOptionData.kt new file mode 100644 index 00000000..4cd2f1b4 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/data/MenuOptionData.kt @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.navigation.data + +data class MenuOptionData(val title: String, val logoEmoji: String, val onClick: () -> Unit) diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessesScreen.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessesScreen.kt index efd001e9..3894730a 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessesScreen.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessesScreen.kt @@ -34,7 +34,7 @@ import org.koin.androidx.compose.koinViewModel @Composable fun ProcessesScreen( - modifier: Modifier, + modifier: Modifier = Modifier, viewModel: ProcessesViewModel = koinViewModel(), onClickEdit: (RunningComponent) -> Unit, ) { @@ -51,7 +51,9 @@ fun ProcessesScreen( ProcessesHeader() if (options.isNotEmpty()) { LazyColumn(modifier = Modifier.padding(horizontal = 20.dp).fillMaxSize()) { - items(options) { option -> ProcessListRow(option, onClickEdit = onClickEdit) } + items(options) { option -> + ProcessListRow(option = option, onClickEdit = onClickEdit) + } } } else { Box(modifier.fillMaxSize()) { @@ -64,13 +66,12 @@ fun ProcessesScreen( @Composable fun ProcessListRow( + modifier: Modifier = Modifier, option: RunningComponent, - onClickProcessInfo: (RunningComponent) -> Unit = - {}, // TODO implement modal with info about processes onClickEdit: (RunningComponent) -> Unit = {}, ) { Row( - modifier = Modifier.fillMaxSize().padding(vertical = 10.dp), + modifier = modifier.fillMaxSize().padding(vertical = 10.dp), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, ) { diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/reset/ResetScreen.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/reset/ResetScreen.kt index 510047f4..30860c97 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/reset/ResetScreen.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/reset/ResetScreen.kt @@ -27,18 +27,21 @@ const val RESET_WARNING = This will set all features to disabled for all processes! """ +const val CONFIRM_BUTTON_TEXT_SIZE = 20f +const val WARNING_TEXT_SIZE = 40f + @Composable fun ResetScreen( - modifier: Modifier, - viewModel: ResetViewModel = koinViewModel(), afterResetConfirmed: () -> Unit, + modifier: Modifier = Modifier, + viewModel: ResetViewModel = koinViewModel(), ) { Box(modifier = modifier.fillMaxSize()) { Text( RESET_WARNING, modifier = Modifier.align(Alignment.Center), - fontSize = TextUnit(40f, TextUnitType.Sp), + fontSize = TextUnit(WARNING_TEXT_SIZE, TextUnitType.Sp), fontWeight = FontWeight.Bold, ) Button( @@ -48,7 +51,10 @@ fun ResetScreen( afterResetConfirmed() }, ) { - Text("Reset configuration", fontSize = TextUnit(40f, TextUnitType.Sp)) + Text( + "Reset configuration", + fontSize = TextUnit(CONFIRM_BUTTON_TEXT_SIZE, TextUnitType.Sp), + ) } } } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/reset/ResetViewModel.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/reset/ResetViewModel.kt index 65a26dda..914e39ab 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/reset/ResetViewModel.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/reset/ResetViewModel.kt @@ -5,12 +5,14 @@ package de.amosproj3.ziofa.ui.reset import androidx.lifecycle.ViewModel -import de.amosproj3.ziofa.api.configuration.BackendConfigurationAccess +import androidx.lifecycle.viewModelScope +import de.amosproj3.ziofa.api.configuration.ConfigurationAccess +import de.amosproj3.ziofa.api.configuration.ConfigurationAction +import kotlinx.coroutines.launch // For consistency ;) -class ResetViewModel(private val backendConfigurationAccess: BackendConfigurationAccess) : - ViewModel() { +class ResetViewModel(private val configurationAccess: ConfigurationAccess) : ViewModel() { fun reset() { - backendConfigurationAccess.reset() + viewModelScope.launch { configurationAccess.performAction(ConfigurationAction.Reset) } } } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/ConfigurationConverters.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/ConfigurationConverters.kt index 14c76c80..53e85a5d 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/ConfigurationConverters.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/ConfigurationConverters.kt @@ -4,25 +4,20 @@ package de.amosproj3.ziofa.ui.shared -import de.amosproj3.ziofa.api.configuration.ConfigurationUpdate -import de.amosproj3.ziofa.api.configuration.LocalConfigurationAccess -import de.amosproj3.ziofa.client.JniReferencesConfig -import de.amosproj3.ziofa.client.SysSendmsgConfig -import de.amosproj3.ziofa.client.UprobeConfig -import de.amosproj3.ziofa.client.VfsWriteConfig +import de.amosproj3.ziofa.client.Configuration import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions /** - * Convert ConfigurationUpdate to UI Options ([BackendFeatureOptions] ). Show as enabled depending - * on the PIDs the screen is configuring. + * Convert [Configuration] to UI Options ([BackendFeatureOptions] ). Show as enabled depending on + * the PIDs the screen is configuring. */ -fun ConfigurationUpdate.Valid.toUIOptionsForPids( +fun Configuration.toUIOptionsForPids( relevantPids: List? = null ): List { val options = mutableListOf() if (relevantPids != null) { options.add( - this.configuration.vfsWrite?.let { + this.vfsWrite?.let { BackendFeatureOptions.VfsWriteOption( enabled = it.entries.keys.anyPidsEnabled(relevantPids), pids = it.entries.keys, @@ -31,7 +26,7 @@ fun ConfigurationUpdate.Valid.toUIOptionsForPids( ) options.add( - this.configuration.sysSendmsg?.let { + this.sysSendmsg?.let { BackendFeatureOptions.SendMessageOption( enabled = it.entries.keys.anyPidsEnabled(relevantPids), pids = it.entries.keys, @@ -40,7 +35,7 @@ fun ConfigurationUpdate.Valid.toUIOptionsForPids( ) options.add( - this.configuration.jniReferences?.let { + this.jniReferences?.let { BackendFeatureOptions.JniReferencesOption( enabled = it.pids.anyPidsEnabled(relevantPids), pids = it.pids.toSet(), @@ -48,7 +43,16 @@ fun ConfigurationUpdate.Valid.toUIOptionsForPids( } ?: BackendFeatureOptions.JniReferencesOption(enabled = false, pids = setOf()) ) - this.configuration.uprobes + options.add( + this.sysSigquit?.let { + BackendFeatureOptions.SigquitOption( + enabled = it.pids.anyPidsEnabled(relevantPids), + pids = it.pids.toSet(), + ) + } ?: BackendFeatureOptions.SigquitOption(enabled = false, pids = setOf()) + ) + + this.uprobes .filter { it.pid == null || relevantPids.contains(it.pid!!.toUInt()) } .forEach { uprobeConfig -> options.add( @@ -64,57 +68,3 @@ fun ConfigurationUpdate.Valid.toUIOptionsForPids( } return options.toList() } - -/** - * Convert [BackendFeatureOptions] from UI to configuration and set the changes in the local - * configuration. - */ -fun BackendFeatureOptions.setInLocalConfiguration( - localConfigurationAccess: LocalConfigurationAccess, - pids: Set, - active: Boolean, -) { - when (this) { - is BackendFeatureOptions.VfsWriteOption -> { - localConfigurationAccess.changeFeatureConfiguration( - enable = active, - vfsWriteFeature = VfsWriteConfig(pids.associateWith { DURATION_THRESHOLD }), - ) - } - - is BackendFeatureOptions.SendMessageOption -> { - localConfigurationAccess.changeFeatureConfiguration( - enable = active, - sendMessageFeature = - SysSendmsgConfig( - pids.associateWith { DURATION_THRESHOLD } - // TODO this is not a duration - ), - ) - } - - is BackendFeatureOptions.UprobeOption -> { - localConfigurationAccess.changeFeatureConfiguration( - enable = active, - uprobesFeature = - pids.map { - UprobeConfig( - fnName = this.method, - target = this.odexFilePath, - offset = this.offset, - pid = it, - ) - }, - ) - } - - is BackendFeatureOptions.JniReferencesOption -> { - localConfigurationAccess.changeFeatureConfiguration( - enable = active, - jniReferencesFeature = JniReferencesConfig(pids.toList()), - ) - } - - else -> throw NotImplementedError("NO SUPPORT YET") - } -} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/ConfigurationHelpers.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/ConfigurationHelpers.kt new file mode 100644 index 00000000..73f2c365 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/ConfigurationHelpers.kt @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.shared diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/Settings.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/Settings.kt index 642f5618..8c1f98fc 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/Settings.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/Settings.kt @@ -5,7 +5,7 @@ package de.amosproj3.ziofa.ui.shared /** How often the process list should be refreshed from the backend */ -val PROCESS_LIST_REFRESH_INTERVAL_MS = 1000L +const val PROCESS_LIST_REFRESH_INTERVAL_MS = 1000L /** The maximum number of datapoints to show on screen */ const val TIME_SERIES_SIZE = 20 diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/SymbolsViewModel.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/SymbolsViewModel.kt index ed46b58e..dfad3dd1 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/SymbolsViewModel.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/SymbolsViewModel.kt @@ -6,14 +6,15 @@ package de.amosproj3.ziofa.ui.symbols import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import de.amosproj3.ziofa.api.configuration.ConfigurationAccess +import de.amosproj3.ziofa.api.configuration.ConfigurationAction import de.amosproj3.ziofa.api.configuration.GetSymbolsRequestState -import de.amosproj3.ziofa.api.configuration.LocalConfigurationAccess import de.amosproj3.ziofa.api.configuration.SymbolsAccess import de.amosproj3.ziofa.client.UprobeConfig +import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions import de.amosproj3.ziofa.ui.symbols.data.SymbolsEntry import de.amosproj3.ziofa.ui.symbols.data.SymbolsScreenState import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -23,21 +24,41 @@ import timber.log.Timber class SymbolsViewModel( private val symbolsAccess: SymbolsAccess, - private val localConfigurationAccess: LocalConfigurationAccess, + private val configurationAccess: ConfigurationAccess, val pids: List, ) : ViewModel() { val screenState = MutableStateFlow(SymbolsScreenState.WaitingForSearch) fun submit() { - val currentState = screenState.value - if (currentState is SymbolsScreenState.SearchResultReady) { - val selectedSymbols = currentState.symbols.entries.filter { it.value }.map { it.key } - pids.forEach { pid -> - localConfigurationAccess.changeFeatureConfiguration( - uprobesFeature = selectedSymbols.map { it.toUprobeConfigForPid(pid) }, - enable = true, - ) + viewModelScope.launch { + val currentState = screenState.value + if (currentState is SymbolsScreenState.SearchResultReady) { + val selectedSymbols = + currentState.symbols.entries.filter { it.value }.map { it.key } + pids.forEach { pid -> + selectedSymbols.forEach { + configurationAccess.performAction( + // TODO how to we make sure, if there are multiple pids, that the we + // only + // set uprobes for the pids where each symbol is coming from?? + + // TODO replace SymbolsEntry with BackendFeatureOption for consistency + ConfigurationAction.ChangeFeature( + backendFeature = + BackendFeatureOptions.UprobeOption( + method = it.name, + enabled = true, + pids = pids.toSet(), + offset = it.offset, + odexFilePath = it.odexFile, + ), + enable = true, + pids = pids.toSet(), + ) + ) + } + } } } } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/composables/SearchResultList.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/composables/SearchResultList.kt index 0c8a23db..56baee3d 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/composables/SearchResultList.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/composables/SearchResultList.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import de.amosproj3.ziofa.ui.symbols.data.SymbolsEntry +@Suppress("MagicNumber") // does not improve readability @Composable fun SearchResultList( symbols: Map, diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/composables/SymbolsSearchBar.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/composables/SymbolsSearchBar.kt index cff6013f..7c1e9168 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/composables/SymbolsSearchBar.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/symbols/composables/SymbolsSearchBar.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +@Suppress("MagicNumber") // does not improve readability @Composable fun SymbolsSearchBar( value: String, diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/theme/Color.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/theme/Color.kt index f437422b..068b9aa7 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/theme/Color.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/theme/Color.kt @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: MIT +@file:Suppress("MagicNumber") + package de.amosproj3.ziofa.ui.theme import androidx.compose.ui.graphics.Color diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/theme/Theme.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/theme/Theme.kt index 824de985..3ecb6845 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/theme/Theme.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/theme/Theme.kt @@ -6,40 +6,13 @@ package de.amosproj3.ziofa.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -private val DarkColorScheme = - darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80) - -private val LightColorScheme = - lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ - ) - @Composable -fun ZIOFATheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit, -) { +fun ZIOFATheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val context = LocalContext.current val colorScheme = if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/FeatureVisualizationConfig.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/FeatureVisualizationConfig.kt new file mode 100644 index 00000000..18b83d02 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/FeatureVisualizationConfig.kt @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: 2025 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.visualization + +import de.amosproj3.ziofa.api.events.DataStreamProvider +import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions +import de.amosproj3.ziofa.ui.visualization.data.ChartMetadata +import de.amosproj3.ziofa.ui.visualization.data.DropdownOption +import de.amosproj3.ziofa.ui.visualization.data.EventListEntry +import de.amosproj3.ziofa.ui.visualization.data.EventListMetadata +import de.amosproj3.ziofa.ui.visualization.data.GraphedData +import de.amosproj3.ziofa.ui.visualization.utils.DEFAULT_CHART_METADATA +import de.amosproj3.ziofa.ui.visualization.utils.DEFAULT_EVENT_LIST_METADATA +import de.amosproj3.ziofa.ui.visualization.utils.getPIDsOrNull +import de.amosproj3.ziofa.ui.visualization.utils.nanosToSeconds +import de.amosproj3.ziofa.ui.visualization.utils.toBucketedHistogram +import de.amosproj3.ziofa.ui.visualization.utils.toCombinedReferenceCount +import de.amosproj3.ziofa.ui.visualization.utils.toEventList +import de.amosproj3.ziofa.ui.visualization.utils.toMovingAverage +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import timber.log.Timber + +/* + * When adding new features, the event visualizations have to be configured in this file. + */ + +/** Configures the metadata for charts for [BackendFeatureOptions]. */ +fun DropdownOption.Metric.getChartMetadata(): ChartMetadata { + return when (this.backendFeature) { + is BackendFeatureOptions.VfsWriteOption -> + ChartMetadata(yLabel = "Top file descriptors", xLabel = "File Descriptor Name") + + is BackendFeatureOptions.SendMessageOption -> + ChartMetadata(yLabel = "Average duration of messages", xLabel = "Seconds since start") + + is BackendFeatureOptions.JniReferencesOption -> + ChartMetadata( + yLabel = "Number of JNI Indirect References", + xLabel = "Events since start", + ) + + else -> { + Timber.e("needs metadata!") + DEFAULT_CHART_METADATA + } + } +} + +/** Configures the headers for [BackendFeatureOptions]. */ +fun DropdownOption.Metric.getEventListMetadata(): EventListMetadata { + return when (this.backendFeature) { + is BackendFeatureOptions.VfsWriteOption -> + EventListMetadata( + label1 = "Process ID", + label2 = "File Descriptor", + label3 = "Event time since Boot in s", + label4 = "Size in byte", + ) + + is BackendFeatureOptions.SendMessageOption -> + EventListMetadata( + label1 = "Process ID", + label2 = "File Descriptor", + label3 = "Event time since Boot in s", + label4 = "Duration in ms", + ) + + is BackendFeatureOptions.JniReferencesOption -> + EventListMetadata( + label1 = "Process ID", + label2 = "Thread ID", + label3 = "Event time since Boot in s", + label4 = "JNI Method Name", + ) + + is BackendFeatureOptions.SigquitOption -> + EventListMetadata( + label1 = "Origin PID", + label2 = "Origin TID", + label3 = "Target PID", + label4 = "Timestamp", + ) + + else -> { + Timber.e("needs metadata!") + DEFAULT_EVENT_LIST_METADATA + } + } +} + +/** + * Create a [Flow] of [GraphedData.EventListData] for the given selection. New features have to be + * mapped to their events here. + */ +fun DataStreamProvider.getEventListData( + selectedComponent: DropdownOption, + selectedMetric: DropdownOption.Metric, +): Flow { + val pids = selectedComponent.getPIDsOrNull() + val metric = selectedMetric.backendFeature + return when (metric) { + is BackendFeatureOptions.VfsWriteOption -> + this.vfsWriteEvents(pids = pids) + .map { + EventListEntry( + col1 = "${it.fp}", + col2 = "${it.pid}", + col3 = it.beginTimeStamp.nanosToSeconds(), + col4 = "${it.bytesWritten}", + ) + } + .toEventList() + + is BackendFeatureOptions.SendMessageOption -> + this.sendMessageEvents(pids = pids) + .map { + EventListEntry( + col1 = "${it.pid}", + col2 = "${it.fd}", + col3 = it.beginTimeStamp.nanosToSeconds(), + col4 = it.durationNanoSecs.nanosToSeconds(), + ) + } + .toEventList() + + is BackendFeatureOptions.JniReferencesOption -> + this.jniReferenceEvents(pids = pids) + .map { + EventListEntry( + col1 = "${it.pid}", + col2 = "${it.tid}", + col3 = "${it.beginTimeStamp}", + col4 = it.jniMethodName!!.name, // TODO why is this nullable?? + ) + } + .toEventList() + + is BackendFeatureOptions.SigquitOption -> + this.sigquitEvents(pids = pids) + .map { + EventListEntry( + col1 = "${it.pid}", + col2 = "${it.tid}", + col3 = "${it.targetPid}", + col4 = it.timeStamp.nanosToSeconds(), + ) + } + .toEventList() + + else -> throw NotImplementedError("NOT IMPLEMENTED YET") + } +} + +/** + * Create a [Flow] of [GraphedData] for the given selection. The events should already be aggregated + * in the flow. New features have to be mapped to their events here. + */ +fun DataStreamProvider.getChartData( + selectedComponent: DropdownOption, + selectedMetric: DropdownOption.Metric, + selectedTimeframe: DropdownOption.Timeframe, + chartMetadata: ChartMetadata, +): Flow { + + val pids = selectedComponent.getPIDsOrNull() + val metric = selectedMetric.backendFeature + + return when (metric) { + is BackendFeatureOptions.VfsWriteOption -> + this.vfsWriteEvents(pids = pids).toBucketedHistogram(chartMetadata, selectedTimeframe) + + is BackendFeatureOptions.SendMessageOption -> + this.sendMessageEvents(pids = pids).toMovingAverage(chartMetadata, selectedTimeframe) + + is BackendFeatureOptions.JniReferencesOption -> + this.jniReferenceEvents(pids = pids) + .toCombinedReferenceCount(chartMetadata, selectedTimeframe) + + else -> throw NotImplementedError("NOT IMPLEMENTED YET") + } +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/VisualizationScreen.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/VisualizationScreen.kt index d3a3c32b..eff40589 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/VisualizationScreen.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/VisualizationScreen.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import de.amosproj3.ziofa.ui.configuration.composables.ErrorScreen -import de.amosproj3.ziofa.ui.visualization.composables.EventList +import de.amosproj3.ziofa.ui.visualization.composables.EventListViewer import de.amosproj3.ziofa.ui.visualization.composables.MetricDropdown import de.amosproj3.ziofa.ui.visualization.composables.SwitchModeFab import de.amosproj3.ziofa.ui.visualization.composables.VicoBar @@ -49,22 +49,26 @@ fun VisualizationScreen( Column(verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxSize()) { when (state) { - is VisualizationScreenState.MetricSelectionValid -> { + is VisualizationScreenState.ChartView -> { MetricSelection( selectionData = state.selectionData, - filterSelected = { viewModel.componentSelected(it) }, - metricSelected = { viewModel.metricSelected(it) }, - timeframeSelected = { viewModel.timeframeSelected(it) }, + optionSelected = { viewModel.optionSelected(it) }, ) - DataViewer(state.graphedData) + ChartViewer(state.graphedData) + } + + is VisualizationScreenState.EventListView -> { + MetricSelection( + selectionData = state.selectionData, + optionSelected = { viewModel.optionSelected(it) }, + ) + EventListViewer(state.graphedData, state.eventListMetadata) } is VisualizationScreenState.WaitingForMetricSelection -> { MetricSelection( selectionData = state.selectionData, - filterSelected = { viewModel.componentSelected(it) }, - metricSelected = { viewModel.metricSelected(it) }, - timeframeSelected = { viewModel.timeframeSelected(it) }, + optionSelected = { viewModel.optionSelected(it) }, ) SelectMetricPrompt() } @@ -74,18 +78,31 @@ fun VisualizationScreen( } } - if (state is VisualizationScreenState.MetricSelectionValid) - SwitchModeFab( - Modifier.align(Alignment.BottomEnd), - onClick = { viewModel.switchMode() }, - activeDisplayMode = state.displayMode, - ) + when (state) { + is VisualizationScreenState.EventListView -> { + SwitchModeFab( + modifier = Modifier.align(Alignment.BottomEnd), + onClick = { viewModel.switchMode() }, + text = "Switch to chart mode", + ) + } + + is VisualizationScreenState.ChartView -> { + SwitchModeFab( + modifier = Modifier.align(Alignment.BottomEnd), + onClick = { viewModel.switchMode() }, + text = "Switch to event mode", + ) + } + + else -> {} + } } } @Composable -fun SelectMetricPrompt() { - Box(Modifier.fillMaxSize()) { +fun SelectMetricPrompt(modifier: Modifier = Modifier) { + Box(modifier.fillMaxSize()) { Text( "Please make a selection!", Modifier.align(Alignment.Center), @@ -95,7 +112,7 @@ fun SelectMetricPrompt() { } @Composable -fun DataViewer(data: GraphedData) { +fun ChartViewer(data: GraphedData) { when (data) { is GraphedData.TimeSeriesData -> VicoTimeSeries(seriesData = data.seriesData, chartMetadata = data.metaData) @@ -103,25 +120,25 @@ fun DataViewer(data: GraphedData) { is GraphedData.HistogramData -> VicoBar(seriesData = data.seriesData, chartMetadata = data.metaData) - is GraphedData.EventListData -> EventList(data.eventData) - GraphedData.EMPTY -> {} + else -> TODO() } } @Composable fun MetricSelection( selectionData: SelectionData, - filterSelected: (DropdownOption) -> Unit, - metricSelected: (DropdownOption) -> Unit, - timeframeSelected: (DropdownOption) -> Unit, + optionSelected: (DropdownOption) -> Unit, + modifier: Modifier = Modifier, ) { - Row(Modifier.fillMaxWidth()) { + Row(modifier.fillMaxWidth()) { + val dropdownModifier = Modifier.weight(1f).padding(end = 0.dp) + MetricDropdown( selectionData.componentOptions, "Select a package", - modifier = Modifier.weight(1f).padding(end = 0.dp), - optionSelected = { filterSelected(it) }, + modifier = dropdownModifier, + optionSelected = { optionSelected(it) }, selectedOption = selectionData.selectedComponent.displayName, ) selectionData.metricOptions @@ -130,8 +147,8 @@ fun MetricSelection( MetricDropdown( metricOptions, "Select a metric", - modifier = Modifier.weight(1f).padding(end = 0.dp), - optionSelected = { metricSelected(it) }, + modifier = dropdownModifier, + optionSelected = { optionSelected(it) }, selectedOption = selectionData.selectedMetric?.displayName ?: "Please select...", ) } ?: Spacer(Modifier.weight(1f)) @@ -141,9 +158,10 @@ fun MetricSelection( MetricDropdown( timeframeOptions, "Select an interval for aggregation", - modifier = Modifier.weight(1f).padding(end = 0.dp), - optionSelected = { timeframeSelected(it) }, - selectionData.selectedTimeframe?.displayName ?: "Please select...", + modifier = dropdownModifier, + optionSelected = { optionSelected(it) }, + selectedOption = + selectionData.selectedTimeframe?.displayName ?: "Please select...", ) } ?: Spacer(Modifier.weight(1f)) } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/VisualizationViewModel.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/VisualizationViewModel.kt index 654db008..2edc151e 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/VisualizationViewModel.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/VisualizationViewModel.kt @@ -6,43 +6,35 @@ package de.amosproj3.ziofa.ui.visualization import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import de.amosproj3.ziofa.api.configuration.BackendConfigurationAccess -import de.amosproj3.ziofa.api.configuration.ConfigurationUpdate +import de.amosproj3.ziofa.api.configuration.ConfigurationAccess +import de.amosproj3.ziofa.api.configuration.ConfigurationState import de.amosproj3.ziofa.api.events.DataStreamProvider import de.amosproj3.ziofa.api.processes.RunningComponentsAccess -import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions import de.amosproj3.ziofa.ui.shared.toUIOptionsForPids import de.amosproj3.ziofa.ui.visualization.data.DropdownOption -import de.amosproj3.ziofa.ui.visualization.data.GraphedData import de.amosproj3.ziofa.ui.visualization.data.SelectionData -import de.amosproj3.ziofa.ui.visualization.data.VisualizationMetaData +import de.amosproj3.ziofa.ui.visualization.data.VisualizationDisplayMode import de.amosproj3.ziofa.ui.visualization.data.VisualizationScreenState import de.amosproj3.ziofa.ui.visualization.utils.DEFAULT_SELECTION_DATA import de.amosproj3.ziofa.ui.visualization.utils.DEFAULT_TIMEFRAME_OPTIONS -import de.amosproj3.ziofa.ui.visualization.utils.VisualizationDisplayMode -import de.amosproj3.ziofa.ui.visualization.utils.getChartMetadata +import de.amosproj3.ziofa.ui.visualization.utils.getActiveMetricsForPids import de.amosproj3.ziofa.ui.visualization.utils.getPIDsOrNull -import de.amosproj3.ziofa.ui.visualization.utils.toBucketedHistogram -import de.amosproj3.ziofa.ui.visualization.utils.toEventList -import de.amosproj3.ziofa.ui.visualization.utils.toMovingAverage +import de.amosproj3.ziofa.ui.visualization.utils.isValidSelection import de.amosproj3.ziofa.ui.visualization.utils.toUIOptions import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import timber.log.Timber class VisualizationViewModel( - backendConfigurationAccess: BackendConfigurationAccess, + configurationAccess: ConfigurationAccess, runningComponentsAccess: RunningComponentsAccess, dataStreamProviderFactory: (CoroutineScope) -> DataStreamProvider, ) : ViewModel() { @@ -51,26 +43,32 @@ class VisualizationViewModel( // Mutable state private val selectedComponent = MutableStateFlow(DropdownOption.Global) - private val selectedMetric = MutableStateFlow(null) - private val selectedTimeframe = MutableStateFlow(null) + private val selectedMetric = MutableStateFlow(null) + private val selectedTimeframe = MutableStateFlow(null) private val displayMode = MutableStateFlow(VisualizationDisplayMode.EVENTS) - // Derived selection data - // This needs to be a StateFlow or else we will update the graphed data with every process list - // update and get flickering on the UI + /** + * Combine the mutable state with current configuration and running components to create a + * dropdown options selecting filters. This needs to be a StateFlow or else we will update the + * graphed data with every process list update and get flickering on the UI + */ private val selectionData = combine( selectedComponent, selectedMetric, selectedTimeframe, runningComponentsAccess.runningComponentsList, - backendConfigurationAccess.backendConfiguration, - ) { activeComponent, activeMetric, activeTimeframe, runningComponents, backendConfig -> - if (backendConfig !is ConfigurationUpdate.Valid) - return@combine DEFAULT_SELECTION_DATA + configurationAccess.configurationState, + ) { activeComponent, activeMetric, activeTimeframe, runningComponents, configState -> + val config = + when (configState) { + is ConfigurationState.Synchronized -> configState.configuration + is ConfigurationState.Different -> configState.backendConfiguration + else -> return@combine DEFAULT_SELECTION_DATA + } val configuredComponents = runningComponents.filter { runningComponent -> - backendConfig.toUIOptionsForPids(runningComponent.pids).any { it.active } + config.toUIOptionsForPids(runningComponent.pids).any { it.active } } SelectionData( selectedComponent = activeComponent, @@ -78,9 +76,7 @@ class VisualizationViewModel( selectedTimeframe = activeTimeframe, componentOptions = configuredComponents.toUIOptions(), metricOptions = - backendConfig.getActiveMetricsForPids( - pids = activeComponent.getPIDsOrNull() - ), + config.getActiveMetricsForPids(pids = activeComponent.getPIDsOrNull()), timeframeOptions = if (activeMetric != null) DEFAULT_TIMEFRAME_OPTIONS else null, ) } @@ -96,22 +92,33 @@ class VisualizationViewModel( combine(selectionData, displayMode) { a, b -> a to b } .flatMapLatest { (selection, mode) -> Timber.i("Data flow changed!") - if ( - selection.selectedMetric != null && - selection.selectedMetric is DropdownOption.MetricOption && - selection.selectedTimeframe != null && - selection.selectedTimeframe is DropdownOption.TimeframeOption - ) { - getDisplayedData( - selectedComponent = selection.selectedComponent, - selectedMetric = selection.selectedMetric, - selectedTimeframe = selection.selectedTimeframe, - visualizationMetaData = selection.selectedMetric.getChartMetadata(), - mode = mode, - ) - .onStart { Timber.i("Subscribed to displayed data") } - .onCompletion { Timber.i("Displayed data completed") } - .map { VisualizationScreenState.MetricSelectionValid(it, selection, mode) } + if (isValidSelection(selection.selectedMetric, selection.selectedTimeframe)) { + when (mode) { + VisualizationDisplayMode.CHART -> + dataStreamProvider + .getChartData( + selectedComponent = selection.selectedComponent, + selectedMetric = selection.selectedMetric, + selectedTimeframe = selection.selectedTimeframe, + chartMetadata = selection.selectedMetric.getChartMetadata(), + ) + .map { VisualizationScreenState.ChartView(it, selection) } + + VisualizationDisplayMode.EVENTS -> + dataStreamProvider + .getEventListData( + selectedComponent = selection.selectedComponent, + selectedMetric = selection.selectedMetric, + ) + .map { + VisualizationScreenState.EventListView( + graphedData = it, + selectionData = selection, + eventListMetadata = + selection.selectedMetric.getEventListMetadata(), + ) + } + } } else { flowOf(VisualizationScreenState.WaitingForMetricSelection(selection, mode)) } @@ -122,32 +129,25 @@ class VisualizationViewModel( VisualizationScreenState.Loading, ) - /** Called when a filter is selected */ - fun componentSelected(componentOption: DropdownOption) { - Timber.i("filterSelected()") - selectedComponent.value = componentOption - } - - /** Called when a metric is selected */ - fun metricSelected(metricOption: DropdownOption) { - Timber.i("metricSelected()") - if (metricOption is DropdownOption.MetricOption) { - selectedMetric.value = metricOption - } else { - throw IllegalArgumentException("Wrong usage of this method") - } - } - - /** Called when a timeframe is selected */ - fun timeframeSelected(timeframeOption: DropdownOption) { - Timber.i("timeframeSelected()") - if (timeframeOption is DropdownOption.TimeframeOption) { - selectedTimeframe.value = timeframeOption - } else { - throw IllegalArgumentException("Wrong usage of this method") + /** + * Called when the selection of a dropdown changes. The change should be reflected in the + * displayed data. + */ + fun optionSelected(option: DropdownOption) { + when (option) { + is DropdownOption.App, + is DropdownOption.Process -> selectedComponent.value = option + + is DropdownOption.Metric -> selectedMetric.value = option + is DropdownOption.Timeframe -> selectedTimeframe.value = option + else -> throw NotImplementedError("unsupported selection") } } + /** + * Cycle the active display mode between [VisualizationDisplayMode.CHART] and + * [VisualizationDisplayMode.EVENTS] + */ fun switchMode() { displayMode.update { prev -> when (prev) { @@ -156,46 +156,4 @@ class VisualizationViewModel( } } } - - /** This needs improvement. Creates [Flow] of [GraphedData] based on the selection. */ - private suspend fun getDisplayedData( - selectedComponent: DropdownOption, - selectedMetric: DropdownOption.MetricOption, - selectedTimeframe: DropdownOption.TimeframeOption, - mode: VisualizationDisplayMode, - visualizationMetaData: VisualizationMetaData, - ): Flow { - - val pids = selectedComponent.getPIDsOrNull() - val metric = selectedMetric.backendFeature - - return if (mode == VisualizationDisplayMode.CHART) { - when (metric) { - is BackendFeatureOptions.VfsWriteOption -> - dataStreamProvider - .vfsWriteEvents(pids = pids) - .toBucketedHistogram(visualizationMetaData, selectedTimeframe) - - is BackendFeatureOptions.SendMessageOption -> - dataStreamProvider - .sendMessageEvents(pids = pids) - .toMovingAverage(visualizationMetaData, selectedTimeframe) - - else -> throw NotImplementedError("NOT IMPLEMENTED YET") - } - } else { - when (metric) { - is BackendFeatureOptions.VfsWriteOption -> - dataStreamProvider.vfsWriteEvents(pids = pids).toEventList() - - is BackendFeatureOptions.SendMessageOption -> - dataStreamProvider.sendMessageEvents(pids = pids).toEventList() - - else -> throw NotImplementedError("NOT IMPLEMENTED YET") - } - } - } - - private fun ConfigurationUpdate.Valid.getActiveMetricsForPids(pids: List?) = - this.toUIOptionsForPids(pids).filter { it.active }.map { DropdownOption.MetricOption(it) } } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/EventList.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/EventList.kt deleted file mode 100644 index 1ab4edc5..00000000 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/EventList.kt +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Luca Bretting -// -// SPDX-License-Identifier: MIT - -package de.amosproj3.ziofa.ui.visualization.composables - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.intl.Locale -import de.amosproj3.ziofa.api.events.BackendEvent - -@Composable -fun EventList(events: List) { - val locale = Locale.current.platformLocale - - events.getOrNull(0)?.let { Header(it) } - LazyColumn(Modifier.fillMaxSize()) { - items(events) { event -> - Row { - Text(text = event.processId.toString(), modifier = Modifier.weight(1f)) - Text(text = event.fileDescriptor.toString(), modifier = Modifier.weight(1f)) - Text( - text = - String.format( - locale, - "%.2f", - event.startTimestamp.toDouble() / 1_000_000_000, - ), - modifier = Modifier.weight(1f), - ) - when (event) { - is BackendEvent.SendMessageEvent -> { - Text( - text = (event.durationNanos / 1_000_000u).toString(), - modifier = Modifier.weight(1f), - ) - } - is BackendEvent.VfsWriteEvent -> { - Text(text = event.size.toString(), modifier = Modifier.weight(1f)) - } - } - } - } - } -} - -@Composable -fun Header(firstEvent: BackendEvent) { - Row { - Text(text = "Process ID", modifier = Modifier.weight(1f)) - Text(text = "File Descriptor", modifier = Modifier.weight(1f)) - Text(text = "Event time since Boot in s", modifier = Modifier.weight(1f)) - when (firstEvent) { - is BackendEvent.SendMessageEvent -> { - Text(text = "Duration in ms", modifier = Modifier.weight(1f)) - } - is BackendEvent.VfsWriteEvent -> { - Text(text = "Size in byte", modifier = Modifier.weight(1f)) - } - } - } -} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/EventListViewer.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/EventListViewer.kt new file mode 100644 index 00000000..dd180345 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/EventListViewer.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.visualization.composables + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.amosproj3.ziofa.ui.visualization.data.EventListMetadata +import de.amosproj3.ziofa.ui.visualization.data.GraphedData + +@Composable +fun EventListViewer( + eventListData: GraphedData.EventListData, + eventListMetadata: EventListMetadata, + modifier: Modifier = Modifier, +) { + HeaderRow(eventListMetadata) + LazyColumn(modifier.fillMaxSize()) { + items(eventListData.eventData) { event -> + ListRow(col1 = event.col1, col2 = event.col2, col3 = event.col3, col4 = event.col4) + } + } +} + +@Composable +fun ListRow(col1: String, col2: String, col3: String, col4: String, modifier: Modifier = Modifier) { + Row(modifier = modifier) { + Text(text = col1, modifier = Modifier.weight(1f)) + Text(text = col2, modifier = Modifier.weight(1f)) + Text(text = col3, modifier = Modifier.weight(1f)) + Text(text = col4, modifier = Modifier.weight(1f)) + } +} + +@Composable +fun HeaderRow(eventListMetadata: EventListMetadata, modifier: Modifier = Modifier) { + Row(modifier = modifier) { + Text(text = eventListMetadata.label1, modifier = Modifier.weight(1f)) + Text(text = eventListMetadata.label2, modifier = Modifier.weight(1f)) + Text(text = eventListMetadata.label3, modifier = Modifier.weight(1f)) + Text(text = eventListMetadata.label4, modifier = Modifier.weight(1f)) + } +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/MetricDropdown.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/MetricDropdown.kt index 0286d69c..5b0f09d0 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/MetricDropdown.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/MetricDropdown.kt @@ -28,7 +28,7 @@ import de.amosproj3.ziofa.ui.visualization.data.DropdownOption @OptIn(ExperimentalMaterial3Api::class) @Composable fun MetricDropdown( - options: List, // TODO replace with data class + options: List, title: String, modifier: Modifier = Modifier, optionSelected: (DropdownOption) -> Unit, @@ -37,11 +37,7 @@ fun MetricDropdown( var expanded by remember { mutableStateOf(false) } Box(modifier = modifier) { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it }, - modifier = modifier, - ) { + ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) { TextField( value = selectedOption, onValueChange = {}, @@ -59,7 +55,7 @@ fun MetricDropdown( DropdownMenuItem( text = { Text(option.displayName) }, trailingIcon = { - if (option is DropdownOption.AppOption) { + if (option is DropdownOption.App) { val painter = rememberDrawablePainter(option.icon) Icon(painter = painter, contentDescription = "") } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/SwitchModeFab.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/SwitchModeFab.kt index 1e22dcc5..03a33b9c 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/SwitchModeFab.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/SwitchModeFab.kt @@ -13,23 +13,13 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import de.amosproj3.ziofa.ui.visualization.utils.VisualizationDisplayMode @Composable -fun SwitchModeFab( - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, - activeDisplayMode: VisualizationDisplayMode, -) { +fun SwitchModeFab(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { ExtendedFloatingActionButton( modifier = modifier.padding(end = 25.dp, bottom = 25.dp), onClick = onClick, icon = { Icon(imageVector = Icons.AutoMirrored.Filled.List, contentDescription = "") }, - text = { - when (activeDisplayMode) { - VisualizationDisplayMode.CHART -> "Switch to event mode" - VisualizationDisplayMode.EVENTS -> "Switch to chart mode" - }.let { Text(it) } - }, + text = { Text(text) }, ) } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/VicoBarChart.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/VicoBarChart.kt index fdd87cf0..d2e4bd69 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/VicoBarChart.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/VicoBarChart.kt @@ -35,16 +35,17 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.columnSeries import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer import com.patrykandpatrick.vico.core.common.shape.CorneredShape -import de.amosproj3.ziofa.ui.visualization.data.VisualizationMetaData +import de.amosproj3.ziofa.ui.visualization.data.ChartMetadata +import de.amosproj3.ziofa.ui.visualization.utils.VICO_LINE_COLOR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber @Composable fun VicoBar( - modifier: Modifier = Modifier, + chartMetadata: ChartMetadata, seriesData: List>, - chartMetadata: VisualizationMetaData, + modifier: Modifier = Modifier, ) { Column( modifier.padding(10.dp).fillMaxSize(), @@ -55,9 +56,8 @@ fun VicoBar( Timber.e("bar data $seriesData") modelProducer.SeriesUpdate(seriesData.map { it.second.toInt() }) modelProducer.TimeSeriesChart( - modifier, - chartMetadata, - seriesData.map { it.first.toString() }, + chartMetadata = chartMetadata, + xLabels = seriesData.map { it.first.toString() }, ) } } @@ -65,9 +65,9 @@ fun VicoBar( @Composable private fun CartesianChartModelProducer.TimeSeriesChart( - modifier: Modifier, - chartMetadata: VisualizationMetaData, + chartMetadata: ChartMetadata, xLabels: List, + modifier: Modifier = Modifier, ) { CartesianChartHost( chart = @@ -76,7 +76,7 @@ private fun CartesianChartModelProducer.TimeSeriesChart( ColumnCartesianLayer.ColumnProvider.series( xLabels.map { _ -> rememberLineComponent( - fill = fill(Color(0xff6438a7)), + fill = fill(VICO_LINE_COLOR), shape = CorneredShape.rounded( bottomLeftPercent = 40, diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/VicoTimeSeries.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/VicoTimeSeries.kt index 92ee37a2..46313f09 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/VicoTimeSeries.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/composables/VicoTimeSeries.kt @@ -35,16 +35,17 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.lineSeries import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer import com.patrykandpatrick.vico.core.common.shape.CorneredShape -import de.amosproj3.ziofa.ui.visualization.data.VisualizationMetaData +import de.amosproj3.ziofa.ui.visualization.data.ChartMetadata +import de.amosproj3.ziofa.ui.visualization.utils.VICO_LINE_COLOR import de.amosproj3.ziofa.ui.visualization.utils.isDefaultSeries import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @Composable fun VicoTimeSeries( - modifier: Modifier = Modifier, seriesData: List>, - chartMetadata: VisualizationMetaData, + chartMetadata: ChartMetadata, + modifier: Modifier = Modifier, ) { Column( modifier.padding(10.dp).fillMaxSize(), @@ -53,15 +54,15 @@ fun VicoTimeSeries( val modelProducer = remember { CartesianChartModelProducer() } if (seriesData.isNotEmpty() && !seriesData.isDefaultSeries()) { modelProducer.SeriesUpdate(seriesData) - modelProducer.TimeSeriesChart(modifier, chartMetadata) + modelProducer.TimeSeriesChart(chartMetadata) } } } @Composable private fun CartesianChartModelProducer.TimeSeriesChart( - modifier: Modifier, - chartMetadata: VisualizationMetaData, + chartMetadata: ChartMetadata, + modifier: Modifier = Modifier, ) { CartesianChartHost( chart = @@ -69,7 +70,7 @@ private fun CartesianChartModelProducer.TimeSeriesChart( rememberLineCartesianLayer( LineCartesianLayer.LineProvider.series( LineCartesianLayer.rememberLine( - remember { LineCartesianLayer.LineFill.single(fill(Color(0xffa485e0))) } + remember { LineCartesianLayer.LineFill.single(fill(VICO_LINE_COLOR)) } ) ) ), diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationMetaData.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/ChartMetadata.kt similarity index 67% rename from frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationMetaData.kt rename to frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/ChartMetadata.kt index 7daeba96..e285369f 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationMetaData.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/ChartMetadata.kt @@ -4,4 +4,4 @@ package de.amosproj3.ziofa.ui.visualization.data -data class VisualizationMetaData(val xLabel: String, val yLabel: String) +data class ChartMetadata(val xLabel: String, val yLabel: String) diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/EventListMetadata.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/EventListMetadata.kt new file mode 100644 index 00000000..bf4d08d8 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/EventListMetadata.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.visualization.data + +data class EventListMetadata( + val label1: String, + val label2: String, + val label3: String, + val label4: String, +) diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/GraphedData.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/GraphedData.kt index 3303e925..37365df7 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/GraphedData.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/GraphedData.kt @@ -4,20 +4,20 @@ package de.amosproj3.ziofa.ui.visualization.data -import de.amosproj3.ziofa.api.events.BackendEvent +data class EventListEntry(val col1: String, val col2: String, val col3: String, val col4: String) sealed class GraphedData { data class TimeSeriesData( val seriesData: List>, - val metaData: VisualizationMetaData, + val metaData: ChartMetadata, ) : GraphedData() data class HistogramData( val seriesData: List>, - val metaData: VisualizationMetaData, + val metaData: ChartMetadata, ) : GraphedData() - data class EventListData(val eventData: List) : GraphedData() + data class EventListData(val eventData: List) : GraphedData() data object EMPTY : GraphedData() } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/SelectionData.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/SelectionData.kt index a8c9a371..aebb7510 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/SelectionData.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/SelectionData.kt @@ -14,7 +14,7 @@ sealed class DropdownOption(val displayName: String) { /** Filter options */ data class Process(val processName: String, val pid: UInt) : DropdownOption(processName) - data class AppOption( + data class App( val appName: String, val packageName: String, val icon: Drawable, @@ -28,12 +28,11 @@ sealed class DropdownOption(val displayName: String) { * * @param backendFeature the associated backend feature of the metric option */ - data class MetricOption(val backendFeature: BackendFeatureOptions) : + data class Metric(val backendFeature: BackendFeatureOptions) : DropdownOption(backendFeature.featureName) /** Timeframe options */ - data class TimeframeOption(val amount: Int, val unit: DurationUnit) : - DropdownOption("$amount $unit") + data class Timeframe(val amount: Int, val unit: DurationUnit) : DropdownOption("$amount $unit") } data class SelectionData( diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationDisplayMode.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationDisplayMode.kt new file mode 100644 index 00000000..d45f85ea --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationDisplayMode.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.visualization.data + +enum class VisualizationDisplayMode { + CHART, + EVENTS, +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationScreenState.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationScreenState.kt index 10a17ec1..a2a51eba 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationScreenState.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/data/VisualizationScreenState.kt @@ -4,13 +4,14 @@ package de.amosproj3.ziofa.ui.visualization.data -import de.amosproj3.ziofa.ui.visualization.utils.VisualizationDisplayMode - sealed class VisualizationScreenState { - data class MetricSelectionValid( - val graphedData: GraphedData, + data class ChartView(val graphedData: GraphedData, val selectionData: SelectionData) : + VisualizationScreenState() + + data class EventListView( + val graphedData: GraphedData.EventListData, val selectionData: SelectionData, - val displayMode: VisualizationDisplayMode, + val eventListMetadata: EventListMetadata, ) : VisualizationScreenState() data class WaitingForMetricSelection( diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/Constants.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/Constants.kt index 2dc003dc..9f871582 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/Constants.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/Constants.kt @@ -4,28 +4,24 @@ package de.amosproj3.ziofa.ui.visualization.utils -import de.amosproj3.ziofa.ui.configuration.data.BackendFeatureOptions +import androidx.compose.ui.graphics.Color +import de.amosproj3.ziofa.ui.visualization.data.ChartMetadata import de.amosproj3.ziofa.ui.visualization.data.DropdownOption +import de.amosproj3.ziofa.ui.visualization.data.EventListMetadata import de.amosproj3.ziofa.ui.visualization.data.GraphedData import de.amosproj3.ziofa.ui.visualization.data.SelectionData -import de.amosproj3.ziofa.ui.visualization.data.VisualizationMetaData import kotlin.time.DurationUnit -import timber.log.Timber - -enum class VisualizationDisplayMode { - CHART, - EVENTS, -} +@Suppress("MagicNumber") // these are constants already val DEFAULT_TIMEFRAME_OPTIONS = listOf( - DropdownOption.TimeframeOption(500, DurationUnit.MILLISECONDS), - DropdownOption.TimeframeOption(1, DurationUnit.SECONDS), - DropdownOption.TimeframeOption(2, DurationUnit.SECONDS), - DropdownOption.TimeframeOption(5, DurationUnit.SECONDS), - DropdownOption.TimeframeOption(10, DurationUnit.SECONDS), - DropdownOption.TimeframeOption(20, DurationUnit.SECONDS), - DropdownOption.TimeframeOption(30, DurationUnit.SECONDS), + DropdownOption.Timeframe(500, DurationUnit.MILLISECONDS), + DropdownOption.Timeframe(1, DurationUnit.SECONDS), + DropdownOption.Timeframe(2, DurationUnit.SECONDS), + DropdownOption.Timeframe(5, DurationUnit.SECONDS), + DropdownOption.Timeframe(10, DurationUnit.SECONDS), + DropdownOption.Timeframe(20, DurationUnit.SECONDS), + DropdownOption.Timeframe(30, DurationUnit.SECONDS), ) val DEFAULT_TIMESERIES_DATA = listOf(-1f to -1f) // TODO replace with reasonable defaults @@ -42,19 +38,9 @@ val DEFAULT_SELECTION_DATA = ) val DEFAULT_CHART_METADATA = // TODO replace with reasonable defaults - VisualizationMetaData(xLabel = "x", yLabel = "y") - -fun DropdownOption.MetricOption.getChartMetadata(): VisualizationMetaData { - return when (this.backendFeature) { - is BackendFeatureOptions.VfsWriteOption -> - VisualizationMetaData("Top file descriptors", "File Descriptor Name") - - is BackendFeatureOptions.SendMessageOption -> - VisualizationMetaData("Average duration of messages", "Seconds since start") - - else -> { - Timber.e("needs metadata!") - DEFAULT_CHART_METADATA - } - } -} + ChartMetadata(xLabel = "x", yLabel = "y") + +val DEFAULT_EVENT_LIST_METADATA = EventListMetadata("unknown", "unknown", "unknown", "unknown") + +const val LIGHT_PURPLE = 0xffa485e0 +val VICO_LINE_COLOR = Color(LIGHT_PURPLE) diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/DataFlowAggregators.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/DataFlowAggregators.kt index f212482d..e5a8444e 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/DataFlowAggregators.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/DataFlowAggregators.kt @@ -4,32 +4,42 @@ package de.amosproj3.ziofa.ui.visualization.utils -import de.amosproj3.ziofa.api.events.BackendEvent +import de.amosproj3.ziofa.client.Event import de.amosproj3.ziofa.ui.shared.HISTOGRAM_BUCKETS import de.amosproj3.ziofa.ui.shared.TIME_SERIES_SIZE +import de.amosproj3.ziofa.ui.visualization.data.ChartMetadata import de.amosproj3.ziofa.ui.visualization.data.DropdownOption +import de.amosproj3.ziofa.ui.visualization.data.EventListEntry import de.amosproj3.ziofa.ui.visualization.data.GraphedData -import de.amosproj3.ziofa.ui.visualization.data.VisualizationMetaData import kotlin.time.toDuration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -fun Flow.toBucketedHistogram( - visualizationMetaData: VisualizationMetaData, - timeframe: DropdownOption.TimeframeOption, +fun Flow.toBucketedHistogram( + chartMetadata: ChartMetadata, + timeframe: DropdownOption.Timeframe, ) = this.toBucketedData(timeframe.amount.toDuration(timeframe.unit).inWholeMilliseconds.toULong()) .sortAndClip(HISTOGRAM_BUCKETS) - .map { GraphedData.HistogramData(it, visualizationMetaData) } + .map { GraphedData.HistogramData(it, chartMetadata) } -fun Flow.toMovingAverage( - visualizationMetaData: VisualizationMetaData, - timeframe: DropdownOption.TimeframeOption, +fun Flow.toMovingAverage( + chartMetadata: ChartMetadata, + timeframe: DropdownOption.Timeframe, ) = this.toAveragedDurationOverTimeframe( TIME_SERIES_SIZE, timeframe.amount.toDuration(timeframe.unit).inWholeMilliseconds, ) - .map { GraphedData.TimeSeriesData(it, visualizationMetaData) } + .map { GraphedData.TimeSeriesData(it, chartMetadata) } -fun Flow.toEventList() = this.accumulateEvents().map { GraphedData.EventListData(it) } +fun Flow.toCombinedReferenceCount( + chartMetadata: ChartMetadata, + timeframe: DropdownOption.Timeframe, +) = + this.toReferenceCount().toTimestampedSeries(TIME_SERIES_SIZE, timeframe.amount.toFloat()).map { + GraphedData.TimeSeriesData(seriesData = it, metaData = chartMetadata) + } + +fun Flow.toEventList() = + this.accumulateEvents().map { GraphedData.EventListData(it) } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/TimeSeriesHelpers.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/TimeSeriesHelpers.kt index 2381c76a..55253e05 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/TimeSeriesHelpers.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/TimeSeriesHelpers.kt @@ -4,11 +4,9 @@ package de.amosproj3.ziofa.ui.visualization.utils -import de.amosproj3.ziofa.api.events.BackendEvent -import de.amosproj3.ziofa.ui.visualization.data.DropdownOption -import kotlin.time.toDuration +import androidx.compose.ui.text.intl.Locale +import de.amosproj3.ziofa.client.Event import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.conflate @@ -17,24 +15,13 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.scan -fun Flow.countPerTimeframe(timeframeOption: DropdownOption.TimeframeOption): Flow = - flow { - var previousCount: UInt = 0u - this@countPerTimeframe.collect { currentCount -> - val countedLastTimeframe = currentCount - previousCount - emit(countedLastTimeframe) - previousCount = currentCount - delay(timeframeOption.amount.toDuration(timeframeOption.unit)) - } - } - -fun Flow.toTimestampedSeries(seriesSize: Int, secondsPerDatapoint: Float) = +fun Flow.toTimestampedSeries(seriesSize: Int, secondsPerDatapoint: Float) = this.scan(listOf>()) { prev, next -> val idx = (prev.lastOrNull()?.first ?: 0.0f) + secondsPerDatapoint prev.plus(idx to next.toFloat()).takeLast(seriesSize) } -fun Flow.toAveragedDurationOverTimeframe( +fun Flow.toAveragedDurationOverTimeframe( seriesSize: Int, millisTimeframeDuration: Long, ) = @@ -44,13 +31,13 @@ fun Flow.toAveragedDurationOverTimeframe( prev.plus(idx to next.toFloat()).takeLast(seriesSize) } -fun Flow.windowed(windowMillis: Long): Flow = flow { +fun Flow.windowed(windowMillis: Long): Flow = flow { val buffer = mutableListOf() var windowStart = System.currentTimeMillis() this@windowed.collect { value -> val now = System.currentTimeMillis() - buffer.add(value.durationOrSize) + buffer.add(value.durationNanoSecs) if (now - windowStart >= windowMillis) { val average = buffer.map { it.toFloat() }.average() @@ -67,15 +54,15 @@ fun Flow.windowed(windowMillis: Long): Flow = flow { } } -fun Flow.toBucketedData(millisTimeframeDuration: ULong) = flow { - val collectedEvents = mutableMapOf>() +fun Flow.toBucketedData(millisTimeframeDuration: ULong) = flow { + val collectedEvents = mutableMapOf>() this@toBucketedData.collect { // Remove old val currentTime = System.currentTimeMillis() collectedEvents.entries.forEach { (_, vfsWriteEventsList) -> vfsWriteEventsList.removeAll { - currentTime.toULong() - it.startTimestamp > millisTimeframeDuration + currentTime.toULong() - it.beginTimeStamp > millisTimeframeDuration } } collectedEvents.entries.removeAll { (_, vfsWriteEventsList) -> @@ -83,7 +70,7 @@ fun Flow.toBucketedData(millisTimeframeDuration: ULong) = flow { } // Add new - val key = it.fileDescriptor + val key = it.fp val currentBucketEntries = collectedEvents.getOrElse(key) { mutableListOf() } currentBucketEntries.add(it) collectedEvents[key] = currentBucketEntries @@ -91,22 +78,36 @@ fun Flow.toBucketedData(millisTimeframeDuration: ULong) = flow { // Emit update emit( collectedEvents.entries.map { (fileDescriptor, writeEventsList) -> - fileDescriptor to writeEventsList.sumOf { event -> event.durationOrSize } + fileDescriptor to writeEventsList.sumOf { event -> event.bytesWritten } } ) } } +fun Flow.toReferenceCount() = + this.scan(0 to 0) { prev, next -> + when (next.jniMethodName) { + Event.JniReferences.JniMethodName.AddLocalRef -> prev.first + 1 to prev.second + Event.JniReferences.JniMethodName.DeleteLocalRef -> prev.first - 1 to prev.second + Event.JniReferences.JniMethodName.AddGlobalRef -> prev.first to prev.second + 1 + Event.JniReferences.JniMethodName.DeleteGlobalRef -> prev.first to prev.second - 1 + null -> prev + } + } + .map { it.first + it.second } + @OptIn(FlowPreview::class) fun Flow>>.sortAndClip(limit: Int) = this.map { it.sortedBy { (fd, size) -> size }.reversed().take(limit) }.conflate().sample(2500) -fun DropdownOption.TimeframeOption.toSeconds(): Float { - return this.amount.toDuration(this.unit).inWholeMilliseconds / 1000.0f +@Suppress("MagicNumber") // unit conversion +fun ULong.nanosToSeconds(): String { + val locale = Locale.current.platformLocale + return String.format(locale, "%.2f", this.toDouble() / 1_000_000_000) } -fun Flow.accumulateEvents() = - this.scan(initial = listOf()) { prev, next -> prev.plus(next) } +fun Flow.accumulateEvents() = + this.scan(initial = listOf()) { prev, next -> prev.plus(next) } fun List>.isDefaultSeries(): Boolean { return this == DEFAULT_TIMESERIES_DATA diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/VisualizationHelpers.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/VisualizationHelpers.kt index 6149d2e3..f1817fc3 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/VisualizationHelpers.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/visualization/utils/VisualizationHelpers.kt @@ -5,33 +5,58 @@ package de.amosproj3.ziofa.ui.visualization.utils import de.amosproj3.ziofa.api.processes.RunningComponent +import de.amosproj3.ziofa.client.Configuration import de.amosproj3.ziofa.ui.shared.toReadableString +import de.amosproj3.ziofa.ui.shared.toUIOptionsForPids import de.amosproj3.ziofa.ui.visualization.data.DropdownOption +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract fun DropdownOption.getPIDsOrNull(): List? { return when (this) { is DropdownOption.Global -> null is DropdownOption.Process -> listOf(this.pid) - is DropdownOption.AppOption -> this.pids - else -> throw IllegalStateException("Invalid filter") + is DropdownOption.App -> this.pids + else -> error("Invalid filter") } } fun List.toUIOptions() = - this.map { - when (it) { + this.map { component -> + when (component) { is RunningComponent.Application -> - DropdownOption.AppOption( - appName = it.packageInfo.displayName, - packageName = it.packageInfo.displayName, - icon = it.packageInfo.icon, - pids = it.processList.map { it.pid.toUInt() }, + DropdownOption.App( + appName = component.packageInfo.displayName, + packageName = component.packageInfo.displayName, + icon = component.packageInfo.icon, + pids = component.processList.map { it.pid }, ) is RunningComponent.StandaloneProcess -> DropdownOption.Process( - it.process.cmd.toReadableString(), - pid = it.process.pid.toUInt(), + component.process.cmd.toReadableString(), + pid = component.process.pid, ) } } + +@OptIn(ExperimentalContracts::class) +fun isValidSelection(selectedMetric: DropdownOption?, selectedTimeframe: DropdownOption?): Boolean { + contract { + returns(true) implies + (selectedMetric is DropdownOption.Metric && + selectedTimeframe is DropdownOption.Timeframe) + } + + return selectedMetric != null && + selectedMetric is DropdownOption.Metric && + selectedTimeframe != null && + selectedTimeframe is DropdownOption.Timeframe +} + +/** + * Get a list of dropdown options from the [Configuration]. This list only contains metric that are + * configured (== active) for the any of the given [pids]. + */ +fun Configuration.getActiveMetricsForPids(pids: List?) = + this.toUIOptionsForPids(pids).filter { it.active }.map { DropdownOption.Metric(it) } diff --git a/frontend/build.gradle.kts b/frontend/build.gradle.kts index 4e62468c..c418b6a2 100644 --- a/frontend/build.gradle.kts +++ b/frontend/build.gradle.kts @@ -1,4 +1,5 @@ -import com.android.utils.TraceUtils.simpleId +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektPlugin // SPDX-FileCopyrightText: 2024 Felix Hilgers // SPDX-FileCopyrightText: 2024 Luca Bretting @@ -17,6 +18,7 @@ plugins { alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.com.ncorti.ktfmt.gradle) apply true alias(libs.plugins.android.library) apply false + alias(libs.plugins.detekt) } subprojects { @@ -40,7 +42,7 @@ tasks.dependencyUpdates.configure { } } -tasks.register("combinedFormat"){ +tasks.register("combinedFormat") { dependsOn(tasks.ktfmtFormat) dependsOn(tasks.versionCatalogFormat) -} +} \ No newline at end of file diff --git a/frontend/client/src/main/java/de/amosproj3/ziofa/client/Client.kt b/frontend/client/src/main/java/de/amosproj3/ziofa/client/Client.kt index 70e45182..9b3cffbe 100644 --- a/frontend/client/src/main/java/de/amosproj3/ziofa/client/Client.kt +++ b/frontend/client/src/main/java/de/amosproj3/ziofa/client/Client.kt @@ -12,6 +12,7 @@ data class Configuration( val sysSendmsg: SysSendmsgConfig?, val uprobes: List, val jniReferences: JniReferencesConfig?, + val sysSigquit: SysSigquitConfig?, ) data class VfsWriteConfig(val entries: Map) @@ -22,6 +23,8 @@ data class UprobeConfig(val fnName: String, val offset: ULong, var target: Strin data class JniReferencesConfig(val pids: List) +data class SysSigquitConfig(val pids: List) + sealed class Event { data class VfsWrite( val pid: UInt, @@ -52,6 +55,13 @@ sealed class Event { DeleteGlobalRef, } } + + data class SysSigquit( + val pid: UInt, + val tid: UInt, + val timeStamp: ULong, + val targetPid: ULong, + ) : Event() } data class Process(val pid: UInt, val ppid: UInt, val state: String, val cmd: Command?) diff --git a/frontend/client/src/mock/java/de/amosproj3/ziofa/client/RustClient.kt b/frontend/client/src/mock/java/de/amosproj3/ziofa/client/RustClient.kt index 64327c0b..1e3aedc2 100644 --- a/frontend/client/src/mock/java/de/amosproj3/ziofa/client/RustClient.kt +++ b/frontend/client/src/mock/java/de/amosproj3/ziofa/client/RustClient.kt @@ -21,6 +21,7 @@ object RustClient : Client { sysSendmsg = SysSendmsgConfig(mapOf(1234u to 30000u, 43124u to 20000u)), uprobes = listOf(), jniReferences = JniReferencesConfig(pids = listOf()), + sysSigquit = SysSigquitConfig(pids = listOf()), ) override suspend fun serverCount(): Flow = flow { @@ -110,12 +111,34 @@ object RustClient : Client { ) } configuration.jniReferences?.pids?.forEach { + val rnd = Random.nextFloat() + if (rnd > 0.33f) { + emit( + Event.JniReferences( + pid = it, + tid = 1234u, + beginTimeStamp = System.currentTimeMillis().toULong(), + jniMethodName = Event.JniReferences.JniMethodName.AddGlobalRef, + ) + ) + } else { + emit( + Event.JniReferences( + pid = it, + tid = 1234u, + beginTimeStamp = System.currentTimeMillis().toULong(), + jniMethodName = Event.JniReferences.JniMethodName.DeleteLocalRef, + ) + ) + } + } + configuration.sysSigquit?.pids?.forEach { emit( - Event.JniReferences( + Event.SysSigquit( pid = it, tid = 1234u, - beginTimeStamp = System.currentTimeMillis().toULong(), - jniMethodName = Event.JniReferences.JniMethodName.AddLocalRef, + timeStamp = 12312412u, + targetPid = 12874u, ) ) } diff --git a/frontend/client/src/real/java/de.amosproj3.ziofa.client/RustClient.kt b/frontend/client/src/real/java/de.amosproj3.ziofa.client/RustClient.kt index 566803a0..3b75e787 100644 --- a/frontend/client/src/real/java/de.amosproj3.ziofa.client/RustClient.kt +++ b/frontend/client/src/real/java/de.amosproj3.ziofa.client/RustClient.kt @@ -64,6 +64,13 @@ private fun uniffi.shared.Event.into() = JniMethodName.UNDEFINED -> null }, ) + is EventData.SysSigquit -> + Event.SysSigquit( + pid = d.v1.pid, + tid = d.v1.tid, + timeStamp = d.v1.timeStamp, + targetPid = d.v1.targetPid, + ) null -> null } @@ -81,6 +88,7 @@ private fun uniffi.shared.Configuration.into() = ) }, jniReferences = jniReferences?.let { JniReferencesConfig(pids = it.pids) }, + sysSigquit = sysSigquit?.let { SysSigquitConfig(pids = it.pids) }, ) private fun Configuration.into() = @@ -97,6 +105,7 @@ private fun Configuration.into() = ) }, jniReferences = jniReferences?.let { uniffi.shared.JniReferencesConfig(it.pids) }, + sysSigquit = sysSigquit?.let { uniffi.shared.SysSigquitConfig(it.pids) }, ) private fun uniffi.shared.StringResponse.into() = StringResponse(name) diff --git a/frontend/gradle/libs.versions.toml b/frontend/gradle/libs.versions.toml index 744d8802..71b368b4 100644 --- a/frontend/gradle/libs.versions.toml +++ b/frontend/gradle/libs.versions.toml @@ -6,12 +6,14 @@ [versions] accompanistDrawablepainter = "0.36.0" activityCompose = "1.9.3" -agp = "8.7.2" +agp = "8.7.3" benmanes-versions = "0.51.0" -compose-navigation = "2.8.4" -composeBom = "2024.11.00" +compose-navigation = "2.8.5" +composeBom = "2024.12.01" coreKtx = "1.15.0" cyclonedx = "1.10.0" +detekt = "1.23.7" +detektRulesVersion = "0.0.26" espressoCore = "3.6.1" jna = "5.15.0" junit = "4.13.2" @@ -24,6 +26,8 @@ timber = "5.0.1" versioncatalogueupdate = "0.8.5" vico = "2.0.0-beta.3" coroutines = "1.9.0" +flowredux="1.2.2" +arrow = "1.2.4" [libraries] accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanistDrawablepainter" } @@ -41,6 +45,7 @@ androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +detekt-compose-rules = { module = "com.twitter.compose.rules:detekt", version.ref = "detektRulesVersion" } jackwharton-timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } junit = { module = "junit:junit", version.ref = "junit" } @@ -55,12 +60,18 @@ vico-core = { module = "com.patrykandpatrick.vico:core", version.ref = "vico" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } +flowredux-jvm = { module = 'com.freeletics.flowredux:flowredux-jvm', version.ref="flowredux" } +flowredux-compose = { module = 'com.freeletics.flowredux:compose' ,version.ref ="flowredux"} +arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } +arrow-fx-coroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } com-github-benmanes-versions = { id = "com.github.ben-manes.versions", version.ref = "benmanes-versions" } com-ncorti-ktfmt-gradle = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt"} kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } nl-littlerobots-versioncatalogueupdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versioncatalogueupdate" } org-cyclonedx-bom = { id = "org.cyclonedx.bom", version.ref = "cyclonedx" } diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a6939483..fc646d31 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -440,6 +440,20 @@ dependencies = [ "xtask", ] +[[package]] +name = "backend-ebpf-test" +version = "0.1.0" +dependencies = [ + "aya", + "aya-ebpf", + "aya-log-ebpf", + "aya-obj", + "backend-common", + "cargo_metadata 0.19.1", + "libc", + "which", +] + [[package]] name = "backtrace" version = "0.3.74" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index dfda1755..8802f3ff 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -15,7 +15,7 @@ members = [ "shared", "client", "playground/sendmsg-demo", - "uniffi-bindgen", + "uniffi-bindgen", "backend/ebpf-test", ] default-members = [ "xtask", @@ -72,6 +72,7 @@ object = "0.36.5" bytemuck = { version = "1.20.0" } crossbeam = "0.8.4" ractor = { version = "0.13.4", default-features = false } +aya-obj = "0.2.1" [profile.release.package.backend-ebpf] debug = 2 diff --git a/rust/backend/daemon/src/collector/mod.rs b/rust/backend/daemon/src/collector/mod.rs index e11eebd6..d25c752d 100644 --- a/rust/backend/daemon/src/collector/mod.rs +++ b/rust/backend/daemon/src/collector/mod.rs @@ -4,10 +4,10 @@ // // SPDX-License-Identifier: MIT -use backend_common::{JNICall, JNIMethodName, SysSendmsgCall, VfsWriteCall}; -use shared::ziofa::{Event, JniReferencesEvent, SysSendmsgEvent, VfsWriteEvent}; -use shared::ziofa::event::{EventData}; -use shared::ziofa::jni_references_event::{JniMethodName}; +use backend_common::{JNICall, JNIMethodName, SysSendmsgCall, VfsWriteCall, SysSigquitCall}; +use shared::ziofa::{Event, JniReferencesEvent, SysSendmsgEvent, VfsWriteEvent, SysSigquitEvent}; +use shared::ziofa::event::EventData; +use shared::ziofa::jni_references_event::JniMethodName; mod ring_buf; mod supervisor; mod event_dipatcher; @@ -63,4 +63,17 @@ impl IntoEvent for JNICall { })) } } +} + +impl IntoEvent for SysSigquitCall { + fn into_event(self) -> Event { + Event { + event_data: Some(EventData::SysSigquit(SysSigquitEvent { + pid: self.pid, + tid: self.tid, + time_stamp: self.time_stamp, + target_pid: self.target_pid, + })) + } + } } \ No newline at end of file diff --git a/rust/backend/daemon/src/collector/supervisor.rs b/rust/backend/daemon/src/collector/supervisor.rs index 6cead8f6..c36ad0e3 100644 --- a/rust/backend/daemon/src/collector/supervisor.rs +++ b/rust/backend/daemon/src/collector/supervisor.rs @@ -21,6 +21,7 @@ enum CollectorT { VfsWrite, SysSendmsg, JniCall, + SysSigquit, } pub struct CollectorSupervisor; @@ -48,7 +49,7 @@ impl CollectorRefs { self.collectors.remove(cell) } async fn start_all(&mut self, registry: &EbpfEventRegistry, event_actor: &ActorRef, supervisor: &ActorCell) -> Result<(), ActorProcessingErr> { - for who in [CollectorT::VfsWrite, CollectorT::SysSendmsg, CollectorT::JniCall] { + for who in [CollectorT::VfsWrite, CollectorT::SysSendmsg, CollectorT::JniCall, CollectorT::SysSigquit] { self.start(who, registry, event_actor, supervisor).await?; } Ok(()) @@ -58,6 +59,7 @@ impl CollectorRefs { CollectorT::VfsWrite => start_collector(registry.vfs_write_events.clone(), event_actor.clone(), supervisor.clone()).await?, CollectorT::SysSendmsg => start_collector(registry.sys_sendmsg_events.clone(), event_actor.clone(), supervisor.clone()).await?, CollectorT::JniCall => start_collector(registry.jni_ref_calls.clone(), event_actor.clone(), supervisor.clone()).await?, + CollectorT::SysSigquit => start_collector(registry.sys_sigquit_events.clone(), event_actor.clone(), supervisor.clone()).await?, }; self.collectors.insert(actor_ref.get_cell(), who); Ok(()) diff --git a/rust/backend/daemon/src/configuration.rs b/rust/backend/daemon/src/configuration.rs deleted file mode 100644 index ec64235b..00000000 --- a/rust/backend/daemon/src/configuration.rs +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Benedikt Zinn -// -// SPDX-License-Identifier: MIT - -use std::{ - fs::File, - io, - io::{BufReader, BufWriter}, -}; - -use shared::config::Configuration; - -pub fn load_from_file(path: &str) -> io::Result { - let file = File::open(path)?; - let reader = BufReader::new(file); - let config = serde_json::from_reader(reader)?; - Ok(config) -} -pub fn save_to_file(config: &Configuration, path: &str) -> io::Result<()> { - let file = File::create(path)?; - let writer = BufWriter::new(file); - serde_json::to_writer(writer, config)?; - Ok(()) -} diff --git a/rust/backend/daemon/src/features/mod.rs b/rust/backend/daemon/src/features/mod.rs index 495df57b..d610e4e9 100644 --- a/rust/backend/daemon/src/features/mod.rs +++ b/rust/backend/daemon/src/features/mod.rs @@ -8,12 +8,14 @@ mod jni_reference_feature; mod vfs_write_feature; mod sys_sendmsg_feature; +mod sys_sigquit_feature; use std::collections::BTreeSet; use aya::EbpfError; use jni_reference_feature::JNIReferencesFeature; use shared::config::Configuration; use sys_sendmsg_feature::SysSendmsgFeature; +use sys_sigquit_feature::SysSigquitFeature; use vfs_write_feature::VfsWriteFeature; use crate::registry::{EbpfRegistry, OwnedHashMap, RegistryGuard}; @@ -29,6 +31,7 @@ pub trait Feature { pub struct Features { sys_sendmsg_feature: SysSendmsgFeature, + sys_sigquit_feature: SysSigquitFeature, vfs_write_feature: VfsWriteFeature, jni_reference_feature: JNIReferencesFeature, } @@ -39,11 +42,13 @@ impl Features { let sys_sendmsg_feature = SysSendmsgFeature::init(registry); let vfs_write_feature = VfsWriteFeature::init(registry); let jni_reference_feature = JNIReferencesFeature::init(registry); + let sys_sigquit_feature = SysSigquitFeature::init(registry); Self { sys_sendmsg_feature, vfs_write_feature, jni_reference_feature, + sys_sigquit_feature, } } @@ -56,6 +61,7 @@ impl Features { self.vfs_write_feature.apply(&config.vfs_write)?; self.sys_sendmsg_feature.apply(&config.sys_sendmsg)?; self.jni_reference_feature.apply( &config.jni_references)?; + self.sys_sigquit_feature.apply( &config.sys_sigquit)?; Ok(()) } diff --git a/rust/backend/daemon/src/features/sys_sigquit_feature.rs b/rust/backend/daemon/src/features/sys_sigquit_feature.rs new file mode 100644 index 00000000..a4caa869 --- /dev/null +++ b/rust/backend/daemon/src/features/sys_sigquit_feature.rs @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 Tom Weisshuhn +// +// SPDX-License-Identifier: MIT + +use aya::EbpfError; +use aya::programs::trace_point::TracePointLink; +use aya::programs::TracePoint; +use shared::config::SysSigquitConfig; +use crate::features::{update_pids, Feature}; +use crate::registry::{EbpfRegistry, OwnedHashMap, RegistryGuard}; + +pub struct SysSigquitFeature { + sys_enter_sigquit: RegistryGuard, + sys_enter_sigquit_link: Option, + trace_sigquit_pids: RegistryGuard>, +} + +impl SysSigquitFeature { + fn create(registry: &EbpfRegistry) -> Self { + Self { + sys_enter_sigquit: registry.program.sys_sigquit.take(), + sys_enter_sigquit_link: None, + trace_sigquit_pids: registry.config.sys_sigquit_pids.take(), + } + } + + fn attach(&mut self) -> Result<(), EbpfError> { + if self.sys_enter_sigquit_link.is_none() { + let link_id = self.sys_enter_sigquit.attach("syscalls","sys_enter_kill")?; + self.sys_enter_sigquit_link = Some(self.sys_enter_sigquit.take_link(link_id)?); + } + + Ok(()) + } + + fn detach(&mut self) { + // the TrakePointLinks will be automatically detached when the reference is dropped + let _ = self.sys_enter_sigquit_link.take(); + } + + fn update_pids( + &mut self, + pids: &[u32] + ) -> Result<(), EbpfError> { + + // the general update_pids function for all features works with hashmaps, so the list is converted into a hashmap with keys always being 0 + let pid_0_tuples: Vec<(u32, u64)> = pids.iter().map(|pid| (*pid, 0)).collect(); + let pids_as_hashmap: std::collections::HashMap = std::collections::HashMap::from_iter(pid_0_tuples); + + update_pids(&pids_as_hashmap, &mut self.trace_sigquit_pids) + } +} + +impl Feature for SysSigquitFeature { + type Config = SysSigquitConfig; + fn init(registry: &EbpfRegistry) -> Self { + SysSigquitFeature::create(registry) + } + + fn apply(&mut self, config: &Option) -> Result<(), EbpfError> { + match config { + Some(config) => { + self.attach()?; + self.update_pids(&config.pids)?; + } + None => { + self.detach(); + } + } + Ok(()) + } +} + + + + + diff --git a/rust/backend/daemon/src/filesystem/memory.rs b/rust/backend/daemon/src/filesystem/memory.rs new file mode 100644 index 00000000..6307eeeb --- /dev/null +++ b/rust/backend/daemon/src/filesystem/memory.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2025 Franz Schlicht +// +// SPDX-License-Identifier: MIT + +use std::io; + +use shared::config::Configuration; + +use super::Filesystem; + +// TODO: members + implementation +pub struct MemoryFilesystem; + +impl Filesystem for MemoryFilesystem { + fn load(&self, _path: &str) -> io::Result { + todo!() + } + + fn save(&self, _config: &Configuration, _path: &str) -> io::Result<()> { + todo!() + } +} \ No newline at end of file diff --git a/rust/backend/daemon/src/filesystem/mod.rs b/rust/backend/daemon/src/filesystem/mod.rs new file mode 100644 index 00000000..4d22069a --- /dev/null +++ b/rust/backend/daemon/src/filesystem/mod.rs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2025 Franz Schlicht +// +// SPDX-License-Identifier: MIT + +use std::io; + +use shared::config::Configuration; + + +mod normal; +mod memory; + +pub use normal::NormalFilesystem; + +// TODO: pub use memory::MemoryFilesystem; + +/* + * TODOs: + * - This should probably not be named Filesystem, because the functionality is much more narrow + * than that. Maybe something like ConfigurationStore or ConfigurationStorage? + * - The trait should definetly be async, because otherwise we always have to use spawn_blocking. + * See the tokio documentation for why: https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html + * You can use tokio::fs for file system operations. + */ +pub trait Filesystem: Send + Sync + 'static { + fn load(&self, path: &str) -> io::Result; + + fn save(&self, config: &Configuration, path: &str) -> io::Result<()>; +} + diff --git a/rust/backend/daemon/src/filesystem/normal.rs b/rust/backend/daemon/src/filesystem/normal.rs new file mode 100644 index 00000000..94b3d62f --- /dev/null +++ b/rust/backend/daemon/src/filesystem/normal.rs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2025 Franz Schlicht +// +// SPDX-License-Identifier: MIT + +use std::{fs::File, io::{self, BufReader, BufWriter}}; + +use shared::config::Configuration; + +use super::Filesystem; + + +pub struct NormalFilesystem; + +impl Filesystem for NormalFilesystem { + fn load(&self, path: &str) -> io::Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + let config = serde_json::from_reader(reader)?; + Ok(config) + } + + fn save(&self, config: &Configuration, path: &str) -> io::Result<()> { + let file = File::create(path)?; + let writer = BufWriter::new(file); + serde_json::to_writer(writer, config)?; + Ok(()) + } +} \ No newline at end of file diff --git a/rust/backend/daemon/src/lib.rs b/rust/backend/daemon/src/lib.rs index 98904dea..3c3a5a83 100644 --- a/rust/backend/daemon/src/lib.rs +++ b/rust/backend/daemon/src/lib.rs @@ -4,7 +4,6 @@ // // SPDX-License-Identifier: MIT -mod configuration; mod constants; mod ebpf_utils; mod helpers; @@ -14,6 +13,7 @@ mod features; mod collector; mod symbols; mod registry; +mod filesystem; pub async fn run_server() { helpers::bump_rlimit(); diff --git a/rust/backend/daemon/src/main.rs b/rust/backend/daemon/src/main.rs index 817f02d8..78d2b54b 100644 --- a/rust/backend/daemon/src/main.rs +++ b/rust/backend/daemon/src/main.rs @@ -5,7 +5,6 @@ // SPDX-License-Identifier: MIT use tracing_subscriber::EnvFilter; -mod configuration; mod constants; mod ebpf_utils; mod helpers; @@ -15,6 +14,7 @@ mod features; mod collector; mod symbols; mod registry; +mod filesystem; #[tokio::main] async fn main() { diff --git a/rust/backend/daemon/src/registry/mod.rs b/rust/backend/daemon/src/registry/mod.rs index 2575d177..8024450b 100644 --- a/rust/backend/daemon/src/registry/mod.rs +++ b/rust/backend/daemon/src/registry/mod.rs @@ -12,7 +12,7 @@ mod typed_ringbuf; use aya::{maps::{HashMap, MapData, MapError, RingBuf}, programs::{KProbe, ProbeKind, ProgramError, TracePoint, UProbe}, EbpfError, EbpfLoader}; use aya_log::EbpfLogger; -use backend_common::{JNICall, SysSendmsgCall, VfsWriteCall}; +use backend_common::{JNICall, SysSendmsgCall, VfsWriteCall, SysSigquitCall}; use pinning::{LoadAndPin, TryMapFromPin}; pub use typed_ringbuf::TypedRingBuffer; pub use single_owner::{RegistryGuard, RegistryItem}; @@ -32,6 +32,7 @@ pub struct EbpfConfigRegistry { pub vfs_write_pids: RegistryItem>, pub sys_sendmsg_pids: RegistryItem>, pub jni_ref_pids: RegistryItem>, + pub sys_sigquit_pids: RegistryItem>, } #[derive(Clone)] @@ -39,6 +40,7 @@ pub struct EbpfEventRegistry { pub vfs_write_events: RegistryItem>, pub sys_sendmsg_events: RegistryItem>, pub jni_ref_calls: RegistryItem>, + pub sys_sigquit_events: RegistryItem>, } #[derive(Clone)] @@ -51,6 +53,7 @@ pub struct EbpfProgramRegistry { pub trace_del_local: RegistryItem, pub trace_add_global: RegistryItem, pub trace_del_global: RegistryItem, + pub sys_sigquit: RegistryItem, } impl EbpfRegistry { @@ -69,6 +72,7 @@ impl EbpfConfigRegistry { vfs_write_pids: HashMap::<_, u32, u64>::try_from_pin(path("VFS_WRITE_PIDS"))?.into(), sys_sendmsg_pids: HashMap::<_, u32, u64>::try_from_pin(path("SYS_SENDMSG_PIDS"))?.into(), jni_ref_pids: HashMap::<_, u32, u64>::try_from_pin(path("JNI_REF_PIDS"))?.into(), + sys_sigquit_pids: HashMap::<_, u32, u64>::try_from_pin(path("SYS_SIGQUIT_PIDS"))?.into(), }) } } @@ -79,6 +83,7 @@ impl EbpfEventRegistry { vfs_write_events: RingBuf::try_from_pin(path("VFS_WRITE_EVENTS"))?.into(), sys_sendmsg_events: RingBuf::try_from_pin(path("SYS_SENDMSG_EVENTS"))?.into(), jni_ref_calls: RingBuf::try_from_pin(path("JNI_REF_CALLS"))?.into(), + sys_sigquit_events: RingBuf::try_from_pin(path("SYS_SIGQUIT_EVENTS"))?.into(), }) } } @@ -94,6 +99,7 @@ impl EbpfProgramRegistry { trace_del_local: UProbe::from_pin(path("trace_del_local"), ProbeKind::UProbe)?.into(), trace_add_global: UProbe::from_pin(path("trace_add_global"), ProbeKind::UProbe)?.into(), trace_del_global: UProbe::from_pin(path("trace_del_global"), ProbeKind::UProbe)?.into(), + sys_sigquit: TracePoint::from_pin(path("sys_sigquit"))?.into(), }) } } @@ -121,6 +127,7 @@ pub fn load_and_pin() -> Result { ebpf.load_and_pin::("trace_del_local", ZIOFA_EBPF_PATH).unwrap(); ebpf.load_and_pin::("trace_add_global", ZIOFA_EBPF_PATH).unwrap(); ebpf.load_and_pin::("trace_del_global", ZIOFA_EBPF_PATH).unwrap(); + ebpf.load_and_pin::("sys_sigquit", ZIOFA_EBPF_PATH).unwrap(); EbpfRegistry::from_pin() } diff --git a/rust/backend/daemon/src/server.rs b/rust/backend/daemon/src/server.rs index 42b14459..08f755c3 100644 --- a/rust/backend/daemon/src/server.rs +++ b/rust/backend/daemon/src/server.rs @@ -6,10 +6,11 @@ // SPDX-License-Identifier: MIT use crate::collector::{CollectorSupervisor, CollectorSupervisorArguments}; +use crate::filesystem::{Filesystem, NormalFilesystem}; use crate::registry; use crate::symbols::SymbolHandler; use crate::{ - configuration, constants, + constants, ebpf_utils::EbpfErrorWrapper, procfs_utils::{list_processes, ProcErrorWrapper}, features::Features, @@ -30,22 +31,27 @@ use tokio::sync::{mpsc, Mutex}; use tokio_stream::wrappers::ReceiverStream; use tonic::{transport::Server, Request, Response, Status}; -pub struct ZiofaImpl { +pub struct ZiofaImpl +where F: Filesystem { features: Arc>, channel: Arc, symbol_handler: Arc>, + filesystem: F, } -impl ZiofaImpl { +impl ZiofaImpl +where F: Filesystem { pub fn new( features: Arc>, channel: Arc, symbol_handler: Arc>, - ) -> ZiofaImpl { + filesystem: F, + ) -> ZiofaImpl { ZiofaImpl { features, channel, symbol_handler, + filesystem } } } @@ -63,7 +69,8 @@ impl Channel { } #[tonic::async_trait] -impl Ziofa for ZiofaImpl { +impl Ziofa for ZiofaImpl +where F: Filesystem { async fn check_server(&self, _: Request<()>) -> Result, Status> { // dummy data let response = CheckServerResponse {}; @@ -77,7 +84,7 @@ impl Ziofa for ZiofaImpl { async fn get_configuration(&self, _: Request<()>) -> Result, Status> { //TODO: if ? fails needs valid return value for the function so that the server doesn't crash. - let config = configuration::load_from_file(constants::DEV_DEFAULT_FILE_PATH)?; + let config = self.filesystem.load(constants::DEV_DEFAULT_FILE_PATH)?; Ok(Response::new(config)) } @@ -87,7 +94,7 @@ impl Ziofa for ZiofaImpl { ) -> Result, Status> { let config = request.into_inner(); - configuration::save_to_file(&config, constants::DEV_DEFAULT_FILE_PATH)?; + self.filesystem.save(&config, constants::DEV_DEFAULT_FILE_PATH)?; let mut features_guard = self.features.lock().await; @@ -242,7 +249,10 @@ pub async fn serve_forever() { let symbol_handler = Arc::new(Mutex::new(SymbolHandler::new())); let features = Arc::new(Mutex::new(features)); - let ziofa_server = ZiofaServer::new(ZiofaImpl::new(features, channel, symbol_handler)); + + let filesystem = NormalFilesystem; + + let ziofa_server = ZiofaServer::new(ZiofaImpl::new(features, channel, symbol_handler, filesystem)); Server::builder() .add_service(ziofa_server) diff --git a/rust/backend/ebpf-test/Cargo.toml b/rust/backend/ebpf-test/Cargo.toml new file mode 100644 index 00000000..8dc77fdd --- /dev/null +++ b/rust/backend/ebpf-test/Cargo.toml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2024 Felix Hilgers +# +# SPDX-License-Identifier: MIT + +[package] +name = "backend-ebpf-test" +version = "0.1.0" +license.workspace = true +repository.workspace = true +edition.workspace = true + +[dependencies] +backend-common = { workspace = true } +aya-ebpf = { workspace = true } +aya-log-ebpf = { workspace = true } + +aya = { workspace = true } +libc = { workspace = true } +aya-obj = { workspace = true } + +[build-dependencies] +which = { workspace = true } +cargo_metadata = { workspace = true } + +[[test]] +name = "prog-test-run" +path = "tests/prog_test_run.rs" \ No newline at end of file diff --git a/rust/backend/ebpf-test/build.rs b/rust/backend/ebpf-test/build.rs new file mode 100644 index 00000000..91a9402d --- /dev/null +++ b/rust/backend/ebpf-test/build.rs @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: 2024 Benedikt Zinn +// SPDX-FileCopyrightText: 2024 Felix Hilgers +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT +// + +// TODO: this is a verbatim copy of backend-daemon/build.rs with just another build variant of the ebpf program + +use std::{ + env, fs, + io::{BufRead as _, BufReader}, + path::PathBuf, + process::{Child, Command, Stdio}, +}; + +use cargo_metadata::{ + Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, TargetKind, +}; + +/// This crate has a runtime dependency on artifacts produced by the `example-ebpf` crate. +/// This would be better expressed as one or more [artifact-dependencies][bindeps] but issues such +/// as: +/// +/// * https://github.com/rust-lang/cargo/issues/12374 +/// * https://github.com/rust-lang/cargo/issues/12375 +/// * https://github.com/rust-lang/cargo/issues/12385 +/// +/// prevent their use for the time being. +/// +/// This file, along with the xtask crate, allows analysis tools such as `cargo check`, `cargo +/// clippy`, and even `cargo build` to work as users expect. Prior to this file's existence, this +/// crate's undeclared dependency on artifacts from `example-ebpf` would cause build (and +/// `cargo check`, and `cargo clippy`) failures until the user ran certain other commands in the +/// workspace. Conversely, those same tools (e.g. cargo test --no-run) would produce stale results +/// if run naively because they'd make use of artifacts from a previous build of +/// `example-ebpf`. +/// +/// Note that this solution is imperfect: in particular it has to balance correctness with +/// performance; an environment variable is used to replace true builds of `example-ebpf` +/// with stubs to preserve the property that code generation and linking (in +/// `example-ebpf`) do not occur on metadata-only actions such as `cargo check` or `cargo +/// clippy` of this crate. This means that naively attempting to `cargo test --no-run` this crate +/// will produce binaries that fail at runtime because the stubs are inadequate for actually running +/// the tests. +/// +/// [bindeps]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html?highlight=feature#artifact-dependencies +fn main() { + let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap(); + let ebpf_package = packages + .into_iter() + .find(|Package { name, .. }| name == "backend-ebpf") + .unwrap(); + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let out_dir = PathBuf::from(out_dir); + + let endian = env::var_os("CARGO_CFG_TARGET_ENDIAN").unwrap(); + let target = if endian == "big" { + "bpfeb" + } else if endian == "little" { + "bpfel" + } else { + panic!("unsupported endian={:?}", endian) + }; + + let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap(); + + let target = format!("{target}-unknown-none"); + + let Package { manifest_path, .. } = ebpf_package; + let ebpf_dir = manifest_path.parent().unwrap(); + + // We have a build-dependency on `example-ebpf`, so cargo will automatically rebuild us + // if `example-ebpf`'s *library* target or any of its dependencies change. Since we + // depend on `example-ebpf`'s *binary* targets, that only gets us half of the way. This + // stanza ensures cargo will rebuild us on changes to the binaries too, which gets us the + // rest of the way. + println!("cargo:rerun-if-changed={}", ebpf_dir.as_str()); + + let mut cmd = Command::new("cargo"); + cmd.args([ + "build", + "-Z", + "build-std=core", + "--bins", + "--features", "prog-test", + "--message-format=json", + "--release", + "--target", + &target, + ]); + + cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch); + + // Workaround to make sure that the rust-toolchain.toml is respected. + for key in ["RUSTUP_TOOLCHAIN", "RUSTC"] { + cmd.env_remove(key); + } + cmd.current_dir(ebpf_dir); + + // Workaround for https://github.com/rust-lang/cargo/issues/6412 where cargo flocks itself. + let ebpf_target_dir = out_dir.join("backend/ebpf"); + cmd.arg("--target-dir").arg(&ebpf_target_dir); + + let mut child = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}")); + let Child { stdout, stderr, .. } = &mut child; + + // Trampoline stdout to cargo warnings. + let stderr = stderr.take().unwrap(); + let stderr = BufReader::new(stderr); + let stderr = std::thread::spawn(move || { + for line in stderr.lines() { + let line = line.unwrap(); + println!("cargo:warning={line}"); + } + }); + + let stdout = stdout.take().unwrap(); + let stdout = BufReader::new(stdout); + let mut executables = Vec::new(); + for message in Message::parse_stream(stdout) { + #[allow(clippy::collapsible_match)] + match message.expect("valid JSON") { + Message::CompilerArtifact(Artifact { + executable, + target: Target { name, .. }, + .. + }) => { + if let Some(executable) = executable { + executables.push((name, executable.into_std_path_buf())); + } + } + Message::CompilerMessage(CompilerMessage { message, .. }) => { + for line in message.rendered.unwrap_or_default().split('\n') { + println!("cargo:warning={line}"); + } + } + Message::TextLine(line) => { + println!("cargo:warning={line}"); + } + _ => {} + } + } + + let status = child + .wait() + .unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}")); + assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}"); + + stderr.join().map_err(std::panic::resume_unwind).unwrap(); + + for (name, binary) in executables { + let dst = out_dir.join(name); + let _: u64 = fs::copy(&binary, &dst) + .unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); + } +} diff --git a/rust/backend/ebpf-test/tests/prog_test_run.rs b/rust/backend/ebpf-test/tests/prog_test_run.rs new file mode 100644 index 00000000..181db8e8 --- /dev/null +++ b/rust/backend/ebpf-test/tests/prog_test_run.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2025 Franz Schlicht +// +// SPDX-License-Identifier: MIT + +use std::{io, mem, os::fd::{AsFd, AsRawFd}}; + +use aya::{maps::{HashMap, RingBuf}, programs::RawTracePoint, EbpfLoader}; +use aya_obj::generated::{bpf_attr, bpf_cmd}; +use backend_common::{SysSigquitCall, TryFromRaw}; +use libc::{getpid, gettid, syscall, SYS_bpf}; + + +#[test] +fn prog_test_run_example() { + let mut ebpf = EbpfLoader::default() + .load(aya::include_bytes_aligned!(concat!( + env!("OUT_DIR"), + "/backend-ebpf" + ))) + .unwrap(); + + let p: &mut RawTracePoint = ebpf.program_mut("sys_sigquit").unwrap().try_into().unwrap(); + p.load().unwrap(); + p.attach("sys_enter").unwrap(); + + let fd = p.fd().unwrap().as_fd().as_raw_fd(); + + let mut pids: HashMap<_, u32, u64> = ebpf.take_map("SYS_SIGQUIT_PIDS").unwrap().try_into().unwrap(); + let old = pids.iter().filter_map(Result::ok).map(|x| x.0).collect::>(); + for old in old { + pids.remove(&old).unwrap(); + } + // Pid of the program seems to always be the next pid + pids.insert(unsafe { gettid() as u32 }, 0, 0).unwrap(); + let mut events: RingBuf<_> = ebpf.take_map("SYS_SIGQUIT_EVENTS").unwrap().try_into().unwrap(); + + let target_pid = 1111; + let signal = 3; // sigquit + let args = [0u64, 0u64, target_pid, signal]; + + let mut attr = unsafe { mem::zeroed::() }; + + attr.test.prog_fd = fd as u32; + attr.test.ctx_in = args.as_ptr() as u64; + attr.test.ctx_size_in = args.len() as u32 * 8; + + let _ = { + let ret = unsafe { syscall(SYS_bpf, bpf_cmd::BPF_PROG_TEST_RUN, &mut attr, size_of::()) }; + + match ret { + 0.. => Ok(ret), + ret => Err((ret, io::Error::last_os_error())), + } + }.unwrap(); + + println!("{:?}", unsafe { attr.test }); + + let first = events.next().unwrap().to_vec(); + + for next in [first] { + println!("{next:?}"); + println!("{:?}", SysSigquitCall::try_from_raw(&*next)); + println!("{} {}", unsafe { gettid() }, unsafe { getpid() }); + } +} \ No newline at end of file diff --git a/rust/backend/ebpf/Cargo.toml b/rust/backend/ebpf/Cargo.toml index cb63ae50..809d403e 100644 --- a/rust/backend/ebpf/Cargo.toml +++ b/rust/backend/ebpf/Cargo.toml @@ -13,14 +13,16 @@ edition.workspace = true [dependencies] backend-common = { workspace = true } - aya-ebpf = { workspace = true } aya-log-ebpf = { workspace = true } +[features] +prog-test = [] + [build-dependencies] which = { workspace = true } xtask = { workspace = true } [[bin]] name = "backend-ebpf" -path = "src/main.rs" +path = "src/main.rs" \ No newline at end of file diff --git a/rust/backend/ebpf/README.md b/rust/backend/ebpf/README.md index 64038003..916a99ee 100644 --- a/rust/backend/ebpf/README.md +++ b/rust/backend/ebpf/README.md @@ -8,14 +8,15 @@ SPDX-License-Identifier: MIT # eBPF programs The entries in the maps are the structs defined in `../common/src/lib.rs`. -The maps `_PIDS` are HashMaps that store the pid as key and as value the duration for a call to be considered blocking in nanosec. +The maps `_PIDS` are HashMaps that store the pid as key and as value the duration for a call to be considered blocking in nanosec. +For the features `SIGQUIT` and `JNIReferences` the durations are irrelevant as they are not relevant for the use-cases. ## overview by hook name -| | type | functions to hook | map | -|---------------|------------|------------------------------------------------------------------------------|---------------------------------------------| -| vfs_write | KProbe | `vfs_write`, `vfs_write_ret` | `VFS_WRITE_EVENTS` | -| sendmsg | Tracepoint | `sys_enter_sendmsg`, `sys_exit_sendmsg` | `SYS_SENDMSG_CALLS` | -| SIGQUIT | Tracepoint | `sys_sigquit` | `SYS_SIGQUIT_CALLS` | -| JNIReferences | UProbe | `trace_add_local`, `trace_del_local`, `trace_add_global`, `trace_del_global` | `JNI_REF_CALLS`, `JNI_REF_PIDS` | -| ... | ... | ... | ... | +| | type | functions to hook | map | +|---------------|------------|------------------------------------------------------------------------------|-------------------------------------| +| vfs_write | KProbe | `vfs_write`, `vfs_write_ret` | `VFS_WRITE_EVENTS` | +| sendmsg | Tracepoint | `sys_enter_sendmsg`, `sys_exit_sendmsg` | `SYS_SENDMSG_CALLS` | +| SIGQUIT | Tracepoint | `sys_sigquit` | `SYS_SIGQUIT_CALLS` | +| JNIReferences | UProbe | `trace_add_local`, `trace_del_local`, `trace_add_global`, `trace_del_global` | `JNI_REF_CALLS` | +| ... | ... | ... | ... | diff --git a/rust/backend/ebpf/src/sys_sigquit.rs b/rust/backend/ebpf/src/sys_sigquit.rs index c843f095..30eca69c 100644 --- a/rust/backend/ebpf/src/sys_sigquit.rs +++ b/rust/backend/ebpf/src/sys_sigquit.rs @@ -2,16 +2,39 @@ // // SPDX-License-Identifier: MIT -use aya_ebpf::{macros::{tracepoint, map}, maps::{RingBuf}, programs::{TracePointContext}, EbpfContext, helpers::gen::bpf_ktime_get_ns}; +use aya_ebpf::{helpers::gen::bpf_ktime_get_ns, macros::map, maps::RingBuf, programs::TracePointContext, EbpfContext}; +use aya_ebpf::maps::HashMap; use aya_log_ebpf::error; -use backend_common::{SysSigquitCall}; +use backend_common::SysSigquitCall; + +#[map(name = "SYS_SIGQUIT_PIDS")] +static SYS_SIGQUIT_PIDS: HashMap = HashMap::pinned(4096, 0); #[map(name = "SYS_SIGQUIT_EVENTS")] pub static SYS_SIGQUIT_EVENTS: RingBuf = RingBuf::pinned(1024, 0); -#[tracepoint] -pub fn sys_sigquit(ctx: TracePointContext) -> u32 { - let pid = ctx.pid(); +// Disclaimer: +// We have to swap here, because BPF_PROG_TEST_RUN does not support Tracepoints +// For testing we can set the prog-test flag and interpret it as TracepointContext, because we can set whatever we want +// For an example see backend/daemon/src/prog_test_run.rs + +#[cfg(feature = "prog-test")] +type Arg = aya_ebpf::programs::RawTracePointContext; + +#[cfg(not(feature = "prog-test"))] +type Arg = aya_ebpf::programs::TracePointContext; + +#[cfg_attr(feature = "prog-test", aya_ebpf::macros::raw_tracepoint)] +#[cfg_attr(not(feature = "prog-test"), aya_ebpf::macros::tracepoint)] +pub fn sys_sigquit(ctx: Arg) -> u32 { + let ctx = TracePointContext::new(ctx.as_ptr()); + let pid = ctx.pid(); + + if unsafe { SYS_SIGQUIT_PIDS.get(&pid).is_none() } { + // ignore signals from this pid + return 0; + } + let tid = ctx.tgid(); let time_stamp: u64; diff --git a/rust/client/src/bin/cli.rs b/rust/client/src/bin/cli.rs index 5e4e609d..04359853 100644 --- a/rust/client/src/bin/cli.rs +++ b/rust/client/src/bin/cli.rs @@ -7,7 +7,7 @@ use clap::Parser; use clap::Subcommand; use client::Client; use client::ClientError; -use shared::config::{Configuration, SysSendmsgConfig, VfsWriteConfig, JniReferencesConfig}; +use shared::config::{Configuration, SysSendmsgConfig, VfsWriteConfig, JniReferencesConfig, SysSigquitConfig}; use std::collections::HashMap; use tokio_stream::StreamExt; @@ -91,6 +91,7 @@ async fn sendmsg(client: &mut Client, pid: u32) -> Result<()> { entries: HashMap::from([(pid, 0)]), }), jni_references: None, + sys_sigquit: Some(SysSigquitConfig { pids: vec![] }), }) .await?; @@ -114,6 +115,7 @@ async fn set_config(client: &mut Client) -> Result<()> { entries: std::collections::HashMap::new(), }), jni_references: Some(JniReferencesConfig { pids: vec![] }), + sys_sigquit: Some(SysSigquitConfig { pids: vec![] }), }) .await?; println!("Success"); diff --git a/rust/client/tests/base.rs b/rust/client/tests/base.rs index 4a6c5602..b6d740d1 100644 --- a/rust/client/tests/base.rs +++ b/rust/client/tests/base.rs @@ -5,7 +5,7 @@ // SPDX-License-Identifier: MIT use client::Client; -use shared::config::{Configuration, SysSendmsgConfig, VfsWriteConfig}; +use shared::config::{Configuration, SysSendmsgConfig, VfsWriteConfig, SysSigquitConfig}; use shared::ziofa::process::Cmd; // client tests assume daemon is running! @@ -55,6 +55,7 @@ async fn set_get_empty_config() { }), // jni_references: Some(JniReferencesConfig { pids: vec![] }), jni_references: None, + sys_sigquit: Some(SysSigquitConfig { pids: vec![] }), }; client diff --git a/rust/shared/build.rs b/rust/shared/build.rs index 14b2c133..b4c85014 100644 --- a/rust/shared/build.rs +++ b/rust/shared/build.rs @@ -22,12 +22,14 @@ static UNIFFI_RECORDS: LazyLock> = LazyLock::new(|| { "VfsWriteEvent", "SysSendmsgEvent", "JniReferencesEvent", + "SysSigquitEvent", "VfsWriteConfig", "SysSendmsgConfig", "JniReferencesConfig", "StringResponse", "Symbol", "SetConfigurationResponse", + "SysSigquitConfig", ] } else { vec![] diff --git a/rust/shared/proto/config.proto b/rust/shared/proto/config.proto index 74635dcb..d9c9efa0 100644 --- a/rust/shared/proto/config.proto +++ b/rust/shared/proto/config.proto @@ -21,7 +21,8 @@ message Configuration { optional VfsWriteConfig vfs_write = 1; optional SysSendmsgConfig sys_sendmsg = 2; optional JniReferencesConfig jniReferences = 3; - repeated UprobeConfig uprobes = 4; + optional SysSigquitConfig sys_sigquit = 4; + repeated UprobeConfig uprobes = 5; } message VfsWriteConfig { @@ -34,4 +35,8 @@ message SysSendmsgConfig { message JniReferencesConfig { repeated uint32 pids = 1; +} + +message SysSigquitConfig { + repeated uint32 pids = 1; } \ No newline at end of file diff --git a/rust/shared/proto/ziofa.proto b/rust/shared/proto/ziofa.proto index b8556b36..9bb0eb57 100644 --- a/rust/shared/proto/ziofa.proto +++ b/rust/shared/proto/ziofa.proto @@ -70,6 +70,7 @@ message Event { VfsWriteEvent vfs_write = 1; SysSendmsgEvent sys_sendmsg = 2; JniReferencesEvent jni_references = 3; + SysSigquitEvent sys_sigquit = 4; } } @@ -103,3 +104,9 @@ message JniReferencesEvent { JniMethodName jni_method_name = 4; } +message SysSigquitEvent { + uint32 pid = 1; + uint32 tid = 2; + uint64 time_stamp = 3; + uint64 target_pid = 4; +} \ No newline at end of file