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