diff --git a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Install.kt b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Install.kt index ce757db4..28e19c45 100644 --- a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Install.kt +++ b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Install.kt @@ -9,7 +9,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -29,11 +29,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.documentfile.provider.DocumentFile -import com.maxkeppeker.sheets.core.models.base.Header -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeler.sheets.list.ListDialog import com.maxkeppeler.sheets.list.models.ListOption -import com.maxkeppeler.sheets.list.models.ListSelection import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination @@ -43,6 +39,9 @@ import shirkneko.zako.sukisu.R import shirkneko.zako.sukisu.ui.component.DialogHandle import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog import shirkneko.zako.sukisu.ui.component.rememberCustomDialog +import shirkneko.zako.sukisu.ui.theme.ThemeConfig +import shirkneko.zako.sukisu.ui.theme.getCardColors +import shirkneko.zako.sukisu.ui.theme.getCardElevation import shirkneko.zako.sukisu.ui.util.* import shirkneko.zako.sukisu.utils.AssetsUtil import java.io.File @@ -500,33 +499,77 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle { val supportedKmi by produceState(initialValue = emptyList()) { value = getSupportedKmis() } - val options = supportedKmi.map { value -> + val listOptions = supportedKmi.map { value -> ListOption( - titleText = value + titleText = value, + subtitleText = null, + icon = null ) } - var selection by remember { mutableStateOf(null) } - Box( - modifier = Modifier.background(MaterialTheme.colorScheme.surface) - ) { - ListDialog( - state = rememberUseCaseState(visible = true, onFinishedRequest = { - onSelected(selection) - }, onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.select_kmi), - ), - selection = ListSelection.Single( - showRadioButtons = true, - options = options, - ) { _, option -> - selection = option.titleText - } - ) + var selection: String? = null + val cardColor = if (!ThemeConfig.useDynamicColor) { + ThemeConfig.currentTheme.ButtonContrast + } else { + MaterialTheme.colorScheme.secondaryContainer } + + AlertDialog( + onDismissRequest = { + dismiss() + }, + title = { + Text(text = stringResource(R.string.select_kmi)) + }, + text = { + Column { + listOptions.forEachIndexed { index, option -> + Row( + modifier = Modifier + .clickable { + selection = supportedKmi[index] + } + .padding(vertical = 8.dp) + ) { + Column { + Text(text = option.titleText) + option.subtitleText?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } + }, + confirmButton = { + TextButton( + onClick = { + if (selection != null) { + onSelected(selection) + } + dismiss() + } + ) { + Text(text = stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton( + onClick = { + dismiss() + } + ) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f), + shape = MaterialTheme.shapes.medium, + tonalElevation = getCardElevation() + ) } } diff --git a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Settings.kt b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Settings.kt index c17482ef..77cdeef8 100644 --- a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Settings.kt +++ b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/Settings.kt @@ -20,9 +20,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Undo import androidx.compose.material.icons.filled.* +import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -49,12 +51,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider -import com.maxkeppeker.sheets.core.models.base.Header +import androidx.core.content.edit import com.maxkeppeker.sheets.core.models.base.IconSource -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeler.sheets.list.ListDialog import com.maxkeppeler.sheets.list.models.ListOption -import com.maxkeppeler.sheets.list.models.ListSelection import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination @@ -75,14 +74,14 @@ import shirkneko.zako.sukisu.ui.component.SwitchItem import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog import shirkneko.zako.sukisu.ui.component.rememberCustomDialog import shirkneko.zako.sukisu.ui.component.rememberLoadingDialog +import shirkneko.zako.sukisu.ui.theme.CardConfig +import shirkneko.zako.sukisu.ui.theme.ThemeConfig +import shirkneko.zako.sukisu.ui.theme.getCardColors +import shirkneko.zako.sukisu.ui.theme.getCardElevation import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost import shirkneko.zako.sukisu.ui.util.getBugreportFile import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material3.MaterialTheme -import shirkneko.zako.sukisu.ui.theme.CardConfig -import androidx.core.content.edit /** @@ -111,7 +110,6 @@ fun SettingScreen(navigator: DestinationsNavigator) { AboutDialog(it) } val loadingDialog = rememberLoadingDialog() - val shrinkDialog = rememberConfirmDialog() // endregion Column( @@ -451,20 +449,73 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle { } var selection = UninstallType.NONE - ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = { - if (selection != UninstallType.NONE) { - onSelected(selection) - } - }, onCloseRequest = { - dismiss() - }), header = Header.Default( - title = stringResource(R.string.settings_uninstall), - ), selection = ListSelection.Single( - showRadioButtons = false, - options = listOptions, - ) { index, _ -> - selection = options[index] - }) + val cardColor = if (!ThemeConfig.useDynamicColor) { + ThemeConfig.currentTheme.ButtonContrast + } else { + MaterialTheme.colorScheme.secondaryContainer + } + + AlertDialog( + onDismissRequest = { + dismiss() + }, + title = { + Text(text = stringResource(R.string.settings_uninstall)) + }, + text = { + Column { + listOptions.forEachIndexed { index, option -> + Row( + modifier = Modifier + .clickable { + selection = options[index] + } + .padding(vertical = 8.dp) + ) { + Icon( + imageVector = options[index].icon, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp) + ) + Column { + Text(text = option.titleText) + option.subtitleText?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } + }, + confirmButton = { + androidx.compose.material3.TextButton( + onClick = { + if (selection != UninstallType.NONE) { + onSelected(selection) + } + dismiss() + } + ) { + Text(text = stringResource(android.R.string.ok)) + } + }, + dismissButton = { + androidx.compose.material3.TextButton( + onClick = { + dismiss() + } + ) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f), + shape = MaterialTheme.shapes.medium, + tonalElevation = getCardElevation() + ) } } diff --git a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt index 5165332a..d3c689ad 100644 --- a/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt +++ b/manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt @@ -29,7 +29,6 @@ import shirkneko.zako.sukisu.R import shirkneko.zako.sukisu.ui.component.ConfirmResult import shirkneko.zako.sukisu.ui.component.SearchAppBar import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog -import shirkneko.zako.sukisu.ui.component.rememberLoadingDialog import shirkneko.zako.sukisu.ui.theme.getCardColors import shirkneko.zako.sukisu.ui.theme.getCardElevation import shirkneko.zako.sukisu.ui.viewmodel.KpmViewModel @@ -38,6 +37,10 @@ import shirkneko.zako.sukisu.ui.util.unloadKpmModule import java.io.File import androidx.core.content.edit import shirkneko.zako.sukisu.ui.theme.ThemeConfig +import shirkneko.zako.sukisu.ui.component.rememberCustomDialog +import shirkneko.zako.sukisu.ui.component.ConfirmDialogHandle +import java.net.URLDecoder +import java.net.URLEncoder /** * KPM 管理界面 @@ -55,7 +58,6 @@ fun KpmScreen( val scope = rememberCoroutineScope() val snackBarHost = remember { SnackbarHostState() } val confirmDialog = rememberConfirmDialog() - val loadingDialog = rememberLoadingDialog() val cardColor = if (!ThemeConfig.useDynamicColor) { ThemeConfig.currentTheme.ButtonContrast } else { @@ -64,17 +66,86 @@ fun KpmScreen( val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val kpmInstall = stringResource(R.string.kpm_install) - val kpmInstallConfirm = stringResource(R.string.kpm_install_confirm) val kpmInstallSuccess = stringResource(R.string.kpm_install_success) val kpmInstallFailed = stringResource(R.string.kpm_install_failed) - val install = stringResource(R.string.install) val cancel = stringResource(R.string.cancel) - val kpmUninstall = stringResource(R.string.kpm_uninstall) - val kpmUninstallConfirmTemplate = stringResource(R.string.kpm_uninstall_confirm) val uninstall = stringResource(R.string.uninstall) + val FailedtoCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file) val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success) val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed) + val kpmInstallMode = stringResource(R.string.kpm_install_mode) + val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load) + val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed) + val kpmInstallModeDescription = stringResource(R.string.kpm_install_mode_description) + + var tempFileForInstall by remember { mutableStateOf(null) } + val installModeDialog = rememberCustomDialog { dismiss -> + AlertDialog( + onDismissRequest = { + dismiss() + tempFileForInstall?.delete() + tempFileForInstall = null + }, + title = { Text(kpmInstallMode) }, + text = { Text(kpmInstallModeDescription) }, + confirmButton = { + Column { + Button( + onClick = { + scope.launch { + dismiss() + tempFileForInstall?.let { tempFile -> + handleModuleInstall( + tempFile = tempFile, + isEmbed = false, + viewModel = viewModel, + snackBarHost = snackBarHost, + kpmInstallSuccess = kpmInstallSuccess, + kpmInstallFailed = kpmInstallFailed + ) + } + tempFileForInstall = null + } + } + ) { + Text(kpmInstallModeLoad) + } + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { + scope.launch { + dismiss() + tempFileForInstall?.let { tempFile -> + handleModuleInstall( + tempFile = tempFile, + isEmbed = true, + viewModel = viewModel, + snackBarHost = snackBarHost, + kpmInstallSuccess = kpmInstallSuccess, + kpmInstallFailed = kpmInstallFailed + ) + } + tempFileForInstall = null + } + } + ) { + Text(kpmInstallModeEmbed) + } + } + }, + dismissButton = { + TextButton( + onClick = { + dismiss() + tempFileForInstall?.delete() + tempFileForInstall = null + } + ) { + Text(cancel) + } + } + ) + } val selectPatchLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() @@ -84,8 +155,10 @@ fun KpmScreen( val uri = result.data?.data ?: return@rememberLauncherForActivityResult scope.launch { - // 复制文件到临时目录 - val tempFile = File(context.cacheDir, "temp_patch.kpm") + val fileName = uri.lastPathSegment ?: "unknown.kpm" + val encodedFileName = URLEncoder.encode(fileName, "UTF-8") + val tempFile = File(context.cacheDir, encodedFileName) + context.contentResolver.openInputStream(uri)?.use { input -> tempFile.outputStream().use { output -> input.copyTo(output) @@ -101,43 +174,8 @@ fun KpmScreen( return@launch } - val confirmResult = confirmDialog.awaitConfirm( - title = kpmInstall, - content = kpmInstallConfirm, - confirm = install, - dismiss = cancel - ) - - if (confirmResult == ConfirmResult.Confirmed) { - val success = loadingDialog.withLoading { - try { - val loadResult = loadKpmModule(tempFile.absolutePath) - if (true && loadResult.startsWith("Error")) { - Log.e("KsuCli", "Failed to load KPM module: $loadResult") - false - } else { - true - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e) - false - } - } - - if (success) { - viewModel.fetchModuleList() - snackBarHost.showSnackbar( - message = kpmInstallSuccess, - duration = SnackbarDuration.Short - ) - } else { - snackBarHost.showSnackbar( - message = kpmInstallFailed, - duration = SnackbarDuration.Short - ) - } - } - tempFile.delete() + tempFileForInstall = tempFile + installModeDialog.show() } } @@ -234,46 +272,21 @@ fun KpmScreen( verticalArrangement = Arrangement.spacedBy(16.dp) ) { items(viewModel.moduleList) { module -> - val kpmUninstallConfirm = String.format(kpmUninstallConfirmTemplate, module.name) KpmModuleItem( module = module, onUninstall = { scope.launch { - val confirmResult = confirmDialog.awaitConfirm( - title = kpmUninstall, - content = kpmUninstallConfirm, - confirm = uninstall, - dismiss = cancel + handleModuleUninstall( + module = module, + viewModel = viewModel, + snackBarHost = snackBarHost, + kpmUninstallSuccess = kpmUninstallSuccess, + kpmUninstallFailed = kpmUninstallFailed, + FailedtoCheckModuleFile = FailedtoCheckModuleFile, + uninstall = uninstall, + cancel = cancel, + confirmDialog = confirmDialog ) - if (confirmResult == ConfirmResult.Confirmed) { - val success = loadingDialog.withLoading { - try { - val unloadResult = unloadKpmModule(module.id) - if (true && unloadResult.startsWith("Error")) { - Log.e("KsuCli", "Failed to unload KPM module: $unloadResult") - false - } else { - true - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e) - false - } - } - - if (success) { - viewModel.fetchModuleList() - snackBarHost.showSnackbar( - message = kpmUninstallSuccess, - duration = SnackbarDuration.Short - ) - } else { - snackBarHost.showSnackbar( - message = kpmUninstallFailed, - duration = SnackbarDuration.Short - ) - } - } } }, onControl = { @@ -287,6 +300,133 @@ fun KpmScreen( } } +private suspend fun handleModuleInstall( + tempFile: File, + isEmbed: Boolean, + viewModel: KpmViewModel, + snackBarHost: SnackbarHostState, + kpmInstallSuccess: String, + kpmInstallFailed: String +) { + val moduleId = extractModuleId(tempFile.name) + if (moduleId == null) { + Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}") + snackBarHost.showSnackbar( + message = kpmInstallFailed, + duration = SnackbarDuration.Short + ) + tempFile.delete() + return + } + + val targetPath = "/data/adb/kpm/$moduleId.kpm" + + try { + if (isEmbed) { + Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor() + Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor() + } + + val loadResult = loadKpmModule(tempFile.absolutePath) + if (loadResult.startsWith("Error")) { + Log.e("KsuCli", "Failed to load KPM module: $loadResult") + snackBarHost.showSnackbar( + message = kpmInstallFailed, + duration = SnackbarDuration.Short + ) + } else { + viewModel.fetchModuleList() + snackBarHost.showSnackbar( + message = kpmInstallSuccess, + duration = SnackbarDuration.Short + ) + } + } catch (e: Exception) { + Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e) + snackBarHost.showSnackbar( + message = kpmInstallFailed, + duration = SnackbarDuration.Short + ) + } + tempFile.delete() +} + +private fun extractModuleId(fileName: String): String? { + return try { + val decodedFileName = URLDecoder.decode(fileName, "UTF-8") + val pattern = "([^/]*?)\\.kpm$".toRegex() + val matchResult = pattern.find(decodedFileName) + matchResult?.groupValues?.get(1) + } catch (e: Exception) { + Log.e("KsuCli", "Failed to extract module ID: ${e.message}", e) + null + } +} + +private suspend fun handleModuleUninstall( + module: KpmViewModel.ModuleInfo, + viewModel: KpmViewModel, + snackBarHost: SnackbarHostState, + kpmUninstallSuccess: String, + kpmUninstallFailed: String, + FailedtoCheckModuleFile: String, + uninstall: String, + cancel: String, + confirmDialog: ConfirmDialogHandle +) { + val moduleFileName = "${module.id}.kpm" + val moduleFilePath = "/data/adb/kpm/$moduleFileName" + + val fileExists = try { + val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0 + result + } catch (e: Exception) { + Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e) + snackBarHost.showSnackbar( + message = FailedtoCheckModuleFile, + duration = SnackbarDuration.Short + ) + false + } + + val confirmResult = confirmDialog.awaitConfirm( + title = "将卸载以下kpm模块:\n$moduleFileName", + content = "The following kpm modules will be uninstalled:\n$moduleFileName", + confirm = uninstall, + dismiss = cancel + ) + + if (confirmResult == ConfirmResult.Confirmed) { + try { + val unloadResult = unloadKpmModule(module.id) + if (unloadResult.startsWith("Error")) { + Log.e("KsuCli", "Failed to unload KPM module: $unloadResult") + snackBarHost.showSnackbar( + message = kpmUninstallFailed, + duration = SnackbarDuration.Short + ) + return + } + + if (fileExists) { + Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor() + } + + viewModel.fetchModuleList() + snackBarHost.showSnackbar( + message = kpmUninstallSuccess, + duration = SnackbarDuration.Short + ) + } catch (e: Exception) { + Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e) + snackBarHost.showSnackbar( + message = kpmUninstallFailed, + duration = SnackbarDuration.Short + ) + } + } +} + @Composable private fun KpmModuleItem( module: KpmViewModel.ModuleInfo, diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index f024370d..4219da62 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -8,28 +8,34 @@ スーパーユーザー: %d モジュール: %d 非対応 - 現在、 KernelSU は GKI カーネルにのみ対応しています + カーネルの KernelSU ドライバが未検出です。カーネルが間違ってませんか? カーネル - アプリのバージョン + SuSFS: %s + SuSFS のバージョン + SuS SU + マネージャーのバージョン Fingerprint - SELinux の状態 - Disabled + SELinux のステータス + 無効 Enforcing Permissive 不明 スーパーユーザー - %s モジュールをオンにできませんでした - %s モジュールをオフにできませんでした + %s モジュールを ON にできませんでした + %s モジュールを OFF にできませんでした モジュールがインストールされていません モジュール + 並べ替え (アクション優先) + 並べ替え (最初に有効) アンインストール + 復元 インストール インストール 再起動 設定 - 通常の再起動 + ソフトリブート リカバリーへ再起動 - ブートローダー へ再起動 + ブートローダーへ再起動 ダウンロードモードへ再起動 EDL へ再起動 アプリについて @@ -37,21 +43,21 @@ %s はアンインストールされました %s をアンインストールできませんでした バージョン - 制作者 + 作者 更新 システムアプリを表示 システムアプリを非表示 ログを送信 セーフモード 再起動すると有効化されます - モジュールが Magisk との競合により利用できません! - KernelSU について + モジュールが Magisk との競合により利用できません! + KernelSU について学ぶ https://kernelsu.org/ja_JP/guide/what-is-kernelsu.html - KernelSU のインストール方法やモジュールの使い方はこちら + KernelSU のインストール方法やモジュールの使い方を学習できます 支援する - KernelSU はこれからもずっと無料でオープンソースです。寄付をして頂くことで、開発を支援していただけます。 - アプリのプロファイル - 既定 + KernelSU は今後も無料でオープンソースです。ですが、寄付をして頂けると開発者への貢献になります。 + %2$s チャンネルにご参加ください。]]> + デフォルト テンプレート カスタム プロファイル名 @@ -59,76 +65,201 @@ 継承 共通 分離 - モジュールのアンマウント グループ + ケーパビリティ SELinux コンテキスト + モジュールのアンマウント %s のアプリのプロファイルの更新をできませでした + 現在の KernelSU のバージョン %d は低すぎるため、マネージャーは正常に動作しません。バージョン %d 以上に更新してください! + デフォルトでモジュールのマウントを解除 + アプリプロファイルの「モジュールのアンマウント」の共通のデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。 + Kprobe フックを非表示にする + KSU によって生成された Kprobe フックを無効化して、代替となる組み込みの非 Kprobe を有効化します。Kprobe をサポートしない 非 GKI カーネルに適用される同等の機能を実装します。 + このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。 ドメイン ルール - 新しいバージョン %s が利用可能です。タップしてダウンロード。 - アップデート + 更新 + モジュールをダウンロード中: %s ダウンロードを開始: %s + 新しいバージョン %s が利用可能です。タップしてダウンロード。 起動 強制停止 再起動 SELinux ルールの更新に失敗しました %s - ケーパビリティ - モジュールをダウンロード中: %s - このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。 - 既定でモジュールのマウントを解除 - アプリプロファイルの「モジュールのアンマウント」の共通のデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。 変更履歴 - インポート成功 - クリップボードからエクスポート - エクスポートするローカル テンプレートが見つかりません! - テンプレート ID はすでに存在します! - クリップボードからインポート - 変更ログの取得に失敗しました: %s - 名前 - 無効なテンプレート ID - オンラインテンプレートの同期 + アプリプロファイルのテンプレート + アプリプロファイルのローカルおよびオンラインテンプレートを管理する テンプレートの作成 - 読み取り専用 - インポート/エクスポート - テンプレートの保存に失敗しました テンプレートの編集 ID - アプリプロファイルのテンプレート + 無効なテンプレート ID + 名前 説明 保存 - アプリプロファイルのローカルおよびオンラインテンプレートを管理する 消去 - クリップボードが空です! テンプレートを表示 - アップデートを確認 - アプリを開いたときにアップデートを自動的に確認する + 読み取り専用 + テンプレート ID はすでに存在します! + インポートとエクスポート + クリップボードからインポート + クリップボードからエクスポート + エクスポートするローカル テンプレートが見つかりません! + インポートが成功しました + オンラインテンプレートの同期 + テンプレートの保存に失敗しました + クリップボードが空です! + 変更ログの取得に失敗しました: %s + 更新を確認 + アプリを開いたときに更新を自動的に確認します root の付与に失敗しました! + アクション 開く - WebView デバッグを有効にする + WebView デバッグを有効化する WebUI のデバッグに使用できます。必要な場合にのみ有効にしてください。 - %1$s パーティション イメージが推奨されます - KMI を選択してください - 次に + 直接インストール (推奨) + ファイルを選択してください 非アクティブなスロットにインストール (OTA 後) 再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。 \nこのオプションは、OTA が完了した後にのみ使用してください。 -\n続く? - 直接インストール (推奨) - ファイルを選択してください +\n続行しますか? + 次へ + %1$s のパーティションイメージを推奨します + KMI を選択してください + アンインストール + 一時的にアンインストールする 完全にアンインストールする ストックイメージを復元 - 一時的にアンインストールする - アンインストール KernelSU を一時的にアンインストールし、次回の再起動後に元の状態に戻します。 - KernelSU (ルートおよびすべてのモジュール) を完全かつ永久にアンインストールします。 + KernelSU (root およびすべてのモジュール) を完全かつ恒久的にアンインストールします。 バックアップが存在する場合、工場出荷時のイメージを復元できます (OTA の前に使用してください)。KernelSU をアンインストールする必要がある場合は、「完全にアンインストールする」を使用してください。 フラッシュ - フラッシュ成功 - フラッシュ失敗 + フラッシュが成功しました + フラッシュに失敗しました 選択された LKM: %s ログを保存 - アクション 保存されたログ - 並べ替え(最初に有効) - 並べ替え(アクション優先) + 対応 + 非対応 + 不明 + SuS SU モード: + + %1$s のモジュールをインストールしますか? + 不明なモジュール + + モジュールの復元を確認 + この操作によりモジュールが上書きされます。続行しますか? + 確認 + キャンセル + + バックアップが完了しました (tar.gz) + バックアップに失敗: %1$s + モジュールをバックアップ + モジュールを復元 + + モジュールは正常に復元されました、再起動が必要です + 復元に失敗: %1$s + 今すぐ再起動 + 不明なエラー + + コマンドの実行に失敗しました: %1$s + + 許可リストのバックアップが成功しました + 許可リストのバックアップに失敗: %1$s + 許可リストの復元を確認 + この操作により許可リストが上書きされます。続行しますか? + 許可リストの復元が成功しました + 許可リストの復元に失敗: %1$s + 許可リストをバックアップ + 許可リストを復元 + カスタム背景を設定 + カスタム背景を設定します + カードの管理 + カードのアルファ + デフォルトに復元 + Android のバージョン + デバイスモデル + %s にスーパーユーザー権限を付与することはできません + su の互換性を無効化する + su コマンドを使用してアプリが root 権限を取得する動作を一時的に無効化します (既存の root プロセスは影響を受けません)。 + SukiSU Beta Manager を使用中です + 選択した %d 個のモジュールをインストールしてもよろしいですか? + %1$d 個のモジュールをインストールしてもよろしいですか?\n\n%2$s + その他の設定 + SELinux + 有効 + 無効 + シンプルモード + ON にすると不要なカードを隠します + カーネルのバージョンを非表示にする + カーネルのバージョンを非表示にします + その他の情報を非表示にする + ホームページ上のスーパーユーザー、モジュール、KPM モジュールの数に関する情報を隠します。 + SuSFS ステータスを非表示にする + ホームページ上の SuSFS ステータス情報を隠します + テーマモード + システムに従う + ライトカラー + ダークカラー + 手動でフック + ダイナミックカラー + システムテーマのダイナミックカラーを使用します + テーマカラーを選択 + ホワイト + ブルー + グリーン + パープル + オレンジ + ピンク + グレー + アイボリー + ブラシの設定 + フラッシュするファイルを選択 + AnyKernel3 をフラッシュ + root 権限が必要です + ファイルのコピーに失敗しました + スクラブが完了しました + すぐに再起動しますか? + はい + いいえ + 再起動に失敗しました + bulk ライセンス + 認証を一括でキャンセル + バックアップ + イエロー + KPM モジュール + カーネルモジュール + カーネルモジュールは現在インストールされていません + リリース + 作者 + アンインストール + アンインストールに失敗しました + アンインストールに失敗しました + インストールを選択 + KPM モジュールの読み込みに成功しました + KPM モジュールの読み込みに失敗しました + KPM パラメーター + 完全 + KPM のバージョン + 閉じる + 以下のカーネルモジュール関数は KernelPatch によって開発され、SukiSU Ultra のカーネルモジュール関数を含むように変更されました + SukiSU Ultra の今後にご期待ください + 成功 + 失敗 + SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献に繋がるでしょう! + 非対応 + 対応 + KPM モジュール数:%d + 無効な KPM ファイル + カーネルはパッチされていません + カーネルは設定されていません + カスタム設定 + インストール + 読み込む + 埋め込む + モジュールのインストールモードを選択してください:\n\n読み込む: モジュールを一時的に読み込みます\n埋め込む: システムに恒久的にインストールします + モジュールファイルの存在を確認できませんでした + モジュールファイルが存在するか確認できません + アンインストールを確認 + アンインストール中 + 中止 diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index 3ea04e70..6d5c7e1c 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -1,2 +1,265 @@ - + + SukiSU Ultra + Home + Chưa cài đặt + Nhấn để cài đặt + Đang làm việc + Phiên bản: %d + Superusers: %d + Mô-đun: %d + Không được hỗ trợ + Không phát hiện được trình điều khiển KernelSU trên kernel của bạn. + Kernel + SuSFS: %s + Phiên bản SuSFS + SuS SU + Phiên bản quản lý + Dấu vân tay + Trạng thái SELinux + Disabled + Enforcing + Permissive + Không rõ + Superuser + Không thể bật mô-đun: %s + Không thể vô hiệu hóa mô-đun: %s + Không có mô-đun nào được cài đặt + Mô-đun + Sắp xếp (theo hành động) + Sắp xếp (theo trạng thái) + Gỡ cài đặt + Khôi phục + Cài đặt + Cài đặt + Khởi động lại + Cài đặt + Khởi động lại không gian người dùng + Khởi động lại vào Recovery + Khởi động lại vào Bootloader + Khởi động lại vào Download mode + Khởi động lại vào EDL + Thông tin + Bạn có chắc chắn muốn gỡ cài đặt mô-đun %s không? + %s đã gỡ cài đặt + Không thể gỡ cài đặt: %s + Phiên bản + Tác giả + Làm mới + Hiển thị ứng dụng hệ thống + Ẩn ứng dụng hệ thống + Gửi nhật ký + Chế độ an toàn + Khởi động lại để có hiệu lực + Các mô-đun không khả dụng do xung đột với Magisk! + Tìm hiểu KernelSU + https://kernelsu.org/guide/what-is-kernelsu.html + Tìm hiểu cách cài đặt KernelSU và sử dụng các mô-đun + Hỗ trợ chúng tôi + KernelSU sẽ luôn là miễn phí và mã nguồn mở. Tuy nhiên, bạn có thể cho chúng tôi thấy rằng bạn quan tâm bằng cách quyên góp. + Tham gia kênh %2$s của chúng tôi]]> + Hồ sơ ứng dụng + Mặc định + Bản mẫu + Tùy chỉnh + Tên hồ sơ + Tên không gian gắn kết + Được thừa hưởng + Toàn cầu + Cá nhân + Nhóm + Khả năng + Bối cảnh SELinux + Bỏ gắn kết mô-đun + Không cập nhật được Hồ sơ ứng dụng cho %s + Phiên bản KernelSU hiện tại %d quá thấp để trình quản lý hoạt động bình thường. Vui lòng nâng cấp lên phiên bản %d hoặc cao hơn! + Bỏ gắn kết các mô-đun theo mặc định + Giá trị mặc định cho \"Bỏ gắn kết các mô-đun\" trong Hồ sơ ứng dụng. Nếu được bật, nó sẽ xóa tất cả các sửa đổi của mô-đun đối với hệ thống đối và các ứng dụng không có hồ sơ được đặt. + Ẩn móc kprobe + Vô hiệu hóa các móc kprobe do KSU tạo ra và kích hoạt các móc không phải kprobe tích hợp sẵn, triển khai sẽ được áp dụng cho hạt nhân không phải GKI, không hỗ trợ krp + Bật tùy chọn này sẽ cho phép KernelSU khôi phục mọi tệp đã được các mô-đun sửa đổi cho ứng dụng này. + Domain + Quy tắc + Cập nhật + Đang tải xuống mô-đun: %s + Bắt đầu tải xuống: %s + Phiên bản mới %s đã có sẵn, hãy nhấp để cập nhật. + Khởi chạy + Buộc dừng + Khởi động lại + Không cập nhật được quy tắc SELinux cho %s + Nhật ký thay đổi + Mẫu hồ sơ ứng dụng + Quản lý mẫu cục bộ và trực tuyến của Hồ sơ ứng dụng + Tạo mẫu + Chỉnh sửa mẫu + ID + ID mẫu không hợp lệ + Tên + Miêu tả + Lưu + Xoá + Xem mẫu + Chỉ đọc + ID mẫu đã tồn tại! + Nhập/Xuất + Nhập từ bộ nhớ tạm clipboard + Xuất vào bộ nhớ tạm clipboard + Cannot find local template to export! + Đã nhập thành công + Đồng bộ mẫu trực tuyến + Không lưu được mẫu + Bộ nhớ tạm đang trống! + Lấy nhật ký thay đổi không thành công: %s + Kiểm tra cập nhật + Tự động kiểm tra cập nhật khi mở ứng dụng + Không cấp được quyền root! + Khởi chạy + Mở + Bật gỡ lỗi WebView + Có thể sử dụng để gỡ lỗi WebUI. Vui lòng chỉ bật khi cần thiết. + Cài đặt trực tiếp (Khuyến nghị) + Chọn một tập tin + Cài đặt vào khe không hoạt động (Sau OTA) + Thiết bị của bạn sẽ **BUỘC** phải khởi động vào khe cắm hiện tại không hoạt động sau khi khởi động lại!\nChỉ sử dụng tùy chọn này sau khi OTA hoàn tất.\nTiếp tục? + Kế tiếp + Phân vùng %1$s được khuyến nghị + Chọn KMI + Uninstall + Gỡ cài đặt tạm thời + Gỡ cài đặt vĩnh viễn + Khôi phục img ban đầu + Gỡ cài đặt tạm thời KernelSU, khôi phục lại trạng thái ban đầu sau lần khởi động lại tiếp theo. + Gỡ cài đặt KernelSU (Root và tất cả các mô-đun) hoàn toàn và vĩnh viễn. + Khôi phục ảnh gốc (nếu có bản sao lưu), thường được sử dụng trước OTA; nếu bạn cần gỡ cài đặt KernelSU, vui lòng sử dụng \"Gỡ cài đặt vĩnh viễn\". + Đang flash... + Flash thành công + Lỗi flash + LKM đã chọn: %s + Lưu nhật ký + Nhật ký đã lưu + Được hỗ trợ + Không được hỗ trợ + Không rõ + Chế độ SuS: SU + + Xác nhận cài đặt mô-đun %1$s ? + Mô-đun không xác định + + Xác nhận khôi phục mô-đun + Hành động này sẽ ghi đè lên tất cả các mô-đun hiện có. Tiếp tục? + Xác nhận + Hủy bỏ + + Sao lưu thành công (tar.gz) + Sao lưu không thành công: %1$s + Sao lưu mô-đun + Khôi phục các mô-đun + + Các mô-đun đã được khôi phục thành công, cần khởi động lại + Khôi phục không thành công: %1$s + Khởi động lại ngay + Lỗi không xác định + + Thực hiện lệnh không thành công: %1$s + + Sao lưu danh sách cho phép thành công + Sao lưu danh sách cho phép không thành công: %1$s + Xác nhận khôi phục danh sách cho phép + Hành động này sẽ ghi đè lên danh sách cho phép hiện tại. Tiếp tục? + Đã khôi phục danh sách cho phép thành công + Khôi phục danh sách cho phép không thành công: %1$s + Sao lưu danh sách cho phép + Khôi phục danh sách cho phép + Cài đặt nền tùy chỉnh + Cài đặt tùy chỉnh nền + Quản lý thẻ + Thẻ alpha + Khôi phục mặc định + Phiên bản Android + Model thiết bị + Không được phép cấp siêu người dùng cho %s + Vô hiệu hóa khả năng tương thích su + Tạm thời vô hiệu hóa mọi ứng dụng khỏi quyền root thông qua lệnh ⁠su (các tiến trình root hiện có sẽ không bị ảnh hưởng). + Bạn đang sử dụng trình quản lý SukiSU thử nghiệm + Bạn có chắc chắn muốn cài đặt các mô-đun %d đã chọn không? + Bạn có chắc chắn muốn cài đặt các mô-đun %1$d sau không? \n\n%2$s + Nhiều thiết lập hơn + SELinux + Đã bật + Đã tắt + Chế độ đơn giản + Ẩn các thẻ không cần thiết khi bật + Ẩn phiên bản kernel + Ẩn phiên bản kernel hiện tại + Ẩn thông tin thêm + Ẩn thông tin về số lượng app đã được cấp root, mô-đun và mô-đun KPM trên trang chủ + Ẩn trạng thái SuSFS + Ẩn thông tin trạng thái SuSFS trên trang chủ + Chế độ chủ đề + Theo hệ thống + Sáng + Tối + Móc thủ công + Màu sắc động + Màu sắc động sử dụng chủ đề hệ thống + Chọn màu chủ đề + Trắng + Xanh dương + Xanh lá + Tím + Cam + Hồng + Xám + Ngà voi + Tùy chọn cọ + Chọn tập tin cần flash + File Anykernel3 + Yêu cầu quyền root + Sao chép tập tin không thành công + Hoàn tất + khởi động lại ngay lập tức? + + Không + Khởi động lại không thành công + Giấy phép số lượng lớn + Hủy ủy quyền hàng loạt + Sao lưu + Vàng + Mô-đun kpm + Mô-đun kernel + Không có mô-đun kernel nào được cài đặt tại thời điểm này + Phát hành + Tác giả + Gỡ cài đặt + Đã gỡ cài đặt thành công + Không thể gỡ cài đặt + Chọn Cài đặt + Tải module kpm thành công + Tải module kpm không thành công + thông số kpm + fulfillment + Phiên bản KPM + đóng + Các chức năng mô-đun kernel sau đây được KernelPatch phát triển và sửa đổi để bao gồm các chức năng mô-đun hạt nhân của SukiSU Ultra + SukiSU Ultra Mong đợi + thành công + thất bại + SukiSU Ultra sẽ là một nhánh tương đối độc lập của KSU trong tương lai, nhưng xin cảm ơn KernelSU và MKSU chính thức vì những đóng góp của họ! + không được hỗ trợ + được hỗ trợ + Số lượng mô-đun KPM:%d + Tệp KPM không hợp lệ + Kernel chưa được vá + Kernel chưa được cấu hình + Cài đặt tùy chỉnh + cài đặt + tải + nhúng + Vui lòng chọn chế độ cài đặt mô-đun: \n\nTải: Tải tạm thời mô-đun \nNhúng: Cài đặt vĩnh viễn vào hệ thống + Không kiểm tra được sự tồn tại của tệp mô-đun + Không thể kiểm tra xem tệp mô-đun có tồn tại không + Xác nhận gỡ cài đặt + loại bỏ + bãi bỏ + diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 5551e9df..246e8314 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -230,11 +230,9 @@ 版本 作者 卸载 - 确定要卸载内核模块 %1$s 吗? 卸载成功 卸载失败 - 加载 kpm 模块 - 确认加载吗? + 选择安装 加载 kpm 模块成功 加载 kpm 模块失败 KPM 版本 @@ -252,4 +250,12 @@ SukiSU Ultra 展望 SukiSU Ultra 未来将会成为一个相对独立的 KSU 分支,但是依然感谢官方 KernelSU 和 MKSU 等做出的贡献 个性化设置 + 安装 + 加载 + 嵌入 + 请选择模块安装模式:\n\n加载:临时加载模块\n嵌入:永久安装到系统 + 无法检查模块文件是否存在 + 确认卸载 + 删除 + 取消 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index cf19487b..d17859e7 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -232,11 +232,9 @@ releases author uninstallation - Determine the kernel module to uninstall: %1$s ? Uninstalled successfully Failed to uninstall - Load the kpm module - Confirm Load? + Select Installation Load of kpm module successful Load of kpm module failed kpm parameters @@ -255,4 +253,13 @@ Kernel not patched Kernel not configured Custom settings + install + Load + embed + Please select the module installation mode: \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system + Failed to check module file existence + Unable to check if module file exists + Confirm uninstallation + removing + abolish diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index 5c1d1ab2..b307df45 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +notify = "6.1" anyhow = "1" clap = { version = "4", features = ["derive"] } const_format = "0.2" diff --git a/userspace/ksud/src/init_event.rs b/userspace/ksud/src/init_event.rs index dcd27fab..32bccedd 100644 --- a/userspace/ksud/src/init_event.rs +++ b/userspace/ksud/src/init_event.rs @@ -5,10 +5,13 @@ use anyhow::{Context, Result}; use log::{info, warn}; use rustix::fs::{MountFlags, mount}; use std::path::Path; +use crate::kpm; pub fn on_post_data_fs() -> Result<()> { ksucalls::report_post_fs_data(); + kpm::start_kpm_watcher()?; + utils::umask(0); #[cfg(unix)] @@ -98,6 +101,13 @@ pub fn on_post_data_fs() -> Result<()> { run_stage("post-mount", true); + for entry in std::fs::read_dir(kpm::KPM_DIR)? { + let path = entry?.path(); + if path.extension().is_some_and(|ext| ext == "kpm") { + let _ = kpm::load_kpm(&path); + } + } + Ok(()) } diff --git a/userspace/ksud/src/kpm.rs b/userspace/ksud/src/kpm.rs new file mode 100644 index 00000000..cc0b0f4c --- /dev/null +++ b/userspace/ksud/src/kpm.rs @@ -0,0 +1,122 @@ +use anyhow::Result; +use notify::{Watcher, RecursiveMode}; +use std::path::Path; +use std::fs; +use anyhow::anyhow; + +pub const KPM_DIR: &str = "/data/adb/kpm"; +pub const KPMMGR_PATH: &str = "/data/adb/ksu/bin/kpmmgr"; + + +pub fn ensure_kpm_dir() -> Result<()> { + if !Path::new(KPM_DIR).exists() { + fs::create_dir_all(KPM_DIR)?; + } + Ok(()) +} + +pub fn start_kpm_watcher() -> Result<()> { + ensure_kpm_dir()?; + load_existing_kpms()?; + + // 检查是否处于安全模式 + if crate::utils::is_safe_mode() { + log::warn!("The system is in safe mode and is deleting all KPM modules..."); + if let Err(e) = remove_all_kpms() { + log::error!("Error deleting all KPM modules: {}", e); + } + return Ok(()); + } + + let mut watcher = notify::recommended_watcher(|res| { + match res { + Ok(event) => handle_kpm_event(event), + Err(e) => log::error!("monitoring error: {:?}", e), + } + })?; + + watcher.watch(Path::new(KPM_DIR), RecursiveMode::NonRecursive)?; + Ok(()) +} + +pub fn handle_kpm_event(event: notify::Event) { + match event.kind { + notify::EventKind::Create(_) => { + event.paths.iter().for_each(|path| { + if path.extension().is_some_and(|ext| ext == "kpm") { + let _ = load_kpm(path); + } + }); + } + notify::EventKind::Remove(_) => { + event.paths.iter().for_each(|path| { + if let Some(name) = path.file_stem() { + let _ = unload_kpm(name.to_string_lossy().as_ref()); + } + }); + } + _ => {} + } +} + +pub fn load_kpm(path: &Path) -> Result<()> { + let status = std::process::Command::new(KPMMGR_PATH) + .args(["load", path.to_str().unwrap(), ""]) + .status()?; + + if status.success() { + log::info!("Loaded KPM: {}", path.display()); + } + Ok(()) +} + +pub fn unload_kpm(name: &str) -> Result<()> { + let status = std::process::Command::new(KPMMGR_PATH) + .args(["unload", name]) + .status() + .map_err(|e| anyhow!("Failed to execute kpmmgr: {}", e))?; + + let kpm_path = Path::new(KPM_DIR).join(format!("{}.kpm", name)); + if kpm_path.exists() { + fs::remove_file(&kpm_path) + .map_err(|e| anyhow!("Failed to delete KPM file: {}", e)) + .map(|_| log::info!("Deleted KPM file: {}", kpm_path.display()))?; + } + + if status.success() { + log::info!("Successfully unloaded KPM: {}", name); + } else { + log::warn!("KPM unloading may have failed: {}", name); + } + + Ok(()) +} + +pub fn remove_all_kpms() -> Result<()> { + ensure_kpm_dir()?; + + for entry in fs::read_dir(KPM_DIR)? { + let path = entry?.path(); + if path.extension().is_some_and(|ext| ext == "kpm") { + if let Some(name) = path.file_stem() { + unload_kpm(name.to_string_lossy().as_ref()) + .unwrap_or_else(|e| log::error!("Failed to remove KPM: {}", e)); + let _ = fs::remove_file(&path); + } + } + } + Ok(()) +} + +// 加载所有现有的 KPM 模块 +pub fn load_existing_kpms() -> Result<()> { + ensure_kpm_dir()?; + + for entry in fs::read_dir(KPM_DIR)? { + let path = entry?.path(); + if path.extension().map_or(false, |ext| ext == "kpm") { + let _ = load_kpm(&path); + } + } + Ok(()) +} \ No newline at end of file diff --git a/userspace/ksud/src/main.rs b/userspace/ksud/src/main.rs index a91fc0cb..e0543d73 100644 --- a/userspace/ksud/src/main.rs +++ b/userspace/ksud/src/main.rs @@ -14,6 +14,7 @@ mod restorecon; mod sepolicy; mod su; mod utils; +mod kpm; fn main() -> anyhow::Result<()> { cli::run()