-
-
Notifications
You must be signed in to change notification settings - Fork 351
feat: track installation outcome to improve pending install state #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6d309d5
610b252
6c443c9
a482aec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package zed.rainxch.details.data.system | ||
|
|
||
| import zed.rainxch.core.domain.logging.GitHubStoreLogger | ||
| import zed.rainxch.details.domain.repository.DetailsRepository | ||
| import zed.rainxch.details.domain.system.AttestationVerifier | ||
| import java.io.File | ||
| import java.io.FileInputStream | ||
| import java.security.MessageDigest | ||
|
Comment on lines
+6
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== JVM-specific imports under commonMain ==="
rg -n -g '**/commonMain/**/*.kt' '^import java\.'
echo
echo "=== JVM-style format calls under commonMain ==="
rg -n -g '**/commonMain/**/*.kt' '\.format\s*\('
echo
echo "=== AttestationVerifierImpl.kt ==="
fd 'AttestationVerifierImpl\.kt$' --exec sed -n '1,80p' {}Repository: OpenHub-Store/GitHub-Store Length of output: 4909 Move JVM-only digest implementation out of Lines 6-8 import JVM-specific APIs ( 🤖 Prompt for AI Agents |
||
|
|
||
| class AttestationVerifierImpl( | ||
| private val detailsRepository: DetailsRepository, | ||
| private val logger: GitHubStoreLogger, | ||
| ) : AttestationVerifier { | ||
| override suspend fun verify( | ||
| owner: String, | ||
| repoName: String, | ||
| filePath: String, | ||
| ): Boolean = | ||
| try { | ||
| val digest = computeSha256(filePath) | ||
| detailsRepository.checkAttestations(owner, repoName, digest) | ||
| } catch (e: Exception) { | ||
| logger.debug("Attestation check error: ${e.message}") | ||
| false | ||
| } | ||
|
|
||
| private fun computeSha256(filePath: String): String { | ||
| val digest = MessageDigest.getInstance("SHA-256") | ||
| val buffer = ByteArray(8192) | ||
| FileInputStream(File(filePath)).use { fis -> | ||
| var bytesRead: Int | ||
| while (fis.read(buffer).also { bytesRead = it } != -1) { | ||
| digest.update(buffer, 0, bytesRead) | ||
| } | ||
| } | ||
| return digest.digest().joinToString("") { "%02x".format(it) } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| package zed.rainxch.details.data.system | ||
|
|
||
| import kotlinx.coroutines.delay | ||
| import zed.rainxch.core.domain.logging.GitHubStoreLogger | ||
| import zed.rainxch.core.domain.model.ApkPackageInfo | ||
| import zed.rainxch.core.domain.model.InstallSource | ||
| import zed.rainxch.core.domain.model.InstalledApp | ||
| import zed.rainxch.core.domain.repository.FavouritesRepository | ||
| import zed.rainxch.core.domain.repository.InstalledAppsRepository | ||
| import zed.rainxch.core.domain.system.Installer | ||
| import zed.rainxch.details.domain.model.ApkValidationResult | ||
| import zed.rainxch.details.domain.model.FingerprintCheckResult | ||
| import zed.rainxch.details.domain.model.SaveInstalledAppParams | ||
| import zed.rainxch.details.domain.model.UpdateInstalledAppParams | ||
| import zed.rainxch.details.domain.system.InstallationManager | ||
| import kotlin.time.Clock.System | ||
| import kotlin.time.ExperimentalTime | ||
|
|
||
| class InstallationManagerImpl( | ||
| private val installer: Installer, | ||
| private val installedAppsRepository: InstalledAppsRepository, | ||
| private val favouritesRepository: FavouritesRepository, | ||
| private val logger: GitHubStoreLogger, | ||
| ) : InstallationManager { | ||
| override suspend fun validateApk( | ||
| filePath: String, | ||
| isUpdate: Boolean, | ||
| trackedPackageName: String?, | ||
| ): ApkValidationResult { | ||
| val apkInfo = installer.getApkInfoExtractor().extractPackageInfo(filePath) | ||
| ?: return ApkValidationResult.ExtractionFailed | ||
|
|
||
| if (isUpdate && trackedPackageName != null && apkInfo.packageName != trackedPackageName) { | ||
| return ApkValidationResult.PackageMismatch( | ||
| apkPackageName = apkInfo.packageName, | ||
| installedPackageName = trackedPackageName, | ||
| ) | ||
| } | ||
|
|
||
| return ApkValidationResult.Valid(apkInfo) | ||
| } | ||
|
|
||
| override suspend fun checkSigningFingerprint(apkInfo: ApkPackageInfo): FingerprintCheckResult { | ||
| val existingApp = | ||
| installedAppsRepository.getAppByPackage(apkInfo.packageName) | ||
| ?: return FingerprintCheckResult.Ok | ||
|
|
||
| val expectedFp = existingApp.signingFingerprint ?: return FingerprintCheckResult.Ok | ||
| val actualFp = apkInfo.signingFingerprint ?: return FingerprintCheckResult.Ok | ||
|
|
||
| return if (expectedFp == actualFp) { | ||
| FingerprintCheckResult.Ok | ||
| } else { | ||
| FingerprintCheckResult.Mismatch( | ||
| expectedFingerprint = expectedFp, | ||
| actualFingerprint = actualFp, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| @OptIn(ExperimentalTime::class) | ||
| override suspend fun saveNewInstalledApp(params: SaveInstalledAppParams): InstalledApp? = | ||
| try { | ||
| val apkInfo = params.apkInfo | ||
| val repo = params.repo | ||
|
|
||
| val installedApp = | ||
| InstalledApp( | ||
| packageName = apkInfo.packageName, | ||
| repoId = repo.id, | ||
| repoName = repo.name, | ||
| repoOwner = repo.owner.login, | ||
| repoOwnerAvatarUrl = repo.owner.avatarUrl, | ||
| repoDescription = repo.description, | ||
| primaryLanguage = repo.language, | ||
| repoUrl = repo.htmlUrl, | ||
| installedVersion = params.releaseTag, | ||
| installedAssetName = params.assetName, | ||
| installedAssetUrl = params.assetUrl, | ||
| latestVersion = params.releaseTag, | ||
| latestAssetName = params.assetName, | ||
| latestAssetUrl = params.assetUrl, | ||
| latestAssetSize = params.assetSize, | ||
| appName = apkInfo.appName, | ||
| installSource = InstallSource.THIS_APP, | ||
| installedAt = System.now().toEpochMilliseconds(), | ||
| lastCheckedAt = System.now().toEpochMilliseconds(), | ||
| lastUpdatedAt = System.now().toEpochMilliseconds(), | ||
| isUpdateAvailable = false, | ||
| updateCheckEnabled = true, | ||
| releaseNotes = "", | ||
| systemArchitecture = installer.detectSystemArchitecture().name, | ||
| fileExtension = params.assetName.substringAfterLast('.', ""), | ||
| isPendingInstall = params.isPendingInstall, | ||
| installedVersionName = apkInfo.versionName, | ||
| installedVersionCode = apkInfo.versionCode, | ||
| latestVersionName = apkInfo.versionName, | ||
| latestVersionCode = apkInfo.versionCode, | ||
| signingFingerprint = apkInfo.signingFingerprint, | ||
| ) | ||
|
|
||
| installedAppsRepository.saveInstalledApp(installedApp) | ||
|
|
||
| if (params.isFavourite) { | ||
| favouritesRepository.updateFavoriteInstallStatus( | ||
| repoId = repo.id, | ||
| installed = true, | ||
| packageName = apkInfo.packageName, | ||
| ) | ||
| } | ||
|
|
||
| delay(1000) | ||
| val reloaded = installedAppsRepository.getAppByPackage(apkInfo.packageName) | ||
| logger.debug("Successfully saved and reloaded app: ${reloaded?.packageName}") | ||
| reloaded | ||
| } catch (t: Throwable) { | ||
| logger.error("Failed to save installed app to database: ${t.message}") | ||
| t.printStackTrace() | ||
| null | ||
| } | ||
|
|
||
| override suspend fun updateInstalledAppVersion(params: UpdateInstalledAppParams) { | ||
| val packageName = params.apkInfo.packageName | ||
| installedAppsRepository.updateAppVersion( | ||
| packageName = packageName, | ||
| newTag = params.releaseTag, | ||
| newAssetName = params.assetName, | ||
| newAssetUrl = params.assetUrl, | ||
| newVersionName = params.apkInfo.versionName, | ||
| newVersionCode = params.apkInfo.versionCode, | ||
| signingFingerprint = params.apkInfo.signingFingerprint, | ||
| ) | ||
| installedAppsRepository.updatePendingStatus(packageName, params.isPendingInstall) | ||
|
Comment on lines
+122
to
+133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the pending flag in the same persistence operation.
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package zed.rainxch.details.domain.model | ||
|
|
||
| import zed.rainxch.core.domain.model.ApkPackageInfo | ||
|
|
||
| sealed interface ApkValidationResult { | ||
| /** APK is valid and ready to install. */ | ||
| data class Valid( | ||
| val apkInfo: ApkPackageInfo, | ||
| ) : ApkValidationResult | ||
|
|
||
| /** Could not extract package information from the APK. */ | ||
| data object ExtractionFailed : ApkValidationResult | ||
|
|
||
| /** Package name in the APK does not match the currently installed app. */ | ||
| data class PackageMismatch( | ||
| val apkPackageName: String, | ||
| val installedPackageName: String, | ||
| ) : ApkValidationResult | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package zed.rainxch.details.domain.model | ||
|
|
||
| sealed interface FingerprintCheckResult { | ||
| /** Fingerprint matches or no prior fingerprint is recorded. */ | ||
| data object Ok : FingerprintCheckResult | ||
|
|
||
| /** Signing key has changed compared to the previously installed version. */ | ||
| data class Mismatch( | ||
| val expectedFingerprint: String, | ||
| val actualFingerprint: String, | ||
| ) : FingerprintCheckResult | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package zed.rainxch.details.domain.model | ||
|
|
||
| import zed.rainxch.core.domain.model.ApkPackageInfo | ||
| import zed.rainxch.core.domain.model.GithubRepoSummary | ||
|
|
||
| data class SaveInstalledAppParams( | ||
| val repo: GithubRepoSummary, | ||
| val apkInfo: ApkPackageInfo, | ||
| val assetName: String, | ||
| val assetUrl: String, | ||
| val assetSize: Long, | ||
| val releaseTag: String, | ||
| val isPendingInstall: Boolean, | ||
| val isFavourite: Boolean, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return
COMPLETEDfor the synchronous desktop paths.This now always reports
DELEGATED_TO_SYSTEM, but several branches finish beforeinstall(...)returns:installAppImage(), successfulinstallDebPackage()/installRpmPackage()runs, and the successful.pkgflow that waits forinstaller. Any caller that relies onInstallOutcomewill misclassify those installs as pending. Thread the outcome back from the platform helpers instead of hard-coding it here.🤖 Prompt for AI Agents