From af2b2f33c29ae6fab2ace0d8ddf2c7a474400779 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 26 Oct 2023 17:33:57 -0600 Subject: [PATCH] Fix/ntp (#2479) * rework ntp faiure handling and display to user * uptime in seconds * change how we handle ntp --------- Co-authored-by: Aiden McClelland --- backend/src/account.rs | 7 +- backend/src/context/rpc.rs | 7 +- backend/src/db/model.rs | 5 +- backend/src/init.rs | 39 ++++++++-- backend/src/lib.rs | 5 ++ backend/src/net/ssl.rs | 69 +++++++++++------ backend/src/setup.rs | 3 +- backend/src/system.rs | 63 +++++++++++++++- .../download-doc/download-doc.component.html | 4 +- .../src/services/error-toast.service.ts | 2 +- .../login/ca-wizard/ca-wizard.component.ts | 2 +- .../app/pages/server-routes/lan/lan.page.html | 2 +- .../server-metrics/server-metrics.page.html | 75 ++++++++++++------- .../server-metrics/server-metrics.page.ts | 4 +- .../server-show/server-show.page.html | 27 ++++++- .../ui/src/app/services/api/api.types.ts | 5 +- .../services/api/embassy-mock-api.service.ts | 5 +- .../ui/src/app/services/api/mock-patch.ts | 2 +- .../src/app/services/patch-db/data-model.ts | 2 +- .../ui/src/app/services/time-service.ts | 67 ++++++++--------- libs/helpers/src/rsync.rs | 2 +- 21 files changed, 284 insertions(+), 113 deletions(-) diff --git a/backend/src/account.rs b/backend/src/account.rs index 1f8d86e91..0c06f2a5f 100644 --- a/backend/src/account.rs +++ b/backend/src/account.rs @@ -1,4 +1,5 @@ -use digest::Digest; +use std::time::SystemTime; + use ed25519_dalek::SecretKey; use openssl::pkey::{PKey, Private}; use openssl::x509::X509; @@ -29,11 +30,11 @@ pub struct AccountInfo { pub root_ca_cert: X509, } impl AccountInfo { - pub fn new(password: &str) -> Result { + pub fn new(password: &str, start_time: SystemTime) -> Result { let server_id = generate_id(); let hostname = generate_hostname(); let root_ca_key = generate_key()?; - let root_ca_cert = make_root_cert(&root_ca_key, &hostname)?; + let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?; Ok(Self { server_id, hostname, diff --git a/backend/src/context/rpc.rs b/backend/src/context/rpc.rs index abb122d6e..f235572e4 100644 --- a/backend/src/context/rpc.rs +++ b/backend/src/context/rpc.rs @@ -15,6 +15,7 @@ use serde::Deserialize; use sqlx::postgres::PgConnectOptions; use sqlx::PgPool; use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; +use tokio::time::Instant; use tracing::instrument; use super::setup::CURRENT_SECRET; @@ -29,7 +30,7 @@ use crate::install::cleanup::{cleanup_failed, uninstall}; use crate::manager::ManagerMap; use crate::middleware::auth::HashSessionToken; use crate::net::net_controller::NetController; -use crate::net::ssl::SslManager; +use crate::net::ssl::{root_ca_start_time, SslManager}; use crate::net::wifi::WpaCli; use crate::notifications::NotificationManager; use crate::shutdown::Shutdown; @@ -123,6 +124,7 @@ pub struct RpcContextSeed { pub current_secret: Arc, pub client: Client, pub hardware: Hardware, + pub start_time: Instant, } pub struct Hardware { @@ -158,7 +160,7 @@ impl RpcContext { base.dns_bind .as_deref() .unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]), - SslManager::new(&account)?, + SslManager::new(&account, root_ca_start_time().await?)?, &account.hostname, &account.key, ) @@ -214,6 +216,7 @@ impl RpcContext { .build() .with_kind(crate::ErrorKind::ParseUrl)?, hardware: Hardware { devices, ram }, + start_time: Instant::now(), }); let res = Self(seed.clone()); diff --git a/backend/src/db/model.rs b/backend/src/db/model.rs index 950c6505a..6ce3f8add 100644 --- a/backend/src/db/model.rs +++ b/backend/src/db/model.rs @@ -79,7 +79,7 @@ impl Database { .iter() .map(|x| format!("{x:X}")) .join(":"), - system_start_time: Utc::now().to_rfc3339(), + ntp_synced: false, zram: true, }, package_data: AllPackageData::default(), @@ -125,7 +125,8 @@ pub struct ServerInfo { pub password_hash: String, pub pubkey: String, pub ca_fingerprint: String, - pub system_start_time: String, + #[serde(default)] + pub ntp_synced: bool, #[serde(default)] pub zram: bool, } diff --git a/backend/src/init.rs b/backend/src/init.rs index fdbc41212..0308f684c 100644 --- a/backend/src/init.rs +++ b/backend/src/init.rs @@ -1,7 +1,7 @@ use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; use std::path::Path; -use std::time::Duration; +use std::time::{Duration, SystemTime}; use color_eyre::eyre::eyre; use helpers::NonDetachingJoinHandle; @@ -19,7 +19,6 @@ use crate::install::PKG_ARCHIVE_DIR; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::prelude::*; use crate::sound::BEP; -use crate::system::time; use crate::util::cpupower::{ current_governor, get_available_governors, set_governor, GOVERNOR_PERFORMANCE, }; @@ -361,15 +360,28 @@ pub async fn init(cfg: &RpcContextConfig) -> Result { } } - let mut warn_time_not_synced = true; - for _ in 0..60 { + let mut time_not_synced = true; + let mut not_made_progress = 0u32; + for _ in 0..1800 { if check_time_is_synchronized().await? { - warn_time_not_synced = false; + time_not_synced = false; break; } + let t = SystemTime::now(); tokio::time::sleep(Duration::from_secs(1)).await; + if t.elapsed() + .map(|t| t > Duration::from_secs_f64(1.1)) + .unwrap_or(true) + { + not_made_progress = 0; + } else { + not_made_progress += 1; + } + if not_made_progress > 30 { + break; + } } - if warn_time_not_synced { + if time_not_synced { tracing::warn!("Timed out waiting for system time to synchronize"); } else { tracing::info!("Syncronized system clock"); @@ -385,7 +397,20 @@ pub async fn init(cfg: &RpcContextConfig) -> Result { backup_progress: None, }; - server_info.system_start_time = time().await?; + server_info.ntp_synced = if time_not_synced { + let db = db.clone(); + tokio::spawn(async move { + while !check_time_is_synchronized().await.unwrap() { + tokio::time::sleep(Duration::from_secs(30)).await; + } + db.mutate(|v| v.as_server_info_mut().as_ntp_synced_mut().ser(&true)) + .await + .unwrap() + }); + false + } else { + true + }; db.mutate(|v| { v.as_server_info_mut().ser(&server_info)?; diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 67f34d785..141ef1780 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -17,6 +17,9 @@ lazy_static::lazy_static! { ARCH.to_string() } }; + pub static ref SOURCE_DATE: SystemTime = { + std::fs::metadata(std::env::current_exe().unwrap()).unwrap().modified().unwrap() + }; } pub mod account; @@ -62,6 +65,8 @@ pub mod util; pub mod version; pub mod volume; +use std::time::SystemTime; + pub use config::Config; pub use error::{Error, ErrorKind, ResultExt}; use rpc_toolkit::command; diff --git a/backend/src/net/ssl.rs b/backend/src/net/ssl.rs index c2cab3355..ba2f314b9 100644 --- a/backend/src/net/ssl.rs +++ b/backend/src/net/ssl.rs @@ -4,8 +4,8 @@ use std::net::IpAddr; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; - use futures::FutureExt; +use libc::time_t; use openssl::asn1::{Asn1Integer, Asn1Time}; use openssl::bn::{BigNum, MsbOption}; use openssl::ec::{EcGroup, EcKey}; @@ -19,15 +19,22 @@ use tokio::sync::{Mutex, RwLock}; use tracing::instrument; use crate::account::AccountInfo; -use crate::context::{RpcContext}; +use crate::context::RpcContext; use crate::hostname::Hostname; +use crate::init::check_time_is_synchronized; use crate::net::dhcp::ips; use crate::net::keys::{Key, KeyInfo}; - -use crate::{Error, ErrorKind, ResultExt}; +use crate::{Error, ErrorKind, ResultExt, SOURCE_DATE}; static CERTIFICATE_VERSION: i32 = 2; // X509 version 3 is actually encoded as '2' in the cert because fuck you. +fn unix_time(time: SystemTime) -> time_t { + time.duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as time_t) + .or_else(|_| UNIX_EPOCH.elapsed().map(|d| -(d.as_secs() as time_t))) + .unwrap_or_default() +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct CertPair { pub ed25519: X509, @@ -57,9 +64,13 @@ impl CertPair { }), ); if cert - .not_after() - .compare(Asn1Time::days_from_now(30)?.as_ref())? - == Ordering::Greater + .not_before() + .compare(Asn1Time::days_from_now(0)?.as_ref())? + == Ordering::Less + && cert + .not_after() + .compare(Asn1Time::days_from_now(30)?.as_ref())? + == Ordering::Greater && ips.is_superset(&ip) { return Ok(cert.clone()); @@ -82,6 +93,14 @@ impl CertPair { } } +pub async fn root_ca_start_time() -> Result { + Ok(if check_time_is_synchronized().await? { + SystemTime::now() + } else { + *SOURCE_DATE + }) +} + #[derive(Debug)] pub struct SslManager { hostname: Hostname, @@ -91,9 +110,13 @@ pub struct SslManager { cert_cache: RwLock>, } impl SslManager { - pub fn new(account: &AccountInfo) -> Result { + pub fn new(account: &AccountInfo, start_time: SystemTime) -> Result { let int_key = generate_key()?; - let int_cert = make_int_cert((&account.root_ca_key, &account.root_ca_cert), &int_key)?; + let int_cert = make_int_cert( + (&account.root_ca_key, &account.root_ca_cert), + &int_key, + start_time, + )?; Ok(Self { hostname: account.hostname.clone(), root_cert: account.root_ca_cert.clone(), @@ -162,14 +185,20 @@ pub fn generate_key() -> Result, Error> { } #[instrument(skip_all)] -pub fn make_root_cert(root_key: &PKey, hostname: &Hostname) -> Result { +pub fn make_root_cert( + root_key: &PKey, + hostname: &Hostname, + start_time: SystemTime, +) -> Result { let mut builder = X509Builder::new()?; builder.set_version(CERTIFICATE_VERSION)?; - let embargo = Asn1Time::days_from_now(0)?; + let unix_start_time = unix_time(start_time); + + let embargo = Asn1Time::from_unix(unix_start_time)?; builder.set_not_before(&embargo)?; - let expiration = Asn1Time::days_from_now(3650)?; + let expiration = Asn1Time::from_unix(unix_start_time + (10 * 365 * 86400))?; builder.set_not_after(&expiration)?; builder.set_serial_number(&*rand_serial()?)?; @@ -216,14 +245,17 @@ pub fn make_root_cert(root_key: &PKey, hostname: &Hostname) -> Result, &X509), applicant: &PKey, + start_time: SystemTime, ) -> Result { let mut builder = X509Builder::new()?; builder.set_version(CERTIFICATE_VERSION)?; - let embargo = Asn1Time::days_from_now(0)?; + let unix_start_time = unix_time(start_time); + + let embargo = Asn1Time::from_unix(unix_start_time)?; builder.set_not_before(&embargo)?; - let expiration = Asn1Time::days_from_now(3650)?; + let expiration = Asn1Time::from_unix(unix_start_time + (10 * 365 * 86400))?; builder.set_not_after(&expiration)?; builder.set_serial_number(&*rand_serial()?)?; @@ -346,14 +378,7 @@ pub fn make_leaf_cert( let mut builder = X509Builder::new()?; builder.set_version(CERTIFICATE_VERSION)?; - let embargo = Asn1Time::from_unix( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs() as i64) - .or_else(|_| UNIX_EPOCH.elapsed().map(|d| -(d.as_secs() as i64))) - .unwrap_or_default() - - 86400, - )?; + let embargo = Asn1Time::from_unix(unix_time(SystemTime::now()) - 86400)?; builder.set_not_before(&embargo)?; // Google Apple and Mozilla reject certificate horizons longer than 397 days diff --git a/backend/src/setup.rs b/backend/src/setup.rs index f9e897d01..64c324095 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -31,6 +31,7 @@ use crate::disk::REPAIR_DISK_PATH; use crate::hostname::Hostname; use crate::init::{init, InitResult}; use crate::middleware::encrypt::EncryptedWire; +use crate::net::ssl::root_ca_start_time; use crate::prelude::*; use crate::util::io::{dir_copy, dir_size, Counter}; use crate::{Error, ErrorKind, ResultExt}; @@ -378,7 +379,7 @@ async fn fresh_setup( ctx: &SetupContext, embassy_password: &str, ) -> Result<(Hostname, OnionAddressV3, X509), Error> { - let account = AccountInfo::new(embassy_password)?; + let account = AccountInfo::new(embassy_password, root_ca_start_time().await?)?; let sqlite_pool = ctx.secret_store().await?; account.save(&sqlite_pool).await?; sqlite_pool.close().await; diff --git a/backend/src/system.rs b/backend/src/system.rs index aee32b50a..249ade9c3 100644 --- a/backend/src/system.rs +++ b/backend/src/system.rs @@ -1,6 +1,7 @@ use std::fmt; use chrono::Utc; +use clap::ArgMatches; use color_eyre::eyre::eyre; use futures::FutureExt; use rpc_toolkit::command; @@ -84,9 +85,65 @@ pub async fn zram(#[context] ctx: RpcContext, #[arg] enable: bool) -> Result<(), Ok(()) } -#[command] -pub async fn time() -> Result { - Ok(Utc::now().to_rfc3339()) +#[derive(Serialize, Deserialize)] +pub struct TimeInfo { + now: String, + uptime: u64, +} + +fn display_time(arg: TimeInfo, matches: &ArgMatches) { + use std::fmt::Write; + + use prettytable::*; + + if matches.is_present("format") { + return display_serializable(arg, matches); + } + + let days = arg.uptime / (24 * 60 * 60); + let days_s = arg.uptime % (24 * 60 * 60); + let hours = days_s / (60 * 60); + let hours_s = arg.uptime % (60 * 60); + let minutes = hours_s / 60; + let seconds = arg.uptime % 60; + let mut uptime_string = String::new(); + if days > 0 { + write!(&mut uptime_string, "{days} days").unwrap(); + } + if hours > 0 { + if !uptime_string.is_empty() { + uptime_string += ", "; + } + write!(&mut uptime_string, "{hours} hours").unwrap(); + } + if minutes > 0 { + if !uptime_string.is_empty() { + uptime_string += ", "; + } + write!(&mut uptime_string, "{minutes} minutes").unwrap(); + } + if !uptime_string.is_empty() { + uptime_string += ", "; + } + write!(&mut uptime_string, "{seconds} seconds").unwrap(); + + let mut table = Table::new(); + table.add_row(row![bc -> "NOW", &arg.now]); + table.add_row(row![bc -> "UPTIME", &uptime_string]); + table.print_tty(false).unwrap(); +} + +#[command(display(display_time))] +pub async fn time( + #[context] ctx: RpcContext, + #[allow(unused_variables)] + #[arg(long = "format")] + format: Option, +) -> Result { + Ok(TimeInfo { + now: Utc::now().to_rfc3339(), + uptime: ctx.start_time.elapsed().as_secs(), + }) } #[command( diff --git a/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html b/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html index 0d659241d..25fa87a2e 100644 --- a/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html +++ b/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html @@ -37,7 +37,7 @@

Download your server's Root CA and Note: This address will only work from a Tor-enabled browser. For a secure local connection and faster Tor experience, diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html index c93d633c0..91b00fb28 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html @@ -4,9 +4,9 @@ Monitor - + + + @@ -16,28 +16,51 @@

- Time - - System Time - - {{ systemTime$ | async | date:'MMMM d, y, h:mm a z':'UTC' - }} - + + + +

System Time

+

+ + {{ now.value | date:'MMMM d, y, h:mm a z':'UTC' }} + +

+

+ + NTP not synced, time could be wrong + +

+
+
+ + + +

System Time

+

Loading...

+
+ +
+
+ - System Uptime - - - {{ uptime.days }} Days, {{ uptime.hours }} Hours, - {{ uptime.minutes }} Minutes - - + +

System Uptime

+

+ + + {{ uptime.days }} + Days, + {{ uptime.hours }} + Hours, + {{ uptime.minutes }} + Minutes, + {{ uptime.seconds }} + Seconds + + +

+
@@ -50,9 +73,9 @@ > {{ metric.key }} - {{ metric.value.value }} {{ metric.value.unit }} + + {{ metric.value.value }} {{ metric.value.unit }} + diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts index 83f576919..a4c2dc325 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts @@ -14,8 +14,8 @@ export class ServerMetricsPage { going = false metrics: Metrics = {} - readonly systemTime$ = this.timeService.systemTime$ - readonly systemUptime$ = this.timeService.systemUptime$ + readonly now$ = this.timeService.now$ + readonly uptime$ = this.timeService.uptime$ constructor( private readonly errToast: ErrorToastService, diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html index 2f119590a..760a013e6 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -15,7 +15,32 @@ - + + + +

Clock sync failure

+

+ This will cause connectivity issues. Refer to the StartOS docs to + resolve the issue. +

+
+ + Open Docs + + +
+ +

Http detected

diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index d5c519e69..860bc6003 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -42,7 +42,10 @@ export module RR { export type EchoRes = string export type GetSystemTimeReq = {} // server.time - export type GetSystemTimeRes = string + export type GetSystemTimeRes = { + now: string + uptime: number // seconds + } export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs export type GetServerLogsRes = LogsRes diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 101d5511f..96cc2941c 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -179,7 +179,10 @@ export class MockApiService extends ApiService { params: RR.GetSystemTimeReq, ): Promise { await pauseFor(2000) - return new Date().toUTCString() + return { + now: new Date().toUTCString(), + uptime: 1234567, + } } async getServerLogs( diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 3c19a4da7..02faddeb7 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -70,7 +70,7 @@ export const mockPatchData: DataModel = { hostname: 'random-words', pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m', 'ca-fingerprint': 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15', - 'system-start-time': new Date(new Date().valueOf() - 360042).toUTCString(), + 'ntp-synced': false, zram: false, platform: 'x86_64-nonfree', }, diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index 02af5e4a9..ba2dd4de7 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -76,7 +76,7 @@ export interface ServerInfo { hostname: string pubkey: string 'ca-fingerprint': string - 'system-start-time': string + 'ntp-synced': boolean zram: boolean platform: string } diff --git a/frontend/projects/ui/src/app/services/time-service.ts b/frontend/projects/ui/src/app/services/time-service.ts index 355a671ff..26d1a216b 100644 --- a/frontend/projects/ui/src/app/services/time-service.ts +++ b/frontend/projects/ui/src/app/services/time-service.ts @@ -1,54 +1,53 @@ import { Injectable } from '@angular/core' -import { - map, - shareReplay, - startWith, - switchMap, - take, - tap, -} from 'rxjs/operators' +import { map, shareReplay, startWith, switchMap } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' import { DataModel } from './patch-db/data-model' import { ApiService } from './api/embassy-api.service' -import { combineLatest, from, timer } from 'rxjs' +import { combineLatest, from, interval } from 'rxjs' @Injectable({ providedIn: 'root', }) export class TimeService { - private readonly startTimeMs$ = this.patch - .watch$('server-info', 'system-start-time') - .pipe(map(startTime => new Date(startTime).valueOf())) - - readonly systemTime$ = from(this.apiService.getSystemTime({})).pipe( - switchMap(utcStr => { - const dateObj = new Date(utcStr) - const msRemaining = (60 - dateObj.getSeconds()) * 1000 - dateObj.setSeconds(0) - const current = dateObj.valueOf() - return timer(msRemaining, 60000).pipe( + private readonly time$ = from(this.apiService.getSystemTime({})).pipe( + switchMap(({ now, uptime }) => { + const current = new Date(now).valueOf() + return interval(1000).pipe( map(index => { const incremented = index + 1 - const msToAdd = 60000 * incremented - return current + msToAdd + return { + now: current + 1000 * incremented, + uptime: uptime + incremented, + } + }), + startWith({ + now: current, + uptime, }), - startWith(current), ) }), + shareReplay({ bufferSize: 1, refCount: true }), ) - readonly systemUptime$ = combineLatest([ - this.startTimeMs$, - this.systemTime$, + readonly now$ = combineLatest([ + this.time$, + this.patch.watch$('server-info', 'ntp-synced'), ]).pipe( - map(([startTime, currentTime]) => { - const ms = currentTime - startTime - const days = Math.floor(ms / (24 * 60 * 60 * 1000)) - const daysms = ms % (24 * 60 * 60 * 1000) - const hours = Math.floor(daysms / (60 * 60 * 1000)) - const hoursms = ms % (60 * 60 * 1000) - const minutes = Math.floor(hoursms / (60 * 1000)) - return { days, hours, minutes } + map(([time, synced]) => ({ + value: time.now, + synced, + })), + ) + + readonly uptime$ = this.time$.pipe( + map(({ uptime }) => { + const days = Math.floor(uptime / (24 * 60 * 60)) + const daysSec = uptime % (24 * 60 * 60) + const hours = Math.floor(daysSec / (60 * 60)) + const hoursSec = uptime % (60 * 60) + const minutes = Math.floor(hoursSec / 60) + const seconds = uptime % 60 + return { days, hours, minutes, seconds } }), ) diff --git a/libs/helpers/src/rsync.rs b/libs/helpers/src/rsync.rs index c09ac3d64..1ac24c8b2 100644 --- a/libs/helpers/src/rsync.rs +++ b/libs/helpers/src/rsync.rs @@ -71,7 +71,7 @@ impl Rsync { cmd.arg(format!("--exclude={}", exclude)); } let mut command = cmd - .arg("-acAXH") + .arg("-actAXH") .arg("--info=progress2") .arg("--no-inc-recursive") .arg(src.as_ref())