Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,183 changes: 3,060 additions & 123 deletions backend/Cargo.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions backend/crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ default = []
ntp = ["dep:tokio", "dep:ntp_usg-server", "dep:tracing-subscriber"]
# TCP/UDP transport (pulls in tokio)
net = ["dep:tokio"]
# ADJ download through git (dependency added when the module is written)
git = []
# ADJ download through git
git = ["dep:tempfile"]
# Packet sniffing through pcap (dependency added when the module is written)
pcap = ["net"]
# CSV data logger (records, sessions, subloggers)
Expand All @@ -32,6 +32,9 @@ tracing.workspace = true
tokio = { workspace = true, optional = true, features = ["macros", "rt-multi-thread"] }
tracing-subscriber = { workspace = true, optional = true , features = ["env-filter", "fmt"]}
tracing-appender = { workspace = true, optional = true }
gix = { version = "0.87.1", features = ["blocking-http-transport-reqwest-rust-tls", "blocking-network-client", "worktree-mutation"] }
dirs = "7.0.0"
tempfile = { version = "3.27", optional = true }
ntp_usg-server = { version = "5.0.0", optional = true, features = ["ipv4", "tokio"] }

# `workspace = true` can't be combined with extra lints in the same table,
Expand Down
48 changes: 44 additions & 4 deletions backend/crates/core/src/adj.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,35 @@
//! [`crate::model::PodData`], plus backend-session metadata not modeled as
//! vehicle data ([`crate::adj::AdjInfo`]).
//!
//! Loading (and if `git` feature is enabled also downloading) is not specific to `station`: a sniffer, a
//! simulator, or any other tool that reads an ADJ needs exactly the same
//! parsing. Scope for this iteration: no `sockets.json`, no
//! [`load_from_dir`] loads an existing ADJ directory. With the `git` feature
//! enabled, `auto_adj` updates the ADJ repository before loading its data
//! and returns the checked-out commit hash.
//!
//! Loading is not specific to `station`: a sniffer, a simulator, or any
//! other tool that reads an ADJ needs exactly the same parsing.
//!
//! Scope for this iteration: no `sockets.json`, no
//! `period`/`period_type`/`socket` packet fields (matches
//! [`crate::model::PacketDef`]'s own omission), and no schema validation
//! (`adj::validate` is a separate, deliberately out-of-scope concern for
//! now) — this module only surfaces structural errors it cannot avoid,
//! now). This module only surfaces structural errors it cannot avoid,
//! e.g. a packet referencing a measurement alias that doesn't exist.

mod error;
mod info;
mod raw;

#[cfg(feature = "git")]
use std::path::PathBuf;
#[cfg(feature = "git")]
use tracing::{info, instrument};

pub use error::{LoadError, MeasurementError, ProtectionError};
pub use info::{AdjInfo, ProtectionTypeInfo};
pub use raw::load_from_dir;

#[cfg(feature = "git")]
use crate::git::{unwrap_repo_path, update_adj};
use crate::model::PodData;

/// The result of loading an ADJ: the vehicle's data (as [`crate::model`]
Expand All @@ -31,3 +43,31 @@ pub struct Adj {
/// Backend-session metadata from `general_info.json`.
pub info: AdjInfo,
}

/// Updates the ADJ repository, loads its data, and returns its full commit hash.
///
/// `path` identifies the repository directory, not its parent. If omitted,
/// the platform's user cache directory is used.
///
/// Online, the repository is replaced and local edits and commits are discarded.
/// Offline, the current cached checkout is loaded without switching branches,
/// so it may differ from `branch`. Clone failures do not fall back to the cache.
///
/// This performs blocking network and filesystem operations. Call during startup,
/// before readers use ADJ, with only one updater at a time.
///
/// # Errors
///
/// Returns [`LoadError::Git`] if path resolution or the update fails.
/// Other variants report failures loading the checked-out ADJ data.
/// Loading failures do not restore the previous cache.
#[cfg(feature = "git")]
#[instrument(level = "debug")]
pub fn auto_adj(path: Option<PathBuf>, branch: &str) -> Result<(Adj, String), LoadError> {
let repo_path = unwrap_repo_path(path)?;
let hash = update_adj(Some(repo_path.clone()), branch)?;
let adj = load_from_dir(&repo_path)?;

info!(path = ?repo_path, %hash, "ADJ loaded");
Ok((adj, hash))
}
12 changes: 10 additions & 2 deletions backend/crates/core/src/adj/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Errors produced while loading an ADJ.
//! Errors produced while acquiring or loading an ADJ.
//!
//! Layered on purpose: [`LoadError`] is the only public error type here —
//! the one a composition root's own aggregate error would `#[from]`-chain
Expand All @@ -10,7 +10,10 @@
use std::net::AddrParseError;
use std::path::PathBuf;

/// An error produced while loading an ADJ directory into an [`crate::adj::Adj`].
#[cfg(feature = "git")]
use crate::git::GitError;

/// An error produced while acquiring or loading an [`crate::adj::Adj`].
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
/// Couldn't read a file.
Expand Down Expand Up @@ -118,6 +121,11 @@ pub enum LoadError {
#[source]
source: MeasurementError,
},

/// Couldn't resolve the ADJ repository path or update its checkout.
#[cfg(feature = "git")]
#[error("couldn't prepare ADJ repository: {0}")]
Git(#[from] GitError),
}

/// An error converting a raw measurement into [`crate::model::Measurement`].
Expand Down
11 changes: 11 additions & 0 deletions backend/crates/core/src/git.rs
Comment thread
pminmic marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! Blocking Git helpers for ADJ, an application-owned, replaceable data cache.

mod clone;
mod error;
mod repository;
mod update;

pub use error::{CloneError, GitError, RepositoryError};
pub use repository::{branch_exists, list_remote_branches, open_repo};
pub(crate) use update::unwrap_repo_path;
pub use update::update_adj;
71 changes: 71 additions & 0 deletions backend/crates/core/src/git/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ADJ Git helpers

This module downloads and updates the local [ADJ repository](https://github.com/Hyperloop-UPV/adj) using [`gix`](https://crates.io/crates/gix), with [`tracing`](https://crates.io/crates/tracing) for logging. It supports branch selection and offline use of an existing local checkout.

## Workflow

`update_adj(path, branch)` returns the full hash of the checked-out commit.
A custom path identifies the ADJ repository itself. The default is
`<cache directory>/hyperloop-control-station/adj`, using `dirs::cache_dir()`.
An existing invalid repository or inaccessible path returns an error.

The existing connectivity check attempts a TCP connection to `1.1.1.1:443`
with a three-second timeout. This checks external TCP connectivity, not GitHub
availability.

### Online

Online updates replace the local checkout and do not preserve local edits or commits.

- Shallow-clone the requested branch into a temporary directory beside ADJ.
- Complete checkout and read its commit hash before touching the cache.
- Move the old cache aside and install the new checkout.
- Restore the old cache if installation fails. If restoration also fails,
preserve the backup and include its location in the error.
- Remove the old cache after successful installation.

A clone error leaves the old cache untouched and is returned directly; it does
not automatically fall back to offline operation. No separate fetch of the
existing cache, merge, or branch switch is needed because the checkout is replaced.

### Offline

- If ADJ exists, return its current commit hash without updating or switching branches.
- Otherwise, return `ADJ is not available locally and cannot be downloaded`.

The cached branch may differ from the requested branch when offline.

## Other Helpers

- `open_repo` opens the custom or default repository path.
- `list_remote_branches(repo)` queries the default fetch remote and returns sorted short
branch names, without downloading objects or updating local references. It
requires remote access and does not fall back to cached branches on failure.
- `branch_exists(repo, name)` uses that listing to check an exact short branch
name. Query errors are propagated; `false` means a successful query did not
find the branch, not that the remote was unreachable.

## Limitations

Replacement is intended for startup, before the backend reads ADJ, with one
updater at a time. The backup and installation renames are not jointly atomic
across a process crash. The connectivity probe can succeed when GitHub is
unreachable, or fail when GitHub is reachable.

## Errors

Public helpers return `GitError` with five categories: `Path`, `Repository`,
`Clone`, `Replacement`, and `UnavailableOffline`. `RepositoryError` and
`CloneError` group the concrete `gix` causes inside the corresponding categories.
If rollback fails, `Replacement` also retains the restoration error and backup
path for recovery.

## Verification

Tests use temporary local Git repositories and the Git CLI to create fixtures;
they do not require external network access. Production Git operations use `gix`.

```bash
cargo test -p software-core -F git
cargo doc --open -p software-core -F git --no-deps
```
Loading
Loading