From 330d5a08af4602b98140b74990acf9710555cd1a Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 1 Jul 2021 16:18:49 -0600 Subject: [PATCH] better network monitoring --- ui/src/app/app.component.ts | 11 +- .../app/components/status/status.component.ts | 6 +- ui/src/app/models/patch-db/data-model.ts | 7 +- ui/src/app/models/patch-db/patch-db-model.ts | 18 +- .../apps-routes/app-list/app-list.page.html | 70 +++--- .../apps-routes/app-list/app-list.page.ts | 18 +- .../apps-routes/app-show/app-show.page.html | 210 +++++++++--------- .../apps-routes/app-show/app-show.page.ts | 17 +- ui/src/app/pipes/display-bulb.pipe.ts | 6 +- ui/src/app/pipes/status.pipe.ts | 5 +- ui/src/app/services/api/api.service.ts | 5 +- ui/src/app/services/api/mock-app-fixures.ts | 7 +- ui/src/app/services/connection.service.ts | 152 +++++++------ .../services/pkg-status-rendering.service.ts | 7 +- 14 files changed, 295 insertions(+), 244 deletions(-) diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 192e37282..06b49bb50 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -12,7 +12,7 @@ import { LoadingOptions } from '@ionic/core' import { PatchDbModel } from './models/patch-db/patch-db-model' import { HttpService } from './services/http.service' import { ServerStatus } from './models/patch-db/data-model' -import { ConnectionService } from './services/connection.service' +import { ConnectionFailure, ConnectionService } from './services/connection.service' @Component({ selector: 'app-root', @@ -83,6 +83,7 @@ export class AppComponent { this.http.authReqEnabled = true this.showMenu = true this.patch.start() + this.connectionService.start() // watch network this.watchConnection(auth) // watch router to highlight selected menu item @@ -95,6 +96,7 @@ export class AppComponent { } else if (auth === AuthState.UNVERIFIED) { this.http.authReqEnabled = false this.showMenu = false + this.connectionService.stop() this.patch.stop() this.storage.clear() this.router.navigate(['/login'], { replaceUrl: true }) @@ -107,13 +109,13 @@ export class AppComponent { } private watchConnection (auth: AuthState): void { - this.connectionService.monitor$() + this.connectionService.watch$() .pipe( distinctUntilChanged(), takeWhile(() => auth === AuthState.VERIFIED), ) - .subscribe(internet => { - if (!internet) { + .subscribe(connectionFailure => { + if (connectionFailure !== ConnectionFailure.None) { this.presentToastOffline() } else { if (this.offlineToast) { @@ -121,7 +123,6 @@ export class AppComponent { this.offlineToast = undefined } } - console.log('INTERNET CONNECTION', internet) }) } diff --git a/ui/src/app/components/status/status.component.ts b/ui/src/app/components/status/status.component.ts index 07783c7d6..7e91441c5 100644 --- a/ui/src/app/components/status/status.component.ts +++ b/ui/src/app/components/status/status.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core' import { PackageDataEntry } from 'src/app/models/patch-db/data-model' -import { ConnectionState } from 'src/app/services/connection.service' import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' +import { ConnectionStatus } from 'patch-db-client' @Component({ selector: 'status', @@ -10,7 +10,7 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' }) export class StatusComponent { @Input() pkg: PackageDataEntry - @Input() connection: ConnectionState + @Input() connected: boolean @Input() size?: 'small' | 'medium' | 'large' = 'large' @Input() style?: string = 'regular' @Input() weight?: string = 'normal' @@ -19,7 +19,7 @@ export class StatusComponent { showDots = false ngOnChanges () { - const { display, color, showDots } = renderPkgStatus(this.pkg, this.connection) + const { display, color, showDots } = renderPkgStatus(this.pkg) this.display = display this.color = color this.showDots = showDots diff --git a/ui/src/app/models/patch-db/data-model.ts b/ui/src/app/models/patch-db/data-model.ts index 6e8055cae..b3c814776 100644 --- a/ui/src/app/models/patch-db/data-model.ts +++ b/ui/src/app/models/patch-db/data-model.ts @@ -16,7 +16,8 @@ export interface ServerInfo { 'lan-address': URL 'tor-address': URL status: ServerStatus - registry: URL + 'package-registry': URL + 'system-registry': URL wifi: WiFiInfo 'unread-notification-count': number specs: { @@ -24,6 +25,10 @@ export interface ServerInfo { Disk: string Memory: string } + 'connection-addresses': { + tor: string[] + clearnet: string[] + } } export enum ServerStatus { diff --git a/ui/src/app/models/patch-db/patch-db-model.ts b/ui/src/app/models/patch-db/patch-db-model.ts index dfcbb62f8..8fd1f376b 100644 --- a/ui/src/app/models/patch-db/patch-db-model.ts +++ b/ui/src/app/models/patch-db/patch-db-model.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, InjectionToken } from '@angular/core' -import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client' +import { Bootstrapper, ConnectionStatus, PatchDB, Source, Store } from 'patch-db-client' import { Observable, of, Subscription } from 'rxjs' -import { catchError, debounceTime } from 'rxjs/operators' +import { catchError, debounceTime, map } from 'rxjs/operators' import { DataModel } from './data-model' export const BOOTSTRAPPER = new InjectionToken>('app.config') @@ -37,6 +37,7 @@ export class PatchDbModel { }, error: e => { console.error('patch-db-sync sub ERROR', e) + this.start() }, complete: () => { console.error('patch-db-sync sub COMPLETE') @@ -54,7 +55,18 @@ export class PatchDbModel { } } - watch$: Store < DataModel > ['watch$'] = (...args: (string | number)[]): Observable => { + connected$ (): Observable { + return this.patchDb.connectionStatus$ + .pipe( + map(status => status === ConnectionStatus.Connected), + ) + } + + connectionStatus$ (): Observable { + return this.patchDb.connectionStatus$.asObservable() + } + + watch$: Store ['watch$'] = (...args: (string | number)[]): Observable => { // console.log('WATCHING') return this.patchDb.store.watch$(...(args as [])).pipe( catchError(e => { diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.html b/ui/src/app/pages/apps-routes/app-list/app-list.page.html index 4f3de538e..6eef95aab 100644 --- a/ui/src/app/pages/apps-routes/app-list/app-list.page.html +++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.html @@ -8,45 +8,43 @@ - -
-
-

Welcome to your Embassy

-

Get started by installing your first service.

-
- - - Marketplace - +
+
+

Welcome to your Embassy

+

Get started by installing your first service.

+ + + Marketplace + +
- - - - - -
-
- -
+ + + + + +
+
+
- - icon - - - - - +
+ + icon + + + + + - - - {{ (pkg.value | manifest).title }} - -
-
-
-
-
- + + + {{ (pkg.value | manifest).title }} + + + + + + diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts index b5860eb33..cc329ebb0 100644 --- a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts +++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts @@ -3,6 +3,7 @@ import { ConfigService } from 'src/app/services/config.service' import { ConnectionService } from 'src/app/services/connection.service' import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model' import { PackageDataEntry } from 'src/app/models/patch-db/data-model' +import { Subscription } from 'rxjs' @Component({ selector: 'app-list', @@ -10,7 +11,9 @@ import { PackageDataEntry } from 'src/app/models/patch-db/data-model' styleUrls: ['./app-list.page.scss'], }) export class AppListPage { - pkgs: PackageDataEntry[] = [] + pkgs: { [id: string]: PackageDataEntry } = { } + connected: boolean + subs: Subscription[] = [] constructor ( private readonly config: ConfigService, @@ -18,6 +21,19 @@ export class AppListPage { public readonly patch: PatchDbModel, ) { } + ngOnInit () { + this.subs = [ + this.patch.watch$('package-data').subscribe(pkgs => { + this.pkgs = pkgs + }), + this.patch.connected$().subscribe(c => this.connected = c), + ] + } + + ngOnDestroy () { + this.subs.forEach(sub => sub.unsubscribe()) + } + launchUi (pkg: PackageDataEntry, event: Event): void { event.preventDefault() event.stopPropagation() diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/ui/src/app/pages/apps-routes/app-show/app-show.page.html index 872226d30..55da888ed 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -16,122 +16,120 @@ {{ error }} - - - -
- - - - - -
- - {{ manifest.title }} - - - {{ manifest.version | displayEmver }} - -
-
-
- -
- - - Configure - - - Stop - - - Fix - - - Start - -
- - - Launch Web Interface - + + +
+ + + + + +
+ + {{ manifest.title }} + + + {{ manifest.version | displayEmver }} + +
+
+
+ +
+ + + Configure + + + Stop + + + Fix + + + Start
+ + + Launch Web Interface + + +
- - - - - -
- -

- {{ button.title }} -
-
-
-
-
+ + + + + +
+ +

+ {{ button.title }} +
+
+
+
+
- - - - - - Dependencies - - - -
- - -
- -
- -

{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}

-

{{ manifest.dependencies[dep.key].version | displayEmver }}

-

{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}

-
+ + + + + + Dependencies + + + +
+ + +
+ +
+ +

{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}

+

{{ manifest.dependencies[dep.key].version | displayEmver }}

+

{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}

+
- - View + + View + + + + + Install - - - Install + + + Start + + Update + + + Configure + + - - - Start - - - Update - - - Configure - - +
+ +
-
- -
- -
-
-
-
-
-
-
- + +
+
+
+
+
+
diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index 160874d31..a857cd693 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -23,10 +23,12 @@ export class AppShowPage { error: string pkgId: string pkg: PackageDataEntry - pkgSub: Subscription hideLAN: boolean buttons: Button[] = [] manifest: Manifest = { } as Manifest + connected: boolean + + subs: Subscription[] = [] FeStatus = FEStatus PackageState = PackageState @@ -49,10 +51,13 @@ export class AppShowPage { async ngOnInit () { this.pkgId = this.route.snapshot.paramMap.get('pkgId') - this.pkgSub = this.patch.watch$('package-data', this.pkgId).subscribe(pkg => { - this.pkg = pkg - this.manifest = getManifest(this.pkg) - }) + this.subs = [ + this.patch.watch$('package-data', this.pkgId).subscribe(pkg => { + this.pkg = pkg + this.manifest = getManifest(this.pkg) + }), + this.patch.connected$().subscribe(c => this.connected = c), + ] this.setButtons() } @@ -61,7 +66,7 @@ export class AppShowPage { } async ngOnDestroy () { - this.pkgSub.unsubscribe() + this.subs.forEach(sub => sub.unsubscribe()) } launchUiTab (): void { diff --git a/ui/src/app/pipes/display-bulb.pipe.ts b/ui/src/app/pipes/display-bulb.pipe.ts index 470dd106d..bd9be9be0 100644 --- a/ui/src/app/pipes/display-bulb.pipe.ts +++ b/ui/src/app/pipes/display-bulb.pipe.ts @@ -1,6 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core' import { PackageDataEntry } from '../models/patch-db/data-model' -import { ConnectionState } from '../services/connection.service' import { renderPkgStatus } from '../services/pkg-status-rendering.service' @Pipe({ @@ -8,8 +7,9 @@ import { renderPkgStatus } from '../services/pkg-status-rendering.service' }) export class DisplayBulbPipe implements PipeTransform { - transform (pkg: PackageDataEntry, bulb: DisplayBulb, connection: ConnectionState): boolean { - const { color } = renderPkgStatus(pkg, connection) + transform (pkg: PackageDataEntry, bulb: DisplayBulb, connected: boolean): boolean { + if (!connected) return bulb === 'off' + const { color } = renderPkgStatus(pkg) switch (color) { case 'danger': return bulb === 'red' case 'success': return bulb === 'green' diff --git a/ui/src/app/pipes/status.pipe.ts b/ui/src/app/pipes/status.pipe.ts index aadf5c6c1..ff841e86a 100644 --- a/ui/src/app/pipes/status.pipe.ts +++ b/ui/src/app/pipes/status.pipe.ts @@ -1,13 +1,12 @@ import { Pipe, PipeTransform } from '@angular/core' import { PackageDataEntry } from '../models/patch-db/data-model' -import { ConnectionState } from '../services/connection.service' import { FEStatus, renderPkgStatus } from '../services/pkg-status-rendering.service' @Pipe({ name: 'status', }) export class StatusPipe implements PipeTransform { - transform (pkg: PackageDataEntry, connection: ConnectionState): FEStatus { - return renderPkgStatus(pkg, connection).feStatus + transform (pkg: PackageDataEntry): FEStatus { + return renderPkgStatus(pkg).feStatus } } \ No newline at end of file diff --git a/ui/src/app/services/api/api.service.ts b/ui/src/app/services/api/api.service.ts index c63484ff3..5f9be1c9b 100644 --- a/ui/src/app/services/api/api.service.ts +++ b/ui/src/app/services/api/api.service.ts @@ -1,11 +1,10 @@ import { Subject, Observable } from 'rxjs' -import { Http, Source, Update, Operation, Revision } from 'patch-db-client' +import { Http, Update, Operation, Revision } from 'patch-db-client' import { RR } from './api-types' import { DataModel } from 'src/app/models/patch-db/data-model' import { filter } from 'rxjs/operators' -import * as uuid from 'uuid' -export abstract class ApiService implements Source, Http { +export abstract class ApiService implements Http { protected readonly sync = new Subject>() private syncing = true diff --git a/ui/src/app/services/api/mock-app-fixures.ts b/ui/src/app/services/api/mock-app-fixures.ts index 8b937e939..52bec63c1 100644 --- a/ui/src/app/services/api/mock-app-fixures.ts +++ b/ui/src/app/services/api/mock-app-fixures.ts @@ -719,13 +719,18 @@ export module Mock { selected: 'Goosers5G', connected: 'Goosers5G', }, - registry: 'https://registry.start9.com', + 'package-registry': 'https://registry.start9.com', + 'system-registry': 'https://registry.start9.com', 'unread-notification-count': 4, specs: { CPU: 'Cortex-A72: 4 Cores @1500MHz', Disk: '1TB SSD', Memory: '8GB', }, + 'connection-addresses': { + tor: [], + clearnet: [], + }, }, 'package-data': { 'bitcoind': bitcoind, diff --git a/ui/src/app/services/connection.service.ts b/ui/src/app/services/connection.service.ts index a5394aa32..bcd8e2c25 100644 --- a/ui/src/app/services/connection.service.ts +++ b/ui/src/app/services/connection.service.ts @@ -1,80 +1,98 @@ import { Injectable } from '@angular/core' -import { BehaviorSubject, fromEvent, merge, Observable, Subscription, timer } from 'rxjs' -import { delay, retryWhen, switchMap, tap } from 'rxjs/operators' -import { ApiService } from './api/api.service' +import { BehaviorSubject, combineLatest, fromEvent, merge, Observable, Subscription } from 'rxjs' +import { ConnectionStatus } from '../../../../../patch-db/client/dist' +import { DataModel } from '../models/patch-db/data-model' +import { PatchDbModel } from '../models/patch-db/patch-db-model' +import { HttpService, Method } from './http.service' @Injectable({ providedIn: 'root', }) export class ConnectionService { - private httpSubscription$: Subscription - private readonly networkState$ = new BehaviorSubject(navigator.onLine) - private readonly internetState$ = new BehaviorSubject(null) + private addrs: DataModel['server-info']['connection-addresses'] + private readonly networkState$ = new BehaviorSubject(true) + private readonly connectionFailure$ = new BehaviorSubject(ConnectionFailure.None) + private subs: Subscription[] = [] constructor ( - private readonly apiService: ApiService, - ) { - merge(fromEvent(window, 'online'), fromEvent(window, 'offline')) - .subscribe(event => { - this.networkState$.next(event.type === 'online') - }) + private readonly httpService: HttpService, + private readonly patch: PatchDbModel, + ) { } - this.networkState$ - .subscribe(online => { - if (online) { - this.testInternet() - } else { - this.killHttp() - this.internetState$.next(false) + watch$ () { + return this.connectionFailure$.asObservable() + } + + start () { + this.subs = [ + this.patch.watch$('server-info') + .subscribe(data => { + if (!data) return + this.addrs = data['connection-addresses'] || { + tor: [], + clearnet: [], + } + }), + + merge(fromEvent(window, 'online'), fromEvent(window, 'offline')) + .subscribe(event => { + this.networkState$.next(event.type === 'online') + }), + + combineLatest([this.networkState$, this.patch.connectionStatus$()]) + .subscribe(async ([network, connectionStatus]) => { + if (connectionStatus !== ConnectionStatus.Disconnected) { + this.connectionFailure$.next(ConnectionFailure.None) + } else if (!network) { + this.connectionFailure$.next(ConnectionFailure.Network) + } else { + this.connectionFailure$.next(ConnectionFailure.Diagnosing) + const torSuccess = await this.testAddrs(this.addrs.tor) + if (torSuccess) { + this.connectionFailure$.next(ConnectionFailure.Embassy) + } else { + const clearnetSuccess = await this.testAddrs(this.addrs.clearnet) + if (clearnetSuccess) { + this.connectionFailure$.next(ConnectionFailure.Tor) + } else { + this.connectionFailure$.next(ConnectionFailure.Internet) + } + } + } + }), + ] + } + + stop () { + this.subs.forEach(sub => { + sub.unsubscribe() + }) + this.subs = [] + } + + private async testAddrs (addrs: string[]): Promise { + if (!addrs.length) return true + + const results = await Promise.all(addrs.map(async addr => { + try { + await this.httpService.httpRequest({ + method: Method.GET, + url: addr, + }) + return true + } catch (e) { + return false } - }) - } - - monitor$ (): Observable { - return this.internetState$.asObservable() - } - - private testInternet (): void { - this.killHttp() - - // ping server every 10 seconds - this.httpSubscription$ = timer(0, 10000) - .pipe( - switchMap(() => this.apiService.echo()), - retryWhen(errors => - errors.pipe( - tap(val => { - console.error('Echo error: ', val) - this.internetState$.next(false) - }), - // restart after 2 seconds - delay(2000), - ), - ), - ) - .subscribe(() => { - this.internetState$.next(true) - }) - } - - private killHttp () { - if (this.httpSubscription$) { - this.httpSubscription$.unsubscribe() - this.httpSubscription$ = undefined - } + })) + return results.includes(true) } } -/** - * Instance of this interface is used to report current connection status. - */ - export interface ConnectionState { - /** - * "True" if browser has network connection. Determined by Window objects "online" / "offline" events. - */ - network: boolean - /** - * "True" if browser has Internet access. Determined by heartbeat system which periodically makes request to heartbeat Url. - */ - internet: boolean | null -} \ No newline at end of file +export enum ConnectionFailure { + None = 'none', + Diagnosing = 'diagnosing', + Network = 'network', + Embassy = 'embassy', + Tor = 'tor', + Internet = 'internet', +} diff --git a/ui/src/app/services/pkg-status-rendering.service.ts b/ui/src/app/services/pkg-status-rendering.service.ts index 6ef09d9dd..219bd6f30 100644 --- a/ui/src/app/services/pkg-status-rendering.service.ts +++ b/ui/src/app/services/pkg-status-rendering.service.ts @@ -1,11 +1,6 @@ import { HealthCheckResultLoading, MainStatusRunning, PackageDataEntry, PackageMainStatus, PackageState, Status } from '../models/patch-db/data-model' -import { ConnectionState } from './connection.service' - -export function renderPkgStatus (pkg: PackageDataEntry, connection: ConnectionState): PkgStatusRendering { - if (!connection.network || !connection.internet) { - return { display: 'Connecting', color: 'medium', showDots: true, feStatus: FEStatus.Connecting } - } +export function renderPkgStatus (pkg: PackageDataEntry): PkgStatusRendering { switch (pkg.state) { case PackageState.Installing: return { display: 'Installing', color: 'primary', showDots: true, feStatus: FEStatus.Installing } case PackageState.Updating: return { display: 'Updating', color: 'primary', showDots: true, feStatus: FEStatus.Updating }