diff --git a/frontend/app/build.gradle.kts b/frontend/app/build.gradle.kts index 39c022b5..72e93def 100644 --- a/frontend/app/build.gradle.kts +++ b/frontend/app/build.gradle.kts @@ -100,6 +100,7 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + implementation(libs.accompanist.drawablepainter) implementation(project(":client")) diff --git a/frontend/app/src/main/AndroidManifest.xml b/frontend/app/src/main/AndroidManifest.xml index 3b0bc750..b6364120 100644 --- a/frontend/app/src/main/AndroidManifest.xml +++ b/frontend/app/src/main/AndroidManifest.xml @@ -9,6 +9,10 @@ SPDX-License-Identifier: MIT + + + { get().packageManager } + single { PackageInformationProvider(get()) } single { RustClientFactory("http://[::1]:50051") } single { ConfigurationManager(clientFactory = get()) } binds arrayOf(ConfigurationAccess::class, ProcessListAccess::class) viewModel { ConfigurationViewModel(configurationAccess = get()) } - viewModel { ProcessesViewModel(processListAccess = get()) } + viewModel { + ProcessesViewModel(processListAccess = get(), packageInformationProvider = get()) + } viewModel { VisualizationViewModel(clientFactory = get()) } } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/api/InstalledPackageInfo.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/api/InstalledPackageInfo.kt new file mode 100644 index 00000000..3f681253 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/api/InstalledPackageInfo.kt @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.api + +import android.graphics.drawable.Drawable + +data class InstalledPackageInfo(val displayName: String, val icon: Drawable) diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/bl/PackageInformationProvider.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/bl/PackageInformationProvider.kt new file mode 100644 index 00000000..93ec3663 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/bl/PackageInformationProvider.kt @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.bl + +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import de.amosproj3.ziofa.api.InstalledPackageInfo +import timber.log.Timber + +class PackageInformationProvider(private val packageManager: PackageManager) { + + private val installedPackages: Map by lazy { + packageManager.getInstalledPackages(PackageManager.GET_META_DATA).associateBy { + it.packageName + } + } + + /** + * Returns the [InstalledPackageInfo] or null if: + * - an error occurred + * - the application info was not found + * - the package name was not found in the installed packages. + */ + fun getPackageInfo(packageName: String): InstalledPackageInfo? { + return try { + installedPackages[packageName]?.applicationInfo?.let { + val displayName = packageManager.getApplicationLabel(it).toString() + val appIcon: Drawable = packageManager.getApplicationIcon(it) + InstalledPackageInfo(displayName, appIcon) + } + } catch (e: Exception) { + Timber.w(e.stackTraceToString()) + return null + } + } +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/Routes.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/Routes.kt index 3be0a513..94d1156b 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/Routes.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/Routes.kt @@ -7,8 +7,9 @@ package de.amosproj3.ziofa.ui /** Routes for the main navigation */ enum class Routes { Visualize, - Configuration, + IndividualConfiguration, About, Home, Processes, + Configuration, } 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 ebe4ea7c..6fa0d964 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 @@ -4,6 +4,7 @@ package de.amosproj3.ziofa.ui +import android.net.Uri import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally @@ -14,17 +15,28 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.NavController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import de.amosproj3.ziofa.ui.about.AboutScreen import de.amosproj3.ziofa.ui.configuration.ConfigurationScreen +import de.amosproj3.ziofa.ui.navigation.ConfigurationMenu import de.amosproj3.ziofa.ui.navigation.HomeScreen import de.amosproj3.ziofa.ui.navigation.composables.ZiofaTopBar +import de.amosproj3.ziofa.ui.processes.ProcessListEntry import de.amosproj3.ziofa.ui.processes.ProcessesScreen +import de.amosproj3.ziofa.ui.shared.deserializePIDs +import de.amosproj3.ziofa.ui.shared.getDisplayName +import de.amosproj3.ziofa.ui.shared.serializePIDs +import de.amosproj3.ziofa.ui.shared.validPIDsOrNull import de.amosproj3.ziofa.ui.visualization.VisualizationScreen +val GLOBAL_CONFIGURATION_ROUTE = + "${Routes.IndividualConfiguration.name}?displayName=${Uri.encode("all processes")}?pids=-1" + /** Main application composable. All calls to [NavController] should happen here. */ @Composable fun ZIOFAApp() { @@ -42,18 +54,42 @@ fun ZIOFAApp() { toVisualize = { navController.navigate(Routes.Visualize.name) }, toConfiguration = { navController.navigate(Routes.Configuration.name) }, toAbout = { navController.navigate(Routes.About.name) }, - toProcesses = { navController.navigate(Routes.Processes.name) }, modifier = Modifier.padding(innerPadding), ) } composable( Routes.Configuration.name, + popEnterTransition = { fadeIn() }, + enterTransition = { slideInHorizontally(initialOffsetX = { it }) + fadeIn() }, + exitTransition = { slideOutHorizontally(targetOffsetX = { it }) + fadeOut() }, + ) { + ConfigurationMenu( + Modifier.padding(innerPadding), + toPresets = { /*TODO*/ }, + toProcesses = { navController.navigate(Routes.Processes.name) }, + toGlobalConfiguration = { navController.navigate(GLOBAL_CONFIGURATION_ROUTE) }, + ) + } + composable( + "${Routes.IndividualConfiguration.name}?displayName={displayName}?pids={pids}", + arguments = + listOf( + navArgument("displayName") { + type = NavType.StringType + nullable = true + }, + navArgument("pids") { + type = NavType.StringType + nullable = true + }, + ), enterTransition = { slideInHorizontally(initialOffsetX = { it }) + fadeIn() }, exitTransition = { slideOutHorizontally(targetOffsetX = { it }) + fadeOut() }, ) { ConfigurationScreen( Modifier.padding(innerPadding), - onBack = { navController.backToHome() }, + onBack = { navController.popBackStack() }, + pids = it.arguments?.getString("pids")?.deserializePIDs()?.validPIDsOrNull(), ) } composable( @@ -73,10 +109,16 @@ fun ZIOFAApp() { composable( Routes.Processes.name, + popEnterTransition = { fadeIn() }, enterTransition = { slideInHorizontally(initialOffsetX = { it }) + fadeIn() }, exitTransition = { slideOutHorizontally(targetOffsetX = { it }) + fadeOut() }, ) { - ProcessesScreen(Modifier.padding(innerPadding)) + ProcessesScreen( + Modifier.padding(innerPadding), + onClickEdit = { + navController.navigate(it.toConfigurationScreenRouteForProcess()) + }, + ) } } } @@ -85,7 +127,10 @@ fun ZIOFAApp() { /** Top bar with a back button on all screens except for the home screen. */ @Composable fun DynamicTopBar(navController: NavController) { - navController.currentBackStackEntryAsState().value?.destination?.route?.let { currentRoute -> + val backStackEntry = navController.currentBackStackEntryAsState().value + val route = backStackEntry?.destination?.route?.split("?")?.getOrNull(0) + val displayName = backStackEntry?.arguments?.getString("displayName") + route?.let { currentRoute -> when (currentRoute) { Routes.Home.name -> { ZiofaTopBar( @@ -94,13 +139,22 @@ fun DynamicTopBar(navController: NavController) { ) } + Routes.IndividualConfiguration.name -> { + ZiofaTopBar( + screenName = "Configuration for $displayName", + onBack = { navController.popBackStack() }, + ) + } + else -> { - ZiofaTopBar(screenName = currentRoute, onBack = { navController.backToHome() }) + ZiofaTopBar(screenName = currentRoute, onBack = { navController.popBackStack() }) } } } } -fun NavController.backToHome() { - this.navigate(Routes.Home.name) { popUpTo(Routes.Home.name) { inclusive = false } } +fun ProcessListEntry.toConfigurationScreenRouteForProcess(): String { + val displayNameParam = Uri.encode(this.getDisplayName()) + val pidsParam = Uri.encode(this.serializePIDs()) + return "${Routes.IndividualConfiguration.name}?displayName=$displayNameParam?pids=$pidsParam" } 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 3a6ac544..fc503be1 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 @@ -28,7 +28,9 @@ fun ConfigurationScreen( modifier: Modifier = Modifier, viewModel: ConfigurationViewModel = koinViewModel(), onBack: () -> Unit = {}, + pids: IntArray? = null, ) { + Box(modifier = modifier.fillMaxSize()) { val screenState by remember { viewModel.configurationScreenState }.collectAsState() val configurationChangedByUser by remember { viewModel.changed }.collectAsState() @@ -47,7 +49,7 @@ fun ConfigurationScreen( if (configurationChangedByUser) { SubmitFab( modifier = Modifier.align(Alignment.BottomEnd), - onClick = { viewModel.configurationSubmitted() }, + onClick = { viewModel.configurationSubmitted(pids) }, ) } } 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 7aab9c79..2df199f9 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 @@ -58,7 +58,12 @@ class ConfigurationViewModel(val configurationAccess: ConfigurationAccess) : Vie _changed.update { true } } - fun configurationSubmitted() { + /** + * Submit the configuration to the backend. + * + * @param pids the affected Process IDs or null if the configuration should be set globally + */ + fun configurationSubmitted(pids: IntArray?) { viewModelScope.launch { configurationAccess.submitConfiguration(checkedOptions.value.toConfiguration()) } 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 new file mode 100644 index 00000000..6a65bd88 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/ConfigurationMenu.kt @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.navigation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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 + +@Composable +fun ConfigurationMenu( + modifier: Modifier = Modifier, + toPresets: () -> Unit, + toProcesses: () -> Unit, + toGlobalConfiguration: () -> Unit, +) { + + Box( + modifier = + modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 50.dp, vertical = 40.dp) + ) { + Column(modifier = Modifier.fillMaxWidth()) { + MenuOptions( + menuOptions = + listOf( + MenuOptionData( + title = "Presets", + logoEmoji = Emoji.Bookmarks.unicode, + onClick = toPresets, + ), + MenuOptionData( + title = "Global", + logoEmoji = Emoji.Globe.unicode, + onClick = toGlobalConfiguration, + ), + MenuOptionData( + title = "Per Process", + logoEmoji = Emoji.MagnifyingGlass.unicode, + onClick = toProcesses, + ), + ) + ) + } + } +} 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 980dcf6d..724af328 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 @@ -5,37 +5,20 @@ package de.amosproj3.ziofa.ui.navigation import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp - -data class MenuOptionData(val title: String, val logoEmoji: String, val onClick: () -> Unit) - -enum class Emoji(val unicode: String) { - Chart("\uD83D\uDCCA"), - Gear("⚙\uFE0F"), - MagnifyingGlass("\uD83D\uDD0E"), - Info("ℹ\uFE0F"), -} +import de.amosproj3.ziofa.ui.navigation.composables.MenuOptionData +import de.amosproj3.ziofa.ui.navigation.composables.MenuOptions +import de.amosproj3.ziofa.ui.navigation.data.Emoji /** Static home screen for navigation */ @Composable @@ -45,7 +28,6 @@ fun HomeScreen( toVisualize: () -> Unit = {}, toConfiguration: () -> Unit = {}, toAbout: () -> Unit = {}, - toProcesses: () -> Unit = {}, ) { Box( modifier = @@ -64,62 +46,9 @@ fun HomeScreen( Emoji.Gear.unicode, toConfiguration, ), - MenuOptionData( - title = "Processes", - Emoji.MagnifyingGlass.unicode, - toProcesses, - ), MenuOptionData(title = "About", Emoji.Info.unicode, toAbout), ) ) } } } - -@Composable -fun MenuOptions(modifier: Modifier = Modifier, menuOptions: List) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - ) { - menuOptions.forEach { - MenuCardWithIcon( - text = it.title, - emoji = it.logoEmoji, - onClick = it.onClick, - modifier = Modifier.weight(1f), - ) - } - } -} - -@Composable -fun MenuCardWithIcon( - text: String, - emoji: String, - modifier: Modifier = Modifier, - onClick: () -> Unit, -) { - val modifierForCards = - modifier.aspectRatio(1f).clickable { onClick() }.focusable().padding(horizontal = 10.dp) - - Card( - modifier = modifierForCards, - elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), - colors = CardDefaults.elevatedCardColors(), - ) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - emoji, - fontSize = TextUnit(120f, TextUnitType.Sp), - modifier = Modifier.padding(bottom = 20.dp), - ) - Text(text, fontSize = TextUnit(40f, TextUnitType.Sp)) - } - } -} 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 new file mode 100644 index 00000000..cdb44434 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/composables/MenuOptions.kt @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.navigation.composables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp + +data class MenuOptionData(val title: String, val logoEmoji: String, val onClick: () -> Unit) + +@Composable +fun MenuOptions(modifier: Modifier = Modifier, menuOptions: List) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + menuOptions.forEach { + MenuCardWithIcon( + text = it.title, + emoji = it.logoEmoji, + onClick = it.onClick, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +fun MenuCardWithIcon( + text: String, + emoji: String, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + val modifierForCards = + modifier.aspectRatio(1f).clickable { onClick() }.focusable().padding(horizontal = 10.dp) + + Card( + modifier = modifierForCards, + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), + colors = CardDefaults.elevatedCardColors(), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + emoji, + fontSize = TextUnit(120f, TextUnitType.Sp), + modifier = Modifier.padding(bottom = 20.dp), + ) + Text(text, fontSize = TextUnit(40f, TextUnitType.Sp)) + } + } +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/data/Emoji.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/data/Emoji.kt new file mode 100644 index 00000000..25cfc03d --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/navigation/data/Emoji.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.navigation.data + +enum class Emoji(val unicode: String) { + Chart("\uD83D\uDCCA"), + Gear("⚙\uFE0F"), + MagnifyingGlass("\uD83D\uDD0E"), + Info("ℹ\uFE0F"), + Globe("\uD83C\uDF10"), + Bookmarks("\uD83D\uDCD1"), +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessListEntry.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessListEntry.kt new file mode 100644 index 00000000..543975de --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessListEntry.kt @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.processes + +import de.amosproj3.ziofa.api.InstalledPackageInfo +import uniffi.shared.Process + +sealed class ProcessListEntry { + + data class ProcessEntry(val process: Process) : ProcessListEntry() + + data class ApplicationEntry( + val packageInfo: InstalledPackageInfo, + val processList: List, + ) : ProcessListEntry() +} 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 9e649e3c..b4fa28ef 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 @@ -9,9 +9,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -20,44 +22,61 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import de.amosproj3.ziofa.ui.processes.composables.EditButton +import de.amosproj3.ziofa.ui.processes.composables.IconAndName +import de.amosproj3.ziofa.ui.processes.composables.ProcessesHeader import org.koin.androidx.compose.koinViewModel -import uniffi.shared.Cmd @Composable -fun ProcessesScreen(modifier: Modifier, viewModel: ProcessesViewModel = koinViewModel()) { +fun ProcessesScreen( + modifier: Modifier, + viewModel: ProcessesViewModel = koinViewModel(), + onClickEdit: (ProcessListEntry) -> Unit, +) { Box(modifier = modifier.fillMaxSize()) { Column { - Row(modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp)) { - Text(text = "CMD", modifier = Modifier.weight(1f)) - Text(text = "State", modifier = Modifier.weight(1f)) - Text(text = "PID", modifier = Modifier.weight(1f)) - Text(text = "Parent PID", modifier = Modifier.weight(1f)) - } - val options by remember { viewModel.processesList }.collectAsState() + + ProcessesHeader() LazyColumn(modifier = Modifier.padding(horizontal = 20.dp).fillMaxSize()) { - items(options) { option -> - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.Top, - ) { - Text(text = option.cmd.toReadableString(), modifier = Modifier.weight(1f)) - Text(text = option.state, modifier = Modifier.weight(1f)) - Text(text = option.pid.toString(), modifier = Modifier.weight(1f)) - Text(text = option.ppid.toString(), modifier = Modifier.weight(1f)) - } - } + items(options) { option -> ProcessListRow(option, onClickEdit = onClickEdit) } } } } } -fun Cmd?.toReadableString(): String { - this?.let { - return when (this) { - is Cmd.Comm -> this.v1 - is Cmd.Cmdline -> this.v1.args.joinToString(" ") +@Composable +fun ProcessListRow( + option: ProcessListEntry, + onClickProcessInfo: (ProcessListEntry) -> Unit = + {}, // TODO implement modal with info about processes + onClickEdit: (ProcessListEntry) -> Unit = {}, +) { + Row( + modifier = Modifier.fillMaxSize().padding(vertical = 10.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + when (option) { + is ProcessListEntry.ProcessEntry -> { + IconAndName(option, modifier = Modifier.weight(1f)) + Text(text = option.process.pid.toString(), modifier = Modifier.weight(1f)) + Text(text = option.process.ppid.toString(), modifier = Modifier.weight(1f)) + } + + is ProcessListEntry.ApplicationEntry -> { + IconAndName(option, modifier = Modifier.weight(1f)) + Text( + text = option.processList.map { it.pid }.joinToString(","), + modifier = Modifier.weight(1f), + ) + Text( + text = option.processList.map { it.ppid }.toSet().joinToString(","), + modifier = Modifier.weight(1f), + ) + } } - } ?: return "null" + EditButton(Modifier.weight(1f), onClick = { onClickEdit(option) }) + } + HorizontalDivider(Modifier.height(5.dp)) } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessesViewModel.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessesViewModel.kt index b8411178..34696823 100644 --- a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessesViewModel.kt +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/ProcessesViewModel.kt @@ -7,13 +7,31 @@ package de.amosproj3.ziofa.ui.processes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.amosproj3.ziofa.api.ProcessListAccess +import de.amosproj3.ziofa.bl.PackageInformationProvider +import de.amosproj3.ziofa.ui.shared.toReadableString import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -class ProcessesViewModel(processListAccess: ProcessListAccess) : ViewModel() { +class ProcessesViewModel( + processListAccess: ProcessListAccess, + packageInformationProvider: PackageInformationProvider, +) : ViewModel() { val processesList = processListAccess.processesList - .map { sortKey -> sortKey.sortedBy { it.pid } } + .map { processList -> processList.groupBy { it.cmd.toReadableString() } } + .map { packageProcessMap -> + packageProcessMap.entries.map { + val packageNameOrOther = it.key + val processList = it.value + // TODO We probably should not retrieve the info of all packages everytime + packageInformationProvider.getPackageInfo(packageNameOrOther)?.let { + ProcessListEntry.ApplicationEntry(it, processList) + } ?: ProcessListEntry.ProcessEntry(processList[0]) + } + } + .map { uiEntryList -> + uiEntryList.sortedBy { if (it is ProcessListEntry.ApplicationEntry) -1 else 1 } + } .stateIn(viewModelScope, started = SharingStarted.Lazily, listOf()) } diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/composables/EditButton.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/composables/EditButton.kt new file mode 100644 index 00000000..3d64fd1d --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/composables/EditButton.kt @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.processes.composables + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun EditButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) { + Box(modifier = modifier.clickable { onClick() }) { + Image( + imageVector = Icons.Filled.Edit, + contentDescription = "", + modifier = Modifier.align(Alignment.CenterEnd), + ) + } +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/composables/IconAndName.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/composables/IconAndName.kt new file mode 100644 index 00000000..3fcac368 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/composables/IconAndName.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.processes.composables + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.accompanist.drawablepainter.rememberDrawablePainter +import de.amosproj3.ziofa.ui.processes.ProcessListEntry +import de.amosproj3.ziofa.ui.shared.toReadableString + +@Composable +fun IconAndName(option: ProcessListEntry.ApplicationEntry, modifier: Modifier = Modifier) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + val painter = rememberDrawablePainter(option.packageInfo.icon) + Image(painter = painter, contentDescription = "", modifier = Modifier.size(50.dp, 50.dp)) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = option.packageInfo.displayName) + } +} + +@Composable +fun IconAndName(option: ProcessListEntry.ProcessEntry, modifier: Modifier = Modifier) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Image( + imageVector = Icons.Filled.Info, + contentDescription = "", + modifier = Modifier.size(50.dp, 50.dp), + ) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = option.process.cmd.toReadableString(), modifier = modifier) + } +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/composables/ProcessesHeader.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/composables/ProcessesHeader.kt new file mode 100644 index 00000000..85e4008c --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/processes/composables/ProcessesHeader.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.processes.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ProcessesHeader() { + Row(modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp)) { + Text(text = "Name", modifier = Modifier.weight(1f)) + Text(text = "PID(s)", modifier = Modifier.weight(1f)) + Text(text = "Parent PID", modifier = Modifier.weight(1f)) + Text(text = "", modifier = Modifier.weight(1f)) + } + HorizontalDivider(Modifier.height(15.dp).background(MaterialTheme.colorScheme.primary)) +} diff --git a/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/Helpers.kt b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/Helpers.kt new file mode 100644 index 00000000..6dbbbe09 --- /dev/null +++ b/frontend/app/src/main/java/de/amosproj3/ziofa/ui/shared/Helpers.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT + +package de.amosproj3.ziofa.ui.shared + +import de.amosproj3.ziofa.ui.processes.ProcessListEntry +import uniffi.shared.Cmd + +fun Cmd?.toReadableString(): String { + this?.let { + return when (this) { + is Cmd.Comm -> this.v1 + is Cmd.Cmdline -> this.v1.args.joinToString(" ") + } + } ?: return "null" +} + +fun ProcessListEntry.getDisplayName(): String { + return when (this) { + is ProcessListEntry.ApplicationEntry -> this.packageInfo.displayName + is ProcessListEntry.ProcessEntry -> this.process.cmd.toReadableString() + } +} + +fun ProcessListEntry.serializePIDs(): String { + return when (this) { + is ProcessListEntry.ApplicationEntry -> this.processList.map { it.pid } + is ProcessListEntry.ProcessEntry -> listOf(this.process.pid) + }.joinToString(",") +} + +fun String.deserializePIDs(): IntArray { + return this.split(",").map { it.toInt() }.toIntArray() +} + +fun IntArray.validPIDsOrNull(): IntArray? { + if (this.contains(-1)) { + return null + } + return this +} diff --git a/frontend/gradle/libs.versions.toml b/frontend/gradle/libs.versions.toml index 76e3bd68..173c3822 100644 --- a/frontend/gradle/libs.versions.toml +++ b/frontend/gradle/libs.versions.toml @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: MIT [versions] +accompanistDrawablepainter = "0.36.0" activityCompose = "1.9.3" # @pin incompatible otherwise agp = "8.6.0" @@ -24,6 +25,7 @@ versioncatalogueupdate = "0.8.5" vico = "2.0.0-beta.2" [libraries] +accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanistDrawablepainter" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 656eab17..89c4111c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -149,6 +149,18 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compat" version = "0.2.4" @@ -269,6 +281,7 @@ dependencies = [ "object", "once_cell", "thiserror 1.0.68", + "tokio", ] [[package]] @@ -310,6 +323,20 @@ dependencies = [ "syn", ] +[[package]] +name = "aya-log" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b600d806c1d07d3b81ab5f4a2a95fd80f479a0d3f1d68f29064d660865f85f02" +dependencies = [ + "aya", + "aya-log-common", + "bytes", + "log", + "thiserror 1.0.68", + "tokio", +] + [[package]] name = "aya-log-common" version = "0.1.15" @@ -377,7 +404,9 @@ dependencies = [ name = "backend-daemon" version = "0.1.0" dependencies = [ + "async-broadcast", "aya", + "aya-log", "backend-common", "cargo_metadata 0.18.1", "clap", @@ -602,6 +631,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-error" version = "0.0.0" @@ -626,6 +664,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "either" version = "1.13.0" @@ -648,6 +692,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.1.1" @@ -1172,6 +1237,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "paste" version = "1.0.15" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index a20ffcd8..342f0ffd 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -48,6 +48,7 @@ backend-ebpf = { path = "./backend/ebpf" } tracing = { version = "0.1.40" } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } procfs = "0.17.0" +async-broadcast = "0.7.1" [profile.dev] panic = "abort" diff --git a/rust/backend/common/src/lib.rs b/rust/backend/common/src/lib.rs index 232e0ebe..00b273e1 100644 --- a/rust/backend/common/src/lib.rs +++ b/rust/backend/common/src/lib.rs @@ -7,25 +7,36 @@ // // SPDX-License-Identifier: MIT -#[derive(Debug, Copy, Clone)] -pub enum KProbeTypes { - Poll, - VfsWrite, -} +pub const TIME_LIMIT_NS: u64 = 1_000_000; #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct VfsWriteCall { - pid: u32, - tid: u32, - begin_time_stamp: u64, - fd: i32, - bytes_written: usize, + pub pid: u32, + pub tid: u32, + pub begin_time_stamp: u64, + pub fp: u64, + pub bytes_written: usize, } impl VfsWriteCall { - pub fn new(pid: u32, tid: u32, begin_time_stamp: u64, fd: i32, bytes_written: usize) -> Self { - Self { pid, tid, begin_time_stamp, fd, bytes_written} + pub fn new(pid: u32, tid: u32, begin_time_stamp: u64, fp: u64, bytes_written: usize) -> Self { + Self { pid, tid, begin_time_stamp, fp, bytes_written} + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct SysSendmsgCall { + pub pid: u32, + pub tid: u32, + pub begin_time_stamp: u64, + pub fd: i32, +} + +impl SysSendmsgCall { + pub fn new(pid: u32, tid: u32, begin_time_stamp: u64, fd: i32) -> Self { + Self { pid, tid, begin_time_stamp, fd} } } @@ -35,6 +46,4 @@ pub fn generate_id(pid: u32, tgid: u32) -> u64{ let tgid_u64 = tgid as u64; (pid_u64 << 32) | tgid_u64 -} - - +} \ No newline at end of file diff --git a/rust/backend/daemon/Cargo.toml b/rust/backend/daemon/Cargo.toml index 63a532dd..9579d860 100644 --- a/rust/backend/daemon/Cargo.toml +++ b/rust/backend/daemon/Cargo.toml @@ -25,6 +25,8 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } procfs = { workspace = true } clap = { workspace = true, features = ["derive"] } +aya-log = { workspace = true } +async-broadcast = { workspace = true } [build-dependencies] cargo_metadata = { workspace = true } @@ -36,4 +38,4 @@ path = "./src/bin/cli.rs" [[test]] name = "base" -path = "./tests/base.rs" \ No newline at end of file +path = "./tests/base.rs" diff --git a/rust/backend/daemon/src/collector.rs b/rust/backend/daemon/src/collector.rs new file mode 100644 index 00000000..bdc7431d --- /dev/null +++ b/rust/backend/daemon/src/collector.rs @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2024 Felix Hilgers +// +// SPDX-License-Identifier: MIT + +use async_broadcast::Sender; +use aya::Ebpf; +use aya::maps::{MapData, MapError, RingBuf}; +use tokio::io::unix::AsyncFd; +use tokio::select; +use tonic::Status; +use tracing::error; +use backend_common::VfsWriteCall; +use shared::ziofa::{Event, VfsWriteEvent}; +use shared::ziofa::event::{EventData}; + +pub struct VfsWriteCollector { + map: AsyncFd> +} + +impl VfsWriteCollector { + pub fn from_ebpf(ebpf: &mut Ebpf) -> Result { + let map: RingBuf<_> = ebpf.take_map("VFS_WRITE_MAP") + .ok_or(MapError::InvalidName { name: "VFS_WRITE_MAP".to_string() })? + .try_into()?; + + let map = AsyncFd::new(map)?; + + Ok(Self { map }) + } + + pub async fn collect(&mut self, tx: Sender>, mut shutdown: tokio::sync::oneshot::Receiver<()>) -> Result<(), std::io::Error> { + loop { + select! { + handle = self.map.readable_mut() => { + let mut handle = handle?; + let rb = handle.get_inner_mut(); + + while let Some(item) = rb.next() { + let data = unsafe { &*(item.as_ptr() as *const VfsWriteCall) }; + let event = Event { + event_data: Some(EventData::VfsWrite(VfsWriteEvent { + pid: data.pid, + tid: data.tid, + begin_time_stamp: data.begin_time_stamp, + fp: data.fp, + bytes_written: data.bytes_written as u64 + })) + }; + match tx.broadcast(Ok(event)).await { + Ok(_) => {}, + Err(async_broadcast::SendError(event)) => { + error!( + ?event, + "Failed to broadcast" + ); + } + } + } + handle.clear_ready(); + } + _ = &mut shutdown => { + break; + } + } + } + + Ok(()) + } +} + diff --git a/rust/backend/daemon/src/lib.rs b/rust/backend/daemon/src/lib.rs index 6d004543..eac05b53 100644 --- a/rust/backend/daemon/src/lib.rs +++ b/rust/backend/daemon/src/lib.rs @@ -11,6 +11,7 @@ mod procfs_utils; mod server; mod features; +mod collector; 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 ebaf135c..ba5e11d0 100644 --- a/rust/backend/daemon/src/main.rs +++ b/rust/backend/daemon/src/main.rs @@ -13,6 +13,7 @@ mod helpers; mod procfs_utils; mod server; mod features; +mod collector; #[tokio::main] async fn main() { diff --git a/rust/backend/daemon/src/server.rs b/rust/backend/daemon/src/server.rs index 6038c54a..0b1a0f5d 100644 --- a/rust/backend/daemon/src/server.rs +++ b/rust/backend/daemon/src/server.rs @@ -6,7 +6,10 @@ use std::{ops::DerefMut, sync::Arc}; +use async_broadcast::{broadcast, Receiver, Sender}; use aya::Ebpf; +use aya_log::EbpfLogger; +use tokio::join; use shared::{ config::Configuration, counter::counter_server::CounterServer, @@ -17,23 +20,37 @@ use shared::{ }; use tokio::sync::Mutex; use tonic::{transport::Server, Request, Response, Status}; - +use shared::ziofa::Event; use crate::{ configuration, constants, counter::Counter, ebpf_utils::{EbpfErrorWrapper, State}, procfs_utils::{list_processes, ProcErrorWrapper}, }; +use crate::collector::VfsWriteCollector; pub struct ZiofaImpl { // tx: Option>>, ebpf: Arc>, state: Arc>, + channel: Arc, } impl ZiofaImpl { - pub fn new(ebpf: Arc>, state: Arc>) -> ZiofaImpl { - ZiofaImpl { ebpf, state } + pub fn new(ebpf: Arc>, state: Arc>, channel: Arc) -> ZiofaImpl { + ZiofaImpl { ebpf, state, channel } + } +} + +pub struct Channel { + tx: Sender>, + rx: Receiver>, +} + +impl Channel { + pub fn new() -> Self { + let (tx, rx) = broadcast(8192); + Self { tx, rx } } } @@ -78,15 +95,14 @@ impl Ziofa for ZiofaImpl { Ok(Response::new(SetConfigurationResponse { response_type: 0 })) } - // type InitStreamStream = ReceiverStream>; - // fn init_stream( - // &self, - // _: Request<()>, - // ) -> Result, Status> { - // let (_tx, rx) = mpsc::channel(1); - // - // Ok(Response::new(Self::InitStreamStream::new(rx))) - // } + type InitStreamStream = Receiver>; + + async fn init_stream( + &self, + _: Request<()>, + ) -> Result, Status> { + Ok(Response::new(self.channel.rx.clone())) + } } pub async fn serve_forever() { @@ -96,17 +112,35 @@ pub async fn serve_forever() { ))) .unwrap(); + EbpfLogger::init(&mut ebpf).unwrap(); + + let mut collector = VfsWriteCollector::from_ebpf(&mut ebpf).unwrap(); + let channel = Arc::new(Channel::new()); + + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); + let event_tx = channel.tx.clone(); + let mut state = State::new(); state.init(&mut ebpf).expect("should work"); let ebpf = Arc::new(Mutex::new(ebpf)); let state = Arc::new(Mutex::new(state)); - let ziofa_server = ZiofaServer::new(ZiofaImpl::new(ebpf.clone(), state)); + let ziofa_server = ZiofaServer::new(ZiofaImpl::new(ebpf.clone(), state, channel)); let counter_server = CounterServer::new(Counter::new(ebpf).await); - Server::builder() - .add_service(ziofa_server) - .add_service(counter_server) - .serve(constants::sock_addr()) - .await - .unwrap(); + + let serve = async move { + Server::builder() + .add_service(ziofa_server) + .add_service(counter_server) + .serve(constants::sock_addr()) + .await + .unwrap(); + shutdown_tx.send(()).unwrap(); + }; + + let collect = async move { + collector.collect(event_tx, shutdown_rx).await.unwrap(); + }; + + let (_, _) = join!(serve, collect); } diff --git a/rust/backend/ebpf/README.md b/rust/backend/ebpf/README.md new file mode 100644 index 00000000..18f6c257 --- /dev/null +++ b/rust/backend/ebpf/README.md @@ -0,0 +1,18 @@ + + + +# eBPF programs + +The entries in the maps are the structs defined in `../common/src/lib.rs`. + +## overview by hook name + +| |type | functions to hook |map | +|-----------|-----------|---------------------------------------|-------------------| +|vfs_write |KProbe |`vfs_write`, `vfs_write_ret` |`VFS_WRITE_MAP` | +|sendmsg |Tracepoint |`sys_enter_sendmsg`, `sys_exit_sendmsg`|`SYS_SENDMSG_MAP` | +|... |... |... |... | diff --git a/rust/backend/ebpf/src/lib.rs b/rust/backend/ebpf/src/lib.rs index 3b6efc19..7146cd95 100644 --- a/rust/backend/ebpf/src/lib.rs +++ b/rust/backend/ebpf/src/lib.rs @@ -8,6 +8,5 @@ // This file exists to enable the library target. -mod vfs_tracing; - -pub use vfs_tracing::{vfs_write, VFS_WRITE_MAP}; +pub mod vfs_write; +pub mod sys_sendmsg; \ No newline at end of file diff --git a/rust/backend/ebpf/src/main.rs b/rust/backend/ebpf/src/main.rs index 29eafcd6..cc4174d2 100644 --- a/rust/backend/ebpf/src/main.rs +++ b/rust/backend/ebpf/src/main.rs @@ -14,7 +14,7 @@ use aya_ebpf::{ maps::{PerCpuArray, RingBuf}, programs::XdpContext, }; -pub use backend_ebpf::{vfs_write, VFS_WRITE_MAP}; +pub use backend_ebpf::{vfs_write, sys_sendmsg}; #[map(name = "COUNTER")] static PACKET_COUNTER: PerCpuArray = PerCpuArray::with_max_entries(1, 0); diff --git a/rust/backend/ebpf/src/sys_sendmsg.rs b/rust/backend/ebpf/src/sys_sendmsg.rs new file mode 100644 index 00000000..c52f8fdc --- /dev/null +++ b/rust/backend/ebpf/src/sys_sendmsg.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Tom Weisshuhn +// +// SPDX-License-Identifier: MIT + +use aya_ebpf::{macros::{tracepoint, map}, maps::{HashMap, RingBuf}, programs::{TracePointContext}, EbpfContext, helpers::gen::bpf_ktime_get_ns}; +use backend_common::{generate_id, SysSendmsgCall}; + +#[map(name = "SYS_SENDMSG_MAP")] +pub static SYS_SENDMSG_MAP: RingBuf = RingBuf::with_byte_size(1024, 0); + + +#[map] +static SYS_SENDMSG_TIMESTAMPS: HashMap = HashMap::with_max_entries(1024, 0); + + +struct SysSendmsgIntern { + begin_time_stamp: u64, + fd: i32, +} + +#[tracepoint] +pub fn sys_enter_sendmsg(ctx: TracePointContext) -> u32 { + let id = generate_id(ctx.pid(), ctx.tgid()); + + let begin_time_stamp; + let fd: i32; + unsafe { + begin_time_stamp = bpf_ktime_get_ns(); + fd = match ctx.read_at(16) { + Ok(arg) => arg, + Err(_) => return 1, + }; + } + + let data: SysSendmsgIntern = SysSendmsgIntern {begin_time_stamp, fd}; + + match SYS_SENDMSG_TIMESTAMPS.insert(&id, &data, 0) { + Ok(_) => 0, + Err(_) => 1, + } +} + + +#[tracepoint] +pub fn sys_exit_sendmsg(ctx: TracePointContext) -> u32 { + let pid = ctx.pid(); + let tgid = ctx.tgid(); + let call_id = generate_id(pid, tgid); + let data = match unsafe { SYS_SENDMSG_TIMESTAMPS.get(&call_id) } { + None => {return 1} + Some(entry) => {entry} + }; + + + let result_data = SysSendmsgCall::new(pid, tgid, data.begin_time_stamp, data.fd); + + let mut entry = match SYS_SENDMSG_MAP.reserve::(0) { + Some(entry) => entry, + None => return 1, + }; + + entry.write(result_data); + entry.submit(0); + + + 0 +} \ No newline at end of file diff --git a/rust/backend/ebpf/src/vfs_tracing.rs b/rust/backend/ebpf/src/vfs_write.rs similarity index 69% rename from rust/backend/ebpf/src/vfs_tracing.rs rename to rust/backend/ebpf/src/vfs_write.rs index 61a1747d..ca7ad1bd 100644 --- a/rust/backend/ebpf/src/vfs_tracing.rs +++ b/rust/backend/ebpf/src/vfs_write.rs @@ -2,10 +2,6 @@ // // SPDX-License-Identifier: MIT - - -const TIME_LIMIT_NS: u64 = 100_000_000; - use aya_ebpf::{ macros::{kprobe, map, kretprobe}, maps::{HashMap, RingBuf}, @@ -13,8 +9,8 @@ use aya_ebpf::{ EbpfContext, helpers::gen::bpf_ktime_get_ns, }; -use aya_log_ebpf::info; -use backend_common::{generate_id, VfsWriteCall}; +use aya_log_ebpf::error; +use backend_common::{generate_id, VfsWriteCall, TIME_LIMIT_NS}; @@ -28,18 +24,31 @@ static VFS_WRITE_TIMESTAMPS: HashMap = HashMap::with_max_en struct VfsWriteIntern { begin_time_stamp: u64, - fd: i32, + fp: u64, bytes_written: usize, } #[kprobe] pub fn vfs_write(ctx: ProbeContext) -> Result<(), u32> { let id = generate_id(ctx.pid(), ctx.tgid()); - let data = VfsWriteIntern { - begin_time_stamp: unsafe {bpf_ktime_get_ns()}, - fd: ctx.arg(0).unwrap_or(-1), - bytes_written: ctx.arg(2).unwrap_or(usize::MAX) as usize, - }; + + let begin_time_stamp: u64; + let fp: u64; + let bytes_written: usize; + unsafe { + begin_time_stamp = bpf_ktime_get_ns(); + fp = match ctx.arg(0) { + Some(arg) => arg, + None => return Err(0), + }; + bytes_written = match ctx.arg(2) { + Some(arg) => arg, + None => return Err(0), + }; + } + + + let data = VfsWriteIntern { begin_time_stamp, fp, bytes_written }; match VFS_WRITE_TIMESTAMPS.insert(&id, &data, 0) { Ok(_) => Ok(()), @@ -62,11 +71,14 @@ pub fn vfs_write_ret(ctx: RetProbeContext) -> Result<(), u32> { }; if probe_end - data.begin_time_stamp > TIME_LIMIT_NS { - let data = VfsWriteCall::new(pid, tgid, data.begin_time_stamp, data.fd, data.bytes_written); + let data = VfsWriteCall::new(pid, tgid, data.begin_time_stamp, data.fp, data.bytes_written); let mut entry = match VFS_WRITE_MAP.reserve::(0) { Some(entry) => entry, - None => return Err(0), + None => { + error!(&ctx, "could not reserve space in VFS_WRITE_MAP"); + return Err(0) + }, }; entry.write(data); diff --git a/rust/client/src/bin/cli.rs b/rust/client/src/bin/cli.rs index 7e7f7852..5d77d088 100644 --- a/rust/client/src/bin/cli.rs +++ b/rust/client/src/bin/cli.rs @@ -4,6 +4,7 @@ use clap::Parser; use client::{Client, ClientError}; +use shared::config::{Configuration, VfsWriteConfig}; use tokio::{join, select, signal::ctrl_c, sync::oneshot}; use tokio_stream::StreamExt; @@ -13,12 +14,7 @@ struct Cli { iface: String, } -#[tokio::main] -pub async fn main() -> anyhow::Result<()> { - let Cli { iface, .. } = Cli::parse(); - - let mut client = Client::connect("http://[::1]:50051".to_owned()).await?; - +pub async fn counter_cli(mut client: Client, iface: String) -> anyhow::Result<()> { if let Err(e) = client.load().await { println!("{e:?}"); } @@ -62,3 +58,25 @@ pub async fn main() -> anyhow::Result<()> { Ok(()) } + +#[tokio::main] +pub async fn main() -> anyhow::Result<()> { + let Cli { .. } = Cli::parse(); + + let mut client = Client::connect("http://[::1]:50051".to_owned()).await?; + + client + .set_configuration(Configuration { + uprobes: vec![], + vfs_write: Some(VfsWriteConfig { pids: vec![] }), + }) + .await?; + + let mut stream = client.init_stream().await?; + + while let Some(next) = stream.next().await { + println!("{next:?}"); + } + + Ok(()) +} diff --git a/rust/client/src/bindings.rs b/rust/client/src/bindings.rs index a115f65e..653244f8 100644 --- a/rust/client/src/bindings.rs +++ b/rust/client/src/bindings.rs @@ -7,6 +7,7 @@ use std::{pin::Pin, sync::Arc}; use shared::{config::Configuration, ziofa::Process}; use tokio::sync::Mutex; use tokio_stream::{Stream, StreamExt}; +use shared::ziofa::Event; type Result = core::result::Result; @@ -25,6 +26,21 @@ impl CountStream { } } +#[derive(uniffi::Object)] +struct EventStream(Mutex> + Send>>>); + +#[uniffi::export(async_runtime = "tokio")] +impl EventStream { + pub async fn next(&self) -> Result> { + let mut guard = self.0.lock().await; + match guard.next().await { + Some(Ok(x)) => Ok(Some(x)), + Some(Err(e)) => Err(e), + None => Ok(None), + } + } +} + #[derive(uniffi::Object)] struct Client(Mutex); @@ -68,14 +84,14 @@ impl Client { Ok(self.0.lock().await.stop_collecting().await?) } - pub async fn server_count(&self) -> Result> { + pub async fn server_count(&self) -> Result { let mut guard = self.0.lock().await; let stream = guard .server_count() .await? .map(|x| x.map_err(ClientError::from)); - Ok(Arc::new(CountStream(Mutex::new(Box::pin(stream))))) + Ok(CountStream(Mutex::new(Box::pin(stream)))) } pub async fn check_server(&self) -> Result<()> { @@ -93,4 +109,14 @@ impl Client { pub async fn set_configuration(&self, configuration: Configuration) -> Result<()> { Ok(self.0.lock().await.set_configuration(configuration).await?) } + + pub async fn init_stream(&self) -> Result { + let mut guard = self.0.lock().await; + let stream = guard + .init_stream() + .await? + .map(|x| x.map_err(ClientError::from)); + + Ok(EventStream(Mutex::new(Box::pin(stream)))) + } } diff --git a/rust/client/src/client.rs b/rust/client/src/client.rs index 06bb111c..8c472b5f 100644 --- a/rust/client/src/client.rs +++ b/rust/client/src/client.rs @@ -12,6 +12,7 @@ use tonic::{ transport::{Channel, Endpoint}, Request, }; +use shared::ziofa::Event; #[derive(Clone, Debug)] pub struct Client { @@ -101,4 +102,8 @@ impl Client { self.ziofa.set_configuration(configuration).await?; Ok(()) } + + pub async fn init_stream(&mut self) -> Result>> { + Ok(self.ziofa.init_stream(()).await?.into_inner().map(|s| Ok(s?))) + } } diff --git a/rust/shared/build.rs b/rust/shared/build.rs index f6c5609d..44d313e9 100644 --- a/rust/shared/build.rs +++ b/rust/shared/build.rs @@ -16,6 +16,9 @@ static UNIFFI_RECORDS: LazyLock> = LazyLock::new(|| { "Configuration", "EbpfEntry", "UprobeConfig", + "Event", + "VfsWriteEvent", + "VfsWriteConfig", ] } else { vec![] @@ -24,7 +27,7 @@ static UNIFFI_RECORDS: LazyLock> = LazyLock::new(|| { static UNIFFI_ENUMS: LazyLock> = LazyLock::new(|| { if cfg!(feature = "uniffi") { - vec!["Process.cmd"] + vec!["Process.cmd", "Event.event_data"] } else { vec![] } diff --git a/rust/shared/proto/ziofa.proto b/rust/shared/proto/ziofa.proto index 351c1d80..15278d40 100644 --- a/rust/shared/proto/ziofa.proto +++ b/rust/shared/proto/ziofa.proto @@ -16,7 +16,7 @@ service Ziofa { rpc GetConfiguration(google.protobuf.Empty) returns (config.Configuration){} rpc SetConfiguration(config.Configuration) returns (SetConfigurationResponse){} -// rpc InitStream(google.protobuf.Empty) returns (stream EbpfStreamObject) {} // all Responses genereated by the ebpf-programms are send via this stream + rpc InitStream(google.protobuf.Empty) returns (stream Event) {} // all Responses genereated by the ebpf-programms are send via this stream } @@ -47,15 +47,16 @@ message SetConfigurationResponse{ uint32 response_type = 1; } -message EbpfStreamObject{ - oneof concrete { - VfsWriteCall vfs_write_call = 1; +message Event { + oneof event_data { + VfsWriteEvent vfs_write = 1; } } -message VfsWriteCall { + +message VfsWriteEvent { uint32 pid = 1; uint32 tid = 2; uint64 begin_time_stamp = 3; - int32 fd = 4; - uint64 bytes_written = 5; // needs to be bits of target architecture + uint64 fp = 4; + uint64 bytes_written = 5; } \ No newline at end of file