v1.0: Advertise node version in gossip (bp #9986) (#9995)

automerge
This commit is contained in:
mergify[bot]
2020-05-12 19:38:52 -07:00
committed by GitHub
parent 5326f3ec73
commit 14bbcef722
13 changed files with 2523 additions and 2343 deletions

4650
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -53,6 +53,7 @@ members = [
"transaction-status", "transaction-status",
"upload-perf", "upload-perf",
"net-utils", "net-utils",
"version",
"vote-signer", "vote-signer",
"cli", "cli",
"rayon-threadlimit", "rayon-threadlimit",

View File

@ -108,6 +108,8 @@ pub struct RpcContactInfo {
pub tpu: Option<SocketAddr>, pub tpu: Option<SocketAddr>,
/// JSON RPC port /// JSON RPC port
pub rpc: Option<SocketAddr>, pub rpc: Option<SocketAddr>,
/// Software version
pub version: Option<String>,
} }
/// Map of leader base58 identity pubkeys to the slot indices relative to the first epoch slot /// Map of leader base58 identity pubkeys to the slot indices relative to the first epoch slot

View File

@ -61,6 +61,7 @@ solana-runtime = { path = "../runtime", version = "1.0.23" }
solana-sdk = { path = "../sdk", version = "1.0.23" } solana-sdk = { path = "../sdk", version = "1.0.23" }
solana-stake-program = { path = "../programs/stake", version = "1.0.23" } solana-stake-program = { path = "../programs/stake", version = "1.0.23" }
solana-storage-program = { path = "../programs/storage", version = "1.0.23" } solana-storage-program = { path = "../programs/storage", version = "1.0.23" }
solana-version = { path = "../version", version = "1.0.23" }
solana-vote-program = { path = "../programs/vote", version = "1.0.23" } solana-vote-program = { path = "../programs/vote", version = "1.0.23" }
solana-vote-signer = { path = "../vote-signer", version = "1.0.23" } solana-vote-signer = { path = "../vote-signer", version = "1.0.23" }
solana-sys-tuner = { path = "../sys-tuner", version = "1.0.23" } solana-sys-tuner = { path = "../sys-tuner", version = "1.0.23" }

View File

@ -20,7 +20,8 @@ use crate::{
crds_gossip_error::CrdsGossipError, crds_gossip_error::CrdsGossipError,
crds_gossip_pull::{CrdsFilter, CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS}, crds_gossip_pull::{CrdsFilter, CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS},
crds_value::{ crds_value::{
self, CrdsData, CrdsValue, CrdsValueLabel, EpochSlots, SnapshotHash, Vote, MAX_WALLCLOCK, self, CrdsData, CrdsValue, CrdsValueLabel, EpochSlots, SnapshotHash, Version, Vote,
MAX_WALLCLOCK,
}, },
packet::{Packet, PACKET_DATA_SIZE}, packet::{Packet, PACKET_DATA_SIZE},
result::{Error, Result}, result::{Error, Result},
@ -336,6 +337,7 @@ impl ClusterInfo {
archivers += 1; archivers += 1;
} }
let node_version = self.get_node_version(&node.id);
if my_shred_version != 0 && (node.shred_version != 0 && node.shred_version != my_shred_version) { if my_shred_version != 0 && (node.shred_version != 0 && node.shred_version != my_shred_version) {
different_shred_nodes += 1; different_shred_nodes += 1;
None None
@ -351,10 +353,9 @@ impl ClusterInfo {
"none".to_string() "none".to_string()
} }
} }
let ip_addr = node.gossip.ip(); let ip_addr = node.gossip.ip();
Some(format!( Some(format!(
"{:15} {:2}| {:5} | {:44} | {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n", "{:15} {:2}| {:5} | {:44} |{:^15}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n",
if ContactInfo::is_valid_address(&node.gossip) { if ContactInfo::is_valid_address(&node.gossip) {
ip_addr.to_string() ip_addr.to_string()
} else { } else {
@ -363,6 +364,11 @@ impl ClusterInfo {
if node.id == my_pubkey { "me" } else { "" }.to_string(), if node.id == my_pubkey { "me" } else { "" }.to_string(),
now.saturating_sub(last_updated), now.saturating_sub(last_updated),
node.id.to_string(), node.id.to_string(),
if let Some(node_version) = node_version {
node_version.to_string()
} else {
"-".to_string()
},
addr_to_string(&ip_addr, &node.gossip), addr_to_string(&ip_addr, &node.gossip),
addr_to_string(&ip_addr, &node.tpu), addr_to_string(&ip_addr, &node.tpu),
addr_to_string(&ip_addr, &node.tpu_forwards), addr_to_string(&ip_addr, &node.tpu_forwards),
@ -370,7 +376,6 @@ impl ClusterInfo {
addr_to_string(&ip_addr, &node.tvu_forwards), addr_to_string(&ip_addr, &node.tvu_forwards),
addr_to_string(&ip_addr, &node.repair), addr_to_string(&ip_addr, &node.repair),
addr_to_string(&ip_addr, &node.serve_repair), addr_to_string(&ip_addr, &node.serve_repair),
addr_to_string(&ip_addr, &node.storage_addr),
addr_to_string(&ip_addr, &node.rpc), addr_to_string(&ip_addr, &node.rpc),
addr_to_string(&ip_addr, &node.rpc_pubsub), addr_to_string(&ip_addr, &node.rpc_pubsub),
node.shred_version, node.shred_version,
@ -381,9 +386,9 @@ impl ClusterInfo {
format!( format!(
"IP Address |Age(ms)| Node identifier \ "IP Address |Age(ms)| Node identifier \
|Gossip| TPU |TPUfwd| TVU |TVUfwd|Repair|ServeR|Storag| RPC |PubSub|ShredVer\n\ | Version |Gossip| TPU |TPUfwd| TVU |TVUfwd|Repair|ServeR| RPC |PubSub|ShredVer\n\
------------------+-------+----------------------------------------------+\ ------------------+-------+----------------------------------------------+---------------+\
------+------+------+------+------+------+------+------+------+------+--------\n\ ------+------+------+------+------+------+------+------+------+--------\n\
{}\ {}\
Nodes: {}{}{}{}", Nodes: {}{}{}{}",
nodes.join(""), nodes.join(""),
@ -398,7 +403,7 @@ impl ClusterInfo {
} else { } else {
"".to_string() "".to_string()
}, },
if spy_nodes > 0 { if different_shred_nodes > 0 {
format!( format!(
"\nNodes with different shred version: {}", "\nNodes with different shred version: {}",
different_shred_nodes different_shred_nodes
@ -556,6 +561,16 @@ impl ClusterInfo {
.map(|x| x.value.contact_info().unwrap()) .map(|x| x.value.contact_info().unwrap())
} }
pub fn get_node_version(&self, pubkey: &Pubkey) -> Option<solana_version::Version> {
self.gossip
.crds
.table
.get(&CrdsValueLabel::Version(*pubkey))
.map(|x| x.value.version())
.flatten()
.map(|version| version.version.clone())
}
/// all validators that have a valid rpc port regardless of `shred_version`. /// all validators that have a valid rpc port regardless of `shred_version`.
pub fn all_rpc_peers(&self) -> Vec<ContactInfo> { pub fn all_rpc_peers(&self) -> Vec<ContactInfo> {
let me = self.my_data(); let me = self.my_data();
@ -1201,6 +1216,14 @@ impl ClusterInfo {
let mut last_contact_info_trace = timestamp(); let mut last_contact_info_trace = timestamp();
let mut adopt_shred_version = obj.read().unwrap().my_data().shred_version == 0; let mut adopt_shred_version = obj.read().unwrap().my_data().shred_version == 0;
let recycler = PacketsRecycler::default(); let recycler = PacketsRecycler::default();
{
let mut obj = obj.write().unwrap();
let message = CrdsValue::new_signed(
CrdsData::Version(Version::new(obj.id())),
&obj.keypair,
);
obj.push_message(message);
}
loop { loop {
let start = timestamp(); let start = timestamp();
thread_mem_usage::datapoint("solana-gossip"); thread_mem_usage::datapoint("solana-gossip");

View File

@ -71,6 +71,7 @@ pub enum CrdsData {
EpochSlots(EpochSlotIndex, EpochSlots), EpochSlots(EpochSlotIndex, EpochSlots),
SnapshotHashes(SnapshotHash), SnapshotHashes(SnapshotHash),
AccountsHashes(SnapshotHash), AccountsHashes(SnapshotHash),
Version(Version),
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
@ -122,6 +123,7 @@ impl Sanitize for CrdsData {
} }
val.sanitize() val.sanitize()
} }
CrdsData::Version(version) => version.sanitize(),
} }
} }
} }
@ -228,6 +230,33 @@ impl Vote {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Version {
pub from: Pubkey,
pub wallclock: u64,
pub version: solana_version::Version,
}
impl Sanitize for Version {
fn sanitize(&self) -> Result<(), SanitizeError> {
if self.wallclock >= MAX_WALLCLOCK {
return Err(SanitizeError::ValueOutOfBounds);
}
self.from.sanitize()?;
self.version.sanitize()
}
}
impl Version {
pub fn new(from: Pubkey) -> Self {
Self {
from,
wallclock: timestamp(),
version: solana_version::Version::default(),
}
}
}
/// Type of the replicated value /// Type of the replicated value
/// These are labels for values in a record that is associated with `Pubkey` /// These are labels for values in a record that is associated with `Pubkey`
#[derive(PartialEq, Hash, Eq, Clone, Debug)] #[derive(PartialEq, Hash, Eq, Clone, Debug)]
@ -237,6 +266,7 @@ pub enum CrdsValueLabel {
EpochSlots(Pubkey), EpochSlots(Pubkey),
SnapshotHashes(Pubkey), SnapshotHashes(Pubkey),
AccountsHashes(Pubkey), AccountsHashes(Pubkey),
Version(Pubkey),
} }
impl fmt::Display for CrdsValueLabel { impl fmt::Display for CrdsValueLabel {
@ -247,6 +277,7 @@ impl fmt::Display for CrdsValueLabel {
CrdsValueLabel::EpochSlots(_) => write!(f, "EpochSlots({})", self.pubkey()), CrdsValueLabel::EpochSlots(_) => write!(f, "EpochSlots({})", self.pubkey()),
CrdsValueLabel::SnapshotHashes(_) => write!(f, "SnapshotHashes({})", self.pubkey()), CrdsValueLabel::SnapshotHashes(_) => write!(f, "SnapshotHashes({})", self.pubkey()),
CrdsValueLabel::AccountsHashes(_) => write!(f, "AccountsHashes({})", self.pubkey()), CrdsValueLabel::AccountsHashes(_) => write!(f, "AccountsHashes({})", self.pubkey()),
CrdsValueLabel::Version(_) => write!(f, "Version({})", self.pubkey()),
} }
} }
} }
@ -259,6 +290,7 @@ impl CrdsValueLabel {
CrdsValueLabel::EpochSlots(p) => *p, CrdsValueLabel::EpochSlots(p) => *p,
CrdsValueLabel::SnapshotHashes(p) => *p, CrdsValueLabel::SnapshotHashes(p) => *p,
CrdsValueLabel::AccountsHashes(p) => *p, CrdsValueLabel::AccountsHashes(p) => *p,
CrdsValueLabel::Version(p) => *p,
} }
} }
} }
@ -276,7 +308,7 @@ impl CrdsValue {
value.sign(keypair); value.sign(keypair);
value value
} }
/// Totally unsecure unverfiable wallclock of the node that generated this message /// Totally unsecure unverifiable wallclock of the node that generated this message
/// Latest wallclock is always picked. /// Latest wallclock is always picked.
/// This is used to time out push messages. /// This is used to time out push messages.
pub fn wallclock(&self) -> u64 { pub fn wallclock(&self) -> u64 {
@ -286,6 +318,7 @@ impl CrdsValue {
CrdsData::EpochSlots(_, vote) => vote.wallclock, CrdsData::EpochSlots(_, vote) => vote.wallclock,
CrdsData::SnapshotHashes(hash) => hash.wallclock, CrdsData::SnapshotHashes(hash) => hash.wallclock,
CrdsData::AccountsHashes(hash) => hash.wallclock, CrdsData::AccountsHashes(hash) => hash.wallclock,
CrdsData::Version(version) => version.wallclock,
} }
} }
pub fn pubkey(&self) -> Pubkey { pub fn pubkey(&self) -> Pubkey {
@ -295,6 +328,7 @@ impl CrdsValue {
CrdsData::EpochSlots(_, slots) => slots.from, CrdsData::EpochSlots(_, slots) => slots.from,
CrdsData::SnapshotHashes(hash) => hash.from, CrdsData::SnapshotHashes(hash) => hash.from,
CrdsData::AccountsHashes(hash) => hash.from, CrdsData::AccountsHashes(hash) => hash.from,
CrdsData::Version(version) => version.from,
} }
} }
pub fn label(&self) -> CrdsValueLabel { pub fn label(&self) -> CrdsValueLabel {
@ -304,6 +338,7 @@ impl CrdsValue {
CrdsData::EpochSlots(_, _) => CrdsValueLabel::EpochSlots(self.pubkey()), CrdsData::EpochSlots(_, _) => CrdsValueLabel::EpochSlots(self.pubkey()),
CrdsData::SnapshotHashes(_) => CrdsValueLabel::SnapshotHashes(self.pubkey()), CrdsData::SnapshotHashes(_) => CrdsValueLabel::SnapshotHashes(self.pubkey()),
CrdsData::AccountsHashes(_) => CrdsValueLabel::AccountsHashes(self.pubkey()), CrdsData::AccountsHashes(_) => CrdsValueLabel::AccountsHashes(self.pubkey()),
CrdsData::Version(_) => CrdsValueLabel::Version(self.pubkey()),
} }
} }
pub fn contact_info(&self) -> Option<&ContactInfo> { pub fn contact_info(&self) -> Option<&ContactInfo> {
@ -347,6 +382,13 @@ impl CrdsValue {
} }
} }
pub fn version(&self) -> Option<&Version> {
match &self.data {
CrdsData::Version(version) => Some(version),
_ => None,
}
}
/// Return all the possible labels for a record identified by Pubkey. /// Return all the possible labels for a record identified by Pubkey.
pub fn record_labels(key: &Pubkey) -> Vec<CrdsValueLabel> { pub fn record_labels(key: &Pubkey) -> Vec<CrdsValueLabel> {
let mut labels = vec![ let mut labels = vec![
@ -354,6 +396,7 @@ impl CrdsValue {
CrdsValueLabel::EpochSlots(*key), CrdsValueLabel::EpochSlots(*key),
CrdsValueLabel::SnapshotHashes(*key), CrdsValueLabel::SnapshotHashes(*key),
CrdsValueLabel::AccountsHashes(*key), CrdsValueLabel::AccountsHashes(*key),
CrdsValueLabel::Version(*key),
]; ];
labels.extend((0..MAX_VOTES).map(|ix| CrdsValueLabel::Vote(ix, *key))); labels.extend((0..MAX_VOTES).map(|ix| CrdsValueLabel::Vote(ix, *key)));
labels labels
@ -403,7 +446,7 @@ mod test {
#[test] #[test]
fn test_labels() { fn test_labels() {
let mut hits = [false; 4 + MAX_VOTES as usize]; let mut hits = [false; 5 + MAX_VOTES as usize];
// this method should cover all the possible labels // this method should cover all the possible labels
for v in &CrdsValue::record_labels(&Pubkey::default()) { for v in &CrdsValue::record_labels(&Pubkey::default()) {
match v { match v {
@ -411,7 +454,8 @@ mod test {
CrdsValueLabel::EpochSlots(_) => hits[1] = true, CrdsValueLabel::EpochSlots(_) => hits[1] = true,
CrdsValueLabel::SnapshotHashes(_) => hits[2] = true, CrdsValueLabel::SnapshotHashes(_) => hits[2] = true,
CrdsValueLabel::AccountsHashes(_) => hits[3] = true, CrdsValueLabel::AccountsHashes(_) => hits[3] = true,
CrdsValueLabel::Vote(ix, _) => hits[*ix as usize + 4] = true, CrdsValueLabel::Version(_) => hits[4] = true,
CrdsValueLabel::Vote(ix, _) => hits[*ix as usize + 5] = true,
} }
} }
assert!(hits.iter().all(|x| *x)); assert!(hits.iter().all(|x| *x));

View File

@ -1066,6 +1066,9 @@ impl RpcSol for RpcSolImpl {
gossip: Some(contact_info.gossip), gossip: Some(contact_info.gossip),
tpu: valid_address_or_none(&contact_info.tpu), tpu: valid_address_or_none(&contact_info.tpu),
rpc: valid_address_or_none(&contact_info.rpc), rpc: valid_address_or_none(&contact_info.rpc),
version: cluster_info
.get_node_version(&contact_info.id)
.map(|v| v.to_string()),
}) })
} else { } else {
None // Exclude spy nodes None // Exclude spy nodes
@ -1468,7 +1471,7 @@ impl RpcSol for RpcSolImpl {
fn get_version(&self, _: Self::Metadata) -> Result<RpcVersionInfo> { fn get_version(&self, _: Self::Metadata) -> Result<RpcVersionInfo> {
Ok(RpcVersionInfo { Ok(RpcVersionInfo {
solana_core: solana_clap_utils::version!().to_string(), solana_core: solana_version::Version::default().to_string(),
}) })
} }
@ -1835,7 +1838,7 @@ pub mod tests {
.expect("actual response deserialization"); .expect("actual response deserialization");
let expected = format!( let expected = format!(
r#"{{"jsonrpc":"2.0","result":[{{"pubkey": "{}", "gossip": "127.0.0.1:1235", "tpu": "127.0.0.1:1234", "rpc": "127.0.0.1:{}"}}],"id":1}}"#, r#"{{"jsonrpc":"2.0","result":[{{"pubkey": "{}", "gossip": "127.0.0.1:1235", "tpu": "127.0.0.1:1234", "rpc": "127.0.0.1:{}", "version": null}}],"id":1}}"#,
leader_pubkey, leader_pubkey,
rpc_port::DEFAULT_RPC_PORT rpc_port::DEFAULT_RPC_PORT
); );
@ -2637,7 +2640,7 @@ pub mod tests {
let expected = json!({ let expected = json!({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"result": { "result": {
"solana-core": solana_clap_utils::version!().to_string() "solana-core": solana_version::version!().to_string()
}, },
"id": 1 "id": 1
}); });

View File

@ -40,7 +40,7 @@ fn test_rpc_client() {
assert_eq!( assert_eq!(
client.get_version().unwrap().solana_core, client.get_version().unwrap().solana_core,
solana_clap_utils::version!() solana_version::version!()
); );
assert!(client.get_account(&bob_pubkey).is_err()); assert!(client.get_account(&bob_pubkey).is_err());

View File

@ -258,7 +258,8 @@ The result field will be an array of JSON objects, each with the following sub f
* `pubkey: <string>` - Node public key, as base-58 encoded string * `pubkey: <string>` - Node public key, as base-58 encoded string
* `gossip: <string>` - Gossip network address for the node * `gossip: <string>` - Gossip network address for the node
* `tpu: <string>` - TPU network address for the node * `tpu: <string>` - TPU network address for the node
* `rpc: <string>` - JSON RPC network address for the node, or `null` if the JSON RPC service is not enabled * `rpc: <string>|null` - JSON RPC network address for the node, or `null` if the JSON RPC service is not enabled
* `version: <string>|null` - The software version of the node, or `null` if the version information is not available
#### Example: #### Example:
@ -267,7 +268,7 @@ The result field will be an array of JSON objects, each with the following sub f
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getClusterNodes"}' http://localhost:8899 curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getClusterNodes"}' http://localhost:8899
// Result // Result
{"jsonrpc":"2.0","result":[{"gossip":"10.239.6.48:8001","pubkey":"9QzsJf7LPLj8GkXbYT3LFDKqsj2hHG7TA3xinJHu8epQ","rpc":"10.239.6.48:8899","tpu":"10.239.6.48:8856"}],"id":1} {"jsonrpc":"2.0","result":[{"gossip":"10.239.6.48:8001","pubkey":"9QzsJf7LPLj8GkXbYT3LFDKqsj2hHG7TA3xinJHu8epQ","rpc":"10.239.6.48:8899","tpu":"10.239.6.48:8856"},"version":"1.0.0 c375ce1f"],"id":1}
``` ```
### getConfirmedBlock ### getConfirmedBlock

View File

@ -27,6 +27,7 @@ use solana_ledger::bank_forks::SnapshotConfig;
use solana_perf::recycler::enable_recycler_warming; use solana_perf::recycler::enable_recycler_warming;
use solana_sdk::{ use solana_sdk::{
clock::Slot, clock::Slot,
commitment_config::CommitmentConfig,
genesis_config::GenesisConfig, genesis_config::GenesisConfig,
hash::Hash, hash::Hash,
pubkey::Pubkey, pubkey::Pubkey,
@ -398,8 +399,10 @@ fn check_vote_account(
node_pubkey: &Pubkey, node_pubkey: &Pubkey,
) -> Result<(), String> { ) -> Result<(), String> {
let found_vote_account = rpc_client let found_vote_account = rpc_client
.get_account(vote_pubkey) .get_account_with_commitment(vote_pubkey, CommitmentConfig::root())
.map_err(|err| format!("Failed to get vote account: {}", err.to_string()))?; .map_err(|err| format!("Failed to get vote account: {}", err.to_string()))?
.value
.ok_or_else(|| format!("vote account does not exist: {}", vote_pubkey))?;
if found_vote_account.owner != solana_vote_program::id() { if found_vote_account.owner != solana_vote_program::id() {
return Err(format!( return Err(format!(
@ -409,8 +412,10 @@ fn check_vote_account(
} }
let found_node_account = rpc_client let found_node_account = rpc_client
.get_account(node_pubkey) .get_account_with_commitment(node_pubkey, CommitmentConfig::root())
.map_err(|err| format!("Failed to get identity account: {}", err.to_string()))?; .map_err(|err| format!("Failed to get identity account: {}", err.to_string()))?
.value
.ok_or_else(|| format!("identity account does not exist: {}", node_pubkey))?;
let found_vote_account = solana_vote_program::vote_state::VoteState::from(&found_vote_account); let found_vote_account = solana_vote_program::vote_state::VoteState::from(&found_vote_account);
if let Some(found_vote_account) = found_vote_account { if let Some(found_vote_account) = found_vote_account {
@ -1282,7 +1287,7 @@ pub fn main() {
}) })
.and_then(|_| { .and_then(|_| {
if let Some(snapshot_hash) = snapshot_hash { if let Some(snapshot_hash) = snapshot_hash {
rpc_client.get_slot() rpc_client.get_slot_with_commitment(CommitmentConfig::root())
.map_err(|err| format!("Failed to get RPC node slot: {}", err)) .map_err(|err| format!("Failed to get RPC node slot: {}", err))
.and_then(|slot| { .and_then(|slot| {
info!("RPC node root slot: {}", slot); info!("RPC node root slot: {}", slot);

2
version/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target/
/farf/

20
version/Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "solana-version"
version = "1.1.11"
description = "Solana Version"
authors = ["Solana Maintainers <maintainers@solana.com>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
edition = "2018"
[dependencies]
serde = "1.0.105"
serde_derive = "1.0.103"
solana-sdk = { path = "../sdk", version = "1.0.23" }
[lib]
name = "solana_version"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

70
version/src/lib.rs Normal file
View File

@ -0,0 +1,70 @@
extern crate serde_derive;
use serde_derive::{Deserialize, Serialize};
use solana_sdk::sanitize::Sanitize;
use std::fmt;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Version {
major: u16,
minor: u16,
patch: u16,
commit: Option<u32>, // first 4 bytes of the sha1 commit hash
}
fn compute_commit(sha1: Option<&'static str>) -> Option<u32> {
let sha1 = sha1?;
if sha1.len() < 8 {
None
} else {
u32::from_str_radix(&sha1[..8], 16).ok()
}
}
impl Default for Version {
fn default() -> Self {
Self {
major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
commit: compute_commit(option_env!("CI_COMMIT")),
}
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}.{}.{} {}",
self.major,
self.minor,
self.patch,
match self.commit {
None => "devbuild".to_string(),
Some(commit) => format!("{:08x}", commit),
}
)
}
}
impl Sanitize for Version {}
#[macro_export]
macro_rules! version {
() => {
&*format!("{}", $crate::Version::default())
};
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_compute_commit() {
assert_eq!(compute_commit(None), None);
assert_eq!(compute_commit(Some("1234567890")), Some(0x12345678));
assert_eq!(compute_commit(Some("HEAD")), None);
assert_eq!(compute_commit(Some("garbagein")), None);
}
}