use hardware requirements to display conflicts and prevent install (#2700)

* use hardware requirements to display conflicts and prevent install

* better messaging and also consider OS compatibility

* wip: backend hw requirements

* update backend components

* migration

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2024-10-29 13:48:03 -06:00
committed by GitHub
parent e1a91a7e53
commit 1be9cdae67
35 changed files with 725 additions and 500 deletions

View File

@@ -27,8 +27,8 @@
}
.published {
margin: 0;
padding: 4px 0 12px 0;
margin: 0px;
padding: 8px 0 8px 0;
font-style: italic;
}

View File

@@ -6,15 +6,19 @@ import { Pipe, PipeTransform } from '@angular/core'
})
export class ConvertBytesPipe implements PipeTransform {
transform(bytes: number): string {
if (bytes === 0) return '0 Bytes'
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
return convertBytes(bytes)
}
}
export function convertBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes'
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
@Pipe({
name: 'durationToSeconds',
})

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { VersionRange, ExtendedVersion, Version } from '@start9labs/start-sdk'
import { ExtendedVersion, VersionRange } from '@start9labs/start-sdk'
@Injectable({
providedIn: 'root',
@@ -29,12 +29,8 @@ export class Exver {
}
}
compareOsVersion(current: string, other: string) {
return Version.parse(current).compare(Version.parse(other))
}
satisfies(version: string, range: string): boolean {
return VersionRange.parse(range).satisfiedBy(ExtendedVersion.parse(version))
return ExtendedVersion.parse(version).satisfies(VersionRange.parse(range))
}
getFlavor(version: string): string | null {

View File

@@ -7,7 +7,7 @@ import {
DiskBackupTarget,
} from 'src/app/services/api/api.types'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
import { Exver, getErrorMessage } from '@start9labs/shared'
import { getErrorMessage } from '@start9labs/shared'
import { Version } from '@start9labs/start-sdk'
@Injectable({
@@ -19,10 +19,7 @@ export class BackupService {
loading = true
loadingError: string | IonicSafeString = ''
constructor(
private readonly embassyApi: ApiService,
private readonly exver: Exver,
) {}
constructor(private readonly embassyApi: ApiService) {}
async getBackupTargets(): Promise<void> {
this.loading = true
@@ -58,15 +55,16 @@ export class BackupService {
hasAnyBackup(target: BackupTarget): boolean {
return Object.values(target.startOs).some(
s => this.exver.compareOsVersion(s.version, '0.3.6') !== 'less',
s => Version.parse(s.version).compare(Version.parse('0.3.6')) !== 'less',
)
}
hasThisBackup(target: BackupTarget, id: string): boolean {
return (
target.startOs[id] &&
this.exver.compareOsVersion(target.startOs[id].version, '0.3.6') !==
'less'
Version.parse(target.startOs[id].version).compare(
Version.parse('0.3.6'),
) !== 'less'
)
}
}

View File

@@ -1,24 +1,24 @@
import { Injectable } from '@angular/core'
import { endWith, Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { Exver } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { ConfigService } from '../../../services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { Version } from '@start9labs/start-sdk'
@Injectable({ providedIn: 'root' })
export class RefreshAlertService extends Observable<boolean> {
private readonly stream$ = this.patch.watch$('serverInfo', 'version').pipe(
map(
version =>
this.exver.compareOsVersion(this.config.version, version) !== 'equal',
Version.parse(this.config.version).compare(Version.parse(version)) !==
'equal',
),
endWith(false),
)
constructor(
private readonly patch: PatchDB<DataModel>,
private readonly exver: Exver,
private readonly config: ConfigService,
) {
super(subscriber => this.stream$.subscribe(subscriber))

View File

@@ -5,6 +5,7 @@ import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { Version } from '@start9labs/start-sdk'
export interface AppRecoverOption extends PackageBackupInfo {
id: string
@@ -34,7 +35,10 @@ export class ToOptionsPipe implements PipeTransform {
id,
installed: !!packageData[id],
checked: false,
newerOS: this.compare(packageBackups[id].osVersion),
newerOS:
Version.parse(packageBackups[id].osVersion).compare(
Version.parse(this.config.version),
) === 'greater',
}))
.sort((a, b) =>
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,
@@ -42,11 +46,4 @@ export class ToOptionsPipe implements PipeTransform {
),
)
}
private compare(version: string): boolean {
// checks to see if backup was made on a newer version of startOS
return (
this.exver.compareOsVersion(version, this.config.version) === 'greater'
)
}
}

View File

@@ -11,49 +11,51 @@
: 'View Installing'
}}
</ion-button>
<ng-container *ngIf="localPkg; else install">
<ng-container
*ngIf="
localPkg.stateInfo.state === 'installed' &&
(localPkg | toManifest) as manifest
"
>
<ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === -1"
expand="block"
color="success"
(click)="tryInstall()"
<ng-container *ngIf="!conflict">
<ng-container *ngIf="localPkg; else install">
<ng-container
*ngIf="
localPkg.stateInfo.state === 'installed' &&
(localPkg | toManifest) as manifest
"
>
Update
</ion-button>
<ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === 1"
expand="block"
color="warning"
(click)="tryInstall()"
>
Downgrade
</ion-button>
<ng-container *ngIf="showDevTools$ | async">
<ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === 0"
*ngIf="(manifest.version | compareExver : pkg.version) === -1"
expand="block"
color="success"
(click)="tryInstall()"
>
Reinstall
Update
</ion-button>
<ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === 1"
expand="block"
color="warning"
(click)="tryInstall()"
>
Downgrade
</ion-button>
<ng-container *ngIf="showDevTools$ | async">
<ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === 0"
expand="block"
color="success"
(click)="tryInstall()"
>
Reinstall
</ion-button>
</ng-container>
</ng-container>
</ng-container>
<ng-template #install>
<ion-button
expand="block"
[color]="localFlavor ? 'warning' : 'success'"
(click)="tryInstall()"
>
{{ localFlavor ? 'Switch' : 'Install' }}
</ion-button>
</ng-template>
</ng-container>
<ng-template #install>
<ion-button
expand="block"
[color]="localFlavor ? 'warning' : 'success'"
(click)="tryInstall()"
>
{{ localFlavor ? 'Switch' : 'Install' }}
</ion-button>
</ng-template>
</div>

View File

@@ -47,6 +47,9 @@ export class MarketplaceShowControlsComponent {
@Input()
localFlavor!: boolean
@Input()
conflict?: string | null
readonly showDevTools$ = this.ClientStorageService.showDevTools$
constructor(

View File

@@ -13,12 +13,20 @@
</ng-container>
<ng-template #show>
<marketplace-package [pkg]="pkg"></marketplace-package>
<marketplace-package [pkg]="pkg">
<ion-item *ngIf="conflict$ | async as conflict" color="warning" class="ion-margin-top">
<ion-icon slot="start" name="warning"></ion-icon>
<ion-label>
<h2 style="font-weight: 600" [innerHTML]="conflict"></h2>
</ion-label>
</ion-item>
</marketplace-package>
<marketplace-show-controls
[url]="url"
[pkg]="pkg"
[localPkg]="localPkg$ | async"
[localFlavor]="!!(localFlavor$ | async)"
[conflict]="conflict$ | async"
></marketplace-show-controls>
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>

View File

@@ -1,15 +1,24 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Exver, getPkgId } from '@start9labs/shared'
import { convertBytes, Exver, getPkgId } from '@start9labs/shared'
import {
AbstractMarketplaceService,
MarketplacePkg,
} from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client'
import { combineLatest, Observable } from 'rxjs'
import { filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators'
import {
filter,
first,
map,
pairwise,
shareReplay,
startWith,
switchMap,
} from 'rxjs/operators'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/util/get-package-data'
import { Version, VersionRange } from '@start9labs/start-sdk'
@Component({
selector: 'marketplace-show',
@@ -25,9 +34,10 @@ export class MarketplaceShowPage {
this.patch.watch$('packageData', this.pkgId).pipe(filter(Boolean)),
this.route.queryParamMap,
]).pipe(
map(([pkg, paramMap]) =>
this.exver.getFlavor(getManifest(pkg).version) === paramMap.get('flavor')
? pkg
map(([localPkg, paramMap]) =>
this.exver.getFlavor(getManifest(localPkg).version) ===
paramMap.get('flavor')
? localPkg
: null,
),
shareReplay({ bufferSize: 1, refCount: true }),
@@ -49,6 +59,83 @@ export class MarketplaceShowPage {
),
)
readonly conflict$: Observable<string> = combineLatest([
this.pkg$,
this.patch.watch$('packageData', this.pkgId).pipe(
map(pkg => getManifest(pkg).version),
pairwise(),
filter(([prev, curr]) => prev !== curr),
map(([_, curr]) => curr),
),
this.patch.watch$('serverInfo').pipe(first()),
]).pipe(
map(([pkg, localVersion, server]) => {
let conflicts: string[] = []
// OS version
if (
!Version.parse(pkg.osVersion).satisfies(
VersionRange.parse(server.packageVersionCompat),
)
) {
const compare = Version.parse(pkg.osVersion).compare(
Version.parse(server.version),
)
conflicts.push(
compare === 'greater'
? `Minimum StartOS version ${pkg.osVersion}. Detected ${server.version}`
: `Version ${pkg.version} is outdated and cannot run newer versions of StartOS`,
)
}
// package version
if (
localVersion &&
pkg.sourceVersion &&
!this.exver.satisfies(localVersion, pkg.sourceVersion)
) {
conflicts.push(
`Currently installed version ${localVersion} cannot be upgraded to version ${pkg.version}. Try installing an older version first.`,
)
}
const { arch, ram, device } = pkg.hardwareRequirements
// arch
if (arch && !arch.includes(server.arch)) {
conflicts.push(
`Arch ${server.arch} is not supported. Supported: ${arch.join(
', ',
)}.`,
)
}
// ram
if (ram && ram > server.ram) {
conflicts.push(
`Minimum ${convertBytes(
ram,
)} of RAM required, detected ${convertBytes(server.ram)}.`,
)
}
// devices
conflicts.concat(
device
.filter(d =>
server.devices.some(
sd =>
d.class === sd.class && !new RegExp(d.pattern).test(sd.product),
),
)
.map(d => d.patternDescription),
)
return conflicts.join(' ')
}),
shareReplay({ bufferSize: 1, refCount: true }),
)
readonly flavors$ = this.route.queryParamMap.pipe(
switchMap(paramMap =>
this.marketplaceService.getSelectedStore$().pipe(

View File

@@ -117,7 +117,7 @@ export module Mock {
assets: [],
volumes: ['main'],
hardwareRequirements: {
device: {},
device: [],
arch: null,
ram: null,
},
@@ -174,7 +174,7 @@ export module Mock {
assets: [],
volumes: ['main'],
hardwareRequirements: {
device: {},
device: [],
arch: null,
ram: null,
},
@@ -224,7 +224,7 @@ export module Mock {
assets: [],
volumes: ['main'],
hardwareRequirements: {
device: {},
device: [],
arch: null,
ram: null,
},
@@ -253,7 +253,7 @@ export module Mock {
'26.1.0:0.1.0': {
title: 'Bitcoin Core',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -286,7 +286,7 @@ export module Mock {
short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
},
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -329,7 +329,7 @@ export module Mock {
'26.1.0:0.1.0': {
title: 'Bitcoin Core',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -362,7 +362,7 @@ export module Mock {
short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
},
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -407,7 +407,7 @@ export module Mock {
'0.17.5:0': {
title: 'LND',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -463,7 +463,7 @@ export module Mock {
'0.17.4-beta:1.0-alpha': {
title: 'LND',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -521,7 +521,7 @@ export module Mock {
'0.3.2.6:0': {
title: 'Bitcoin Proxy',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
@@ -565,7 +565,7 @@ export module Mock {
'27.0.0:1.0.0': {
title: 'Bitcoin Core',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -598,7 +598,7 @@ export module Mock {
short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
},
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -641,7 +641,7 @@ export module Mock {
'0.18.0:0.0.1': {
title: 'LND',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -697,7 +697,7 @@ export module Mock {
'0.3.2.7:0': {
title: 'Bitcoin Proxy',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',

View File

@@ -60,7 +60,7 @@ export const mockPatchData: DataModel = {
// password is asdfasdf
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
versionCompat: '>=0.3.0 <=0.3.6',
packageVersionCompat: '>=0.3.0 <=0.3.6',
postInitMigrationTodos: [],
statusInfo: {
backupProgress: null,
@@ -83,6 +83,8 @@ export const mockPatchData: DataModel = {
selected: null,
lastRegion: null,
},
ram: 8 * 1024 * 1024 * 1024,
devices: [],
},
packageData: {
bitcoind: {

View File

@@ -6,7 +6,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDB } from 'patch-db-client'
import { getServerInfo } from 'src/app/util/get-server-info'
import { DataModel } from './patch-db/data-model'
import { Exver } from '@start9labs/shared'
import { Version } from '@start9labs/start-sdk'
@Injectable({
providedIn: 'root',
@@ -48,14 +48,14 @@ export class EOSService {
constructor(
private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>,
private readonly exver: Exver,
) {}
async loadEos(): Promise<void> {
const { version, id } = await getServerInfo(this.patch)
this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
const updateAvailable =
this.exver.compareOsVersion(this.osUpdate.version, version) === 'greater'
Version.parse(this.osUpdate.version).compare(Version.parse(version)) ===
'greater'
this.updateAvailable$.next(updateAvailable)
}
}