From 9981ee76017ae533d552e606a631fe1a4d42f481 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 3 Sep 2024 09:23:47 -0600 Subject: [PATCH] follow sideload progress (#2718) * follow sideload progress * small bugfix * shareReplay with no refcount false * don't wrap sideload progress in RPCResult * dont present toast --------- Co-authored-by: Aiden McClelland --- core/startos/src/install/mod.rs | 39 ++-- .../apps-routes/app-show/app-show.module.ts | 1 + .../ui/src/app/pages/init/init.service.ts | 4 +- .../src/app/pages/init/logs/logs.service.ts | 2 +- .../server-routes/sideload/sideload.module.ts | 2 + .../server-routes/sideload/sideload.page.html | 195 ++++++++++-------- .../server-routes/sideload/sideload.page.ts | 16 +- .../sideload/sideload.service.ts | 32 +++ .../app/services/api/embassy-api.service.ts | 4 +- .../services/api/embassy-live-api.service.ts | 7 +- .../services/api/embassy-mock-api.service.ts | 14 +- .../src/app/services/marketplace.service.ts | 7 +- .../app/services/patch-db/patch-db-source.ts | 2 +- 13 files changed, 200 insertions(+), 125 deletions(-) create mode 100644 web/projects/ui/src/app/pages/server-routes/sideload/sideload.service.ts diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 7a545a3aa..eefd6eb66 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -17,6 +17,7 @@ use rpc_toolkit::HandlerArgs; use rustyline_async::ReadlineEvent; use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; +use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; use tracing::instrument; use ts_rs::TS; @@ -188,7 +189,7 @@ pub async fn sideload( SideloadParams { session }: SideloadParams, ) -> Result { let (upload, file) = upload(&ctx, session.clone()).await?; - let (err_send, err_recv) = oneshot::channel(); + let (err_send, err_recv) = oneshot::channel::(); let progress = Guid::new(); let progress_tracker = FullProgressTracker::new(); let mut progress_listener = progress_tracker.stream(Some(Duration::from_millis(200))); @@ -202,12 +203,14 @@ pub async fn sideload( use axum::extract::ws::Message; async move { if let Err(e) = async { - type RpcResponse = rpc_toolkit::yajrc::RpcResponse::>; + type RpcResponse = rpc_toolkit::yajrc::RpcResponse< + GenericRpcMethod<&'static str, (), FullProgress>, + >; tokio::select! { res = async { while let Some(progress) = progress_listener.next().await { ws.send(Message::Text( - serde_json::to_string(&RpcResponse::from_result::(Ok(progress))) + serde_json::to_string(&progress) .with_kind(ErrorKind::Serialization)?, )) .await @@ -217,12 +220,8 @@ pub async fn sideload( } => res?, err = err_recv => { if let Ok(e) = err { - ws.send(Message::Text( - serde_json::to_string(&RpcResponse::from_result::(Err(e))) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; + ws.close_result(Err::<&str, _>(e.clone_output())).await?; + return Err(e) } } } @@ -260,7 +259,7 @@ pub async fn sideload( } .await { - let _ = err_send.send(RpcError::from(e.clone_output())); + let _ = err_send.send(e.clone_output()); tracing::error!("Error sideloading package: {e}"); tracing::debug!("{e:?}"); } @@ -407,19 +406,21 @@ pub async fn cli_install( let mut progress = FullProgress::new(); - type RpcResponse = rpc_toolkit::yajrc::RpcResponse< - GenericRpcMethod<&'static str, (), FullProgress>, - >; - loop { tokio::select! { msg = ws.next() => { if let Some(msg) = msg { - if let Message::Text(t) = msg.with_kind(ErrorKind::Network)? { - progress = - serde_json::from_str::(&t) - .with_kind(ErrorKind::Deserialization)?.result?; - bar.update(&progress); + match msg.with_kind(ErrorKind::Network)? { + Message::Text(t) => { + progress = + serde_json::from_str::(&t) + .with_kind(ErrorKind::Deserialization)?; + bar.update(&progress); + } + Message::Close(Some(c)) if c.code != CloseCode::Normal => { + return Err(Error::new(eyre!("{}", c.reason), ErrorKind::Network)) + } + _ => (), } } else { break; diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index 8db082f42..a3c6cc584 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -56,5 +56,6 @@ const routes: Routes = [ StatusComponentModule, SharedPipesModule, ], + exports: [AppShowProgressComponent], }) export class AppShowPageModule {} diff --git a/web/projects/ui/src/app/pages/init/init.service.ts b/web/projects/ui/src/app/pages/init/init.service.ts index c98c0b11c..c58e7777e 100644 --- a/web/projects/ui/src/app/pages/init/init.service.ts +++ b/web/projects/ui/src/app/pages/init/init.service.ts @@ -29,9 +29,7 @@ export class InitService extends Observable { from(this.api.initGetProgress()), ).pipe( switchMap(({ guid, progress }) => - this.api - .openWebsocket$(guid, {}) - .pipe(startWith(progress)), + this.api.openWebsocket$(guid).pipe(startWith(progress)), ), map(({ phases, overall }) => { return { diff --git a/web/projects/ui/src/app/pages/init/logs/logs.service.ts b/web/projects/ui/src/app/pages/init/logs/logs.service.ts index 2553e9872..7ff3cecd1 100644 --- a/web/projects/ui/src/app/pages/init/logs/logs.service.ts +++ b/web/projects/ui/src/app/pages/init/logs/logs.service.ts @@ -38,7 +38,7 @@ export class LogsService extends Observable { private readonly log$ = defer(() => this.api.initFollowLogs({ boot: 0 }), ).pipe( - switchMap(({ guid }) => this.api.openWebsocket$(guid, {})), + switchMap(({ guid }) => this.api.openWebsocket$(guid)), bufferTime(500), filter(logs => !!logs.length), map(convertAnsi), diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts index 27a3757dd..27574da83 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts @@ -5,6 +5,7 @@ import { SideloadPage } from './sideload.page' import { Routes, RouterModule } from '@angular/router' import { ExverPipesModule, SharedPipesModule } from '@start9labs/shared' import { DragNDropDirective } from './dnd.directive' +import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' const routes: Routes = [ { @@ -20,6 +21,7 @@ const routes: Routes = [ RouterModule.forChild(routes), SharedPipesModule, ExverPipesModule, + InstallingProgressPipeModule, ], declarations: [SideloadPage, DragNDropDirective], }) diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html index 0d413075e..abbb9128a 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html @@ -7,92 +7,121 @@ - + + + +

+ {{ phase.name }} + + : {{ progress }}% + +

+ +
+
+ -
- -

Upload .s9pk package file

-

- - Tip: switch to LAN for faster uploads. - -

- - - - -
- - -
-

- - - {{ uploadState?.message }} -

-
-
-
- - - -
-
- -

{{ toUpload.manifest.title }}

-

{{ toUpload.manifest.version }}

+ +
+ +

Upload .s9pk package file

+

+ + Tip: switch to LAN for faster uploads. + +

+ + + + +
+ + +
+

+ + + {{ uploadState?.message }} +

+
+
+
+ + + +
+
+ +

{{ toUpload.manifest.title }}

+

{{ toUpload.manifest.version }}

+
-
- - Try again - - - - Upload & Install + + Try again - -
+ + + Upload & Install + + +
+ diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts index b95c4599f..b5f684103 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts @@ -1,10 +1,12 @@ import { Component } from '@angular/core' -import { isPlatform, NavController } from '@ionic/angular' +import { isPlatform } from '@ionic/angular' import { ErrorService, LoadingService } from '@start9labs/shared' -import { S9pk, T } from '@start9labs/start-sdk' +import { S9pk } from '@start9labs/start-sdk' import cbor from 'cbor' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' +import { SideloadService } from './sideload.service' +import { firstValueFrom } from 'rxjs' interface Positions { [key: string]: [bigint, bigint] // [position, length] @@ -36,12 +38,14 @@ export class SideloadPage { message: string } + readonly progress$ = this.sideloadService.progress$ + constructor( private readonly loader: LoadingService, private readonly api: ApiService, - private readonly navCtrl: NavController, private readonly errorService: ErrorService, private readonly config: ConfigService, + private readonly sideloadService: SideloadService, ) {} handleFileDrop(e: any) { @@ -111,15 +115,15 @@ export class SideloadPage { } async handleUpload() { - const loader = this.loader.open('Uploading package').subscribe() + const loader = this.loader.open('Starting upload').subscribe() try { const res = await this.api.sideloadPackage() + this.sideloadService.followProgress(res.progress) this.api .uploadPackage(res.upload, this.toUpload.file!) .catch(e => console.error(e)) - - this.navCtrl.navigateRoot('/services') + await firstValueFrom(this.sideloadService.websocketConnected$) } catch (e: any) { this.errorService.handleError(e) } finally { diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.service.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.service.ts new file mode 100644 index 000000000..79f871bba --- /dev/null +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core' +import { T } from '@start9labs/start-sdk' +import { endWith, ReplaySubject, shareReplay, Subject, switchMap } from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' + +@Injectable({ + providedIn: 'root', +}) +export class SideloadService { + private readonly guid$ = new Subject() + + readonly websocketConnected$ = new ReplaySubject() + + readonly progress$ = this.guid$.pipe( + switchMap(guid => + this.api + .openWebsocket$(guid, { + openObserver: { + next: () => this.websocketConnected$.next(''), + }, + }) + .pipe(endWith(null)), + ), + shareReplay(1), + ) + + constructor(private readonly api: ApiService) {} + + followProgress(guid: string) { + this.guid$.next(guid) + } +} diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index acd6780d1..31e3ac86e 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -12,7 +12,7 @@ export abstract class ApiService { // http // for sideloading packages - abstract uploadPackage(guid: string, body: Blob): Promise + abstract uploadPackage(guid: string, body: Blob): Promise // for getting static files: ex icons, instructions, licenses abstract getStaticProxy( @@ -29,7 +29,7 @@ export abstract class ApiService { abstract openWebsocket$( guid: string, - config: RR.WebsocketConfig, + config?: RR.WebsocketConfig, ): Observable // state diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 324936e8d..48cf6edd3 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -43,12 +43,11 @@ export class LiveApiService extends ApiService { // for sideloading packages - async uploadPackage(guid: string, body: Blob): Promise { - return this.httpRequest({ + async uploadPackage(guid: string, body: Blob): Promise { + await this.httpRequest({ method: Method.POST, body, url: `/rest/rpc/${guid}`, - responseType: 'text', }) } @@ -86,7 +85,7 @@ export class LiveApiService extends ApiService { openWebsocket$( guid: string, - config: RR.WebsocketConfig, + config: RR.WebsocketConfig = {}, ): Observable { const { location } = this.document.defaultView! const protocol = location.protocol === 'http:' ? 'ws' : 'wss' diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index d44ef809a..a40f29a0a 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -81,9 +81,8 @@ export class MockApiService extends ApiService { .subscribe() } - async uploadPackage(guid: string, body: Blob): Promise { + async uploadPackage(guid: string, body: Blob): Promise { await pauseFor(2000) - return 'success' } async getStaticProxy( @@ -106,7 +105,7 @@ export class MockApiService extends ApiService { openWebsocket$( guid: string, - config: RR.WebsocketConfig, + config: RR.WebsocketConfig = {}, ): Observable { if (guid === 'db-guid') { return this.mockWsSource$.pipe( @@ -125,6 +124,11 @@ export class MockApiService extends ApiService { return from(this.initProgress()).pipe( startWith(PROGRESS), ) as Observable + } else if (guid === 'sideload-progress-guid') { + config.openObserver?.next(new Event('')) + return from(this.initProgress()).pipe( + startWith(PROGRESS), + ) as Observable } else { throw new Error('invalid guid type') } @@ -1079,8 +1083,8 @@ export class MockApiService extends ApiService { async sideloadPackage(): Promise { await pauseFor(2000) return { - upload: '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated - progress: '5120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated + upload: 'sideload-upload-guid', // no significance, randomly generated + progress: 'sideload-progress-guid', // no significance, randomly generated } } diff --git a/web/projects/ui/src/app/services/marketplace.service.ts b/web/projects/ui/src/app/services/marketplace.service.ts index bef380aea..bc2dcc7ab 100644 --- a/web/projects/ui/src/app/services/marketplace.service.ts +++ b/web/projects/ui/src/app/services/marketplace.service.ts @@ -285,7 +285,12 @@ export class MarketplaceService implements AbstractMarketplaceService { this.api.getRegistryPackage(url, id, version ? `=${version}` : null), ).pipe( map(pkgInfo => - this.convertToMarketplacePkg(id, version, flavor, pkgInfo), + this.convertToMarketplacePkg( + id, + version === '*' ? null : version, + flavor, + pkgInfo, + ), ), ) } diff --git a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts index 00ab3d43e..5d1f1ed51 100644 --- a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts +++ b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts @@ -33,7 +33,7 @@ export class PatchDbSource extends Observable[]> { private readonly stream$ = inject(AuthService).isVerified$.pipe( switchMap(verified => (verified ? this.api.subscribeToPatchDB({}) : EMPTY)), switchMap(({ dump, guid }) => - this.api.openWebsocket$(guid, {}).pipe( + this.api.openWebsocket$(guid).pipe( bufferTime(250), filter(revisions => !!revisions.length), startWith([dump]),