caching
This commit is contained in:
parent
6a535099bb
commit
009173a97d
49 changed files with 1788 additions and 389 deletions
|
|
@ -10,6 +10,7 @@ iota-installer = { path = "../iota-installer" }
|
|||
iota-core = { path = "../iota-core" }
|
||||
iota-process-manager = { path = "../iota-process-manager" }
|
||||
iota-paths = { path = "../iota-paths" }
|
||||
iota-terms = { path = "../iota-terms" }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
serde_json = "1"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum, error::ErrorKind};
|
||||
use iota_cli::theme::ThemeName;
|
||||
use iota_terms::TermsType;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CliInvocation {
|
||||
|
|
@ -25,61 +26,220 @@ pub enum OutputFormat {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
enum CliTheme { Monospace, Binary, Ansi, Surface }
|
||||
enum CliTheme {
|
||||
Monospace,
|
||||
Binary,
|
||||
Ansi,
|
||||
Surface,
|
||||
}
|
||||
impl From<CliTheme> for ThemeName {
|
||||
fn from(value: CliTheme) -> Self {
|
||||
match value { CliTheme::Monospace => Self::Monospace, CliTheme::Binary => Self::Binary, CliTheme::Ansi => Self::Ansi, CliTheme::Surface => Self::Surface }
|
||||
match value {
|
||||
CliTheme::Monospace => Self::Monospace,
|
||||
CliTheme::Binary => Self::Binary,
|
||||
CliTheme::Ansi => Self::Ansi,
|
||||
CliTheme::Surface => Self::Surface,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "iota", version, about = "Iota operator console", arg_required_else_help = false)]
|
||||
#[command(
|
||||
name = "iota",
|
||||
version,
|
||||
about = "Iota operator console",
|
||||
arg_required_else_help = false
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(long, global = true, value_enum)] theme: Option<CliTheme>,
|
||||
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Text)] output: OutputFormat,
|
||||
#[arg(long, global = true, value_enum, default_value_t = CapabilityPolicy::Auto)] color: CapabilityPolicy,
|
||||
#[arg(long, global = true, value_enum, default_value_t = CapabilityPolicy::Auto)] unicode: CapabilityPolicy,
|
||||
#[arg(long, global = true)] no_color: bool,
|
||||
#[command(subcommand)] command: Option<CliCommand>,
|
||||
#[arg(long, global = true, value_enum)]
|
||||
theme: Option<CliTheme>,
|
||||
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Text)]
|
||||
output: OutputFormat,
|
||||
#[arg(long, global = true, value_enum, default_value_t = CapabilityPolicy::Auto)]
|
||||
color: CapabilityPolicy,
|
||||
#[arg(long, global = true, value_enum, default_value_t = CapabilityPolicy::Auto)]
|
||||
unicode: CapabilityPolicy,
|
||||
#[arg(long, global = true)]
|
||||
no_color: bool,
|
||||
#[command(subcommand)]
|
||||
command: Option<CliCommand>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum CliCommand {
|
||||
Status, Tasks,
|
||||
Users(UsersArgs), Omikron(OmikronArgs), Identity(IdentityArgs), Daemon(DaemonArgs), Config(ConfigArgs),
|
||||
RegenerateKeys { #[arg(long)] yes: bool },
|
||||
Status,
|
||||
Tasks,
|
||||
Users(UsersArgs),
|
||||
Omikron(OmikronArgs),
|
||||
Identity(IdentityArgs),
|
||||
Daemon(DaemonArgs),
|
||||
Config(ConfigArgs),
|
||||
Terms(TermsArgs),
|
||||
RegenerateKeys {
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
Components,
|
||||
Logs { #[arg(long, default_value_t = 100)] limit: usize },
|
||||
Logs {
|
||||
#[arg(long, default_value_t = 100)]
|
||||
limit: usize,
|
||||
},
|
||||
Update(UpdateArgs),
|
||||
Community(CommunityArgs),
|
||||
Completions { shell: String },
|
||||
Completions {
|
||||
shell: String,
|
||||
},
|
||||
Man,
|
||||
}
|
||||
#[derive(Args, Debug)] struct UsersArgs { #[command(subcommand)] action: UsersAction }
|
||||
#[derive(Subcommand, Debug)] enum UsersAction { List, Show { user_id: i64 }, Add { username: String }, Remove { user_id: i64, #[arg(long)] yes: bool }, Import { username: String } }
|
||||
#[derive(Args, Debug)] struct OmikronArgs { #[command(subcommand)] action: OmikronAction }
|
||||
#[derive(Subcommand, Debug)] enum OmikronAction { Reconnect, Status }
|
||||
#[derive(Args, Debug)] struct IdentityArgs { #[command(subcommand)] action: IdentityAction }
|
||||
#[derive(Subcommand, Debug)] enum IdentityAction { Rotate { #[arg(long)] yes: bool } }
|
||||
#[derive(Args, Debug)] struct ConfigArgs { #[command(subcommand)] action: ConfigAction }
|
||||
#[derive(Subcommand, Debug)] enum ConfigAction { Get, Set { key: String, value: String }, Reload }
|
||||
#[derive(Args, Debug)] struct DaemonArgs { #[command(subcommand)] action: DaemonAction }
|
||||
#[derive(Subcommand, Debug)] enum DaemonAction {
|
||||
Restart { #[arg(long)] yes: bool }, Stop { #[arg(long)] yes: bool },
|
||||
Enable { #[arg(long, value_parser = ["socket", "always-on"])] mode: String }, DisableStartup, Status, StartupStatus, Start, RestartService, StopService,
|
||||
Install { #[arg(long)] bundle: String, #[arg(long)] operator: Option<String> },
|
||||
#[derive(Args, Debug)]
|
||||
struct UsersArgs {
|
||||
#[command(subcommand)]
|
||||
action: UsersAction,
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum UsersAction {
|
||||
List,
|
||||
Show {
|
||||
user_id: i64,
|
||||
},
|
||||
Add {
|
||||
username: String,
|
||||
},
|
||||
Remove {
|
||||
user_id: i64,
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
Import {
|
||||
username: String,
|
||||
},
|
||||
}
|
||||
#[derive(Args, Debug)]
|
||||
struct OmikronArgs {
|
||||
#[command(subcommand)]
|
||||
action: OmikronAction,
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum OmikronAction {
|
||||
Reconnect,
|
||||
Status,
|
||||
}
|
||||
#[derive(Args, Debug)]
|
||||
struct IdentityArgs {
|
||||
#[command(subcommand)]
|
||||
action: IdentityAction,
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum IdentityAction {
|
||||
Rotate {
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
#[derive(Args, Debug)]
|
||||
struct ConfigArgs {
|
||||
#[command(subcommand)]
|
||||
action: ConfigAction,
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum ConfigAction {
|
||||
Get,
|
||||
Set { key: String, value: String },
|
||||
Reload,
|
||||
}
|
||||
#[derive(Args, Debug)]
|
||||
struct DaemonArgs {
|
||||
#[command(subcommand)]
|
||||
action: DaemonAction,
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum DaemonAction {
|
||||
Restart {
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
Stop {
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
Enable {
|
||||
#[arg(long, value_parser = ["socket", "always-on"])]
|
||||
mode: String,
|
||||
},
|
||||
DisableStartup,
|
||||
Status,
|
||||
StartupStatus,
|
||||
Start,
|
||||
RestartService,
|
||||
StopService,
|
||||
Install {
|
||||
#[arg(long)]
|
||||
bundle: String,
|
||||
#[arg(long)]
|
||||
operator: Option<String>,
|
||||
},
|
||||
}
|
||||
#[derive(Args, Debug)]
|
||||
struct UpdateArgs {
|
||||
#[command(subcommand)]
|
||||
action: UpdateAction,
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum UpdateAction {
|
||||
Check,
|
||||
}
|
||||
#[derive(Args, Debug)]
|
||||
struct CommunityArgs {
|
||||
#[command(subcommand)]
|
||||
action: CommunityAction,
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum CommunityAction {
|
||||
List,
|
||||
}
|
||||
#[derive(Args, Debug)]
|
||||
struct TermsArgs {
|
||||
#[command(subcommand)]
|
||||
action: TermsAction,
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum TermsAction {
|
||||
Status {
|
||||
#[arg(long)]
|
||||
system: bool,
|
||||
},
|
||||
Show {
|
||||
document: TermsDocument,
|
||||
},
|
||||
Accept {
|
||||
#[arg(long)]
|
||||
system: bool,
|
||||
},
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
enum TermsDocument {
|
||||
Eula,
|
||||
Tos,
|
||||
Privacy,
|
||||
}
|
||||
impl From<TermsDocument> for TermsType {
|
||||
fn from(value: TermsDocument) -> Self {
|
||||
match value {
|
||||
TermsDocument::Eula => TermsType::EULA,
|
||||
TermsDocument::Tos => TermsType::TOS,
|
||||
TermsDocument::Privacy => TermsType::PP,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Args, Debug)] struct UpdateArgs { #[command(subcommand)] action: UpdateAction }
|
||||
#[derive(Subcommand, Debug)] enum UpdateAction { Check }
|
||||
#[derive(Args, Debug)] struct CommunityArgs { #[command(subcommand)] action: CommunityAction }
|
||||
#[derive(Subcommand, Debug)] enum CommunityAction { List }
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Command {
|
||||
Dashboard,
|
||||
Help,
|
||||
Version,
|
||||
Completions { shell: String },
|
||||
Completions {
|
||||
shell: String,
|
||||
},
|
||||
ManPage,
|
||||
Install {
|
||||
bundle: String,
|
||||
|
|
@ -88,7 +248,9 @@ pub enum Command {
|
|||
Status,
|
||||
Tasks,
|
||||
UsersList,
|
||||
UsersShow { user_id: i64 },
|
||||
UsersShow {
|
||||
user_id: i64,
|
||||
},
|
||||
UsersAdd {
|
||||
username: String,
|
||||
},
|
||||
|
|
@ -96,7 +258,9 @@ pub enum Command {
|
|||
user_id: i64,
|
||||
confirmed: bool,
|
||||
},
|
||||
UsersImport { username: String },
|
||||
UsersImport {
|
||||
username: String,
|
||||
},
|
||||
OmikronReconnect,
|
||||
IdentityRotate {
|
||||
confirmed: bool,
|
||||
|
|
@ -117,16 +281,30 @@ pub enum Command {
|
|||
DaemonRestartService,
|
||||
DaemonStopService,
|
||||
ConfigGet,
|
||||
ConfigSet { key: String, value: String },
|
||||
ConfigSet {
|
||||
key: String,
|
||||
value: String,
|
||||
},
|
||||
ConfigReload,
|
||||
OmikronStatus,
|
||||
RegenerateKeys {
|
||||
confirmed: bool,
|
||||
},
|
||||
Components,
|
||||
Logs { limit: usize },
|
||||
Logs {
|
||||
limit: usize,
|
||||
},
|
||||
UpdateCheck,
|
||||
CommunityList,
|
||||
TermsStatus {
|
||||
system: bool,
|
||||
},
|
||||
TermsShow {
|
||||
document: TermsType,
|
||||
},
|
||||
TermsAccept {
|
||||
system: bool,
|
||||
},
|
||||
}
|
||||
impl CliInvocation {
|
||||
pub fn parse(args: impl IntoIterator<Item = String>) -> Result<Self, String> {
|
||||
|
|
@ -134,13 +312,14 @@ impl CliInvocation {
|
|||
if args.as_slice() == ["help"] {
|
||||
return Ok(Self::special(Command::Help));
|
||||
}
|
||||
let parsed = Cli::try_parse_from(std::iter::once("iota".to_owned()).chain(args)).map_err(|error| {
|
||||
match error.kind() {
|
||||
ErrorKind::DisplayHelp => return "__help__".to_owned(),
|
||||
ErrorKind::DisplayVersion => return "__version__".to_owned(),
|
||||
_ => error.to_string(),
|
||||
}
|
||||
});
|
||||
let parsed =
|
||||
Cli::try_parse_from(std::iter::once("iota".to_owned()).chain(args)).map_err(|error| {
|
||||
match error.kind() {
|
||||
ErrorKind::DisplayHelp => return "__help__".to_owned(),
|
||||
ErrorKind::DisplayVersion => return "__version__".to_owned(),
|
||||
_ => error.to_string(),
|
||||
}
|
||||
});
|
||||
let parsed = match parsed {
|
||||
Ok(parsed) => parsed,
|
||||
Err(marker) if marker == "__help__" => return Ok(Self::special(Command::Help)),
|
||||
|
|
@ -154,33 +333,77 @@ impl CliInvocation {
|
|||
Some(CliCommand::Components) => Command::Components,
|
||||
Some(CliCommand::Completions { shell }) => Command::Completions { shell },
|
||||
Some(CliCommand::Man) => Command::ManPage,
|
||||
Some(CliCommand::Users(users)) => match users.action { UsersAction::List => Command::UsersList, UsersAction::Show { user_id } => Command::UsersShow { user_id }, UsersAction::Add { username } => Command::UsersAdd { username }, UsersAction::Remove { user_id, yes } => Command::UsersRemove { user_id, confirmed: yes }, UsersAction::Import { username } => Command::UsersImport { username } },
|
||||
Some(CliCommand::Omikron(omikron)) => match omikron.action { OmikronAction::Reconnect => Command::OmikronReconnect, OmikronAction::Status => Command::OmikronStatus },
|
||||
Some(CliCommand::Identity(identity)) => match identity.action { IdentityAction::Rotate { yes } => Command::IdentityRotate { confirmed: yes } },
|
||||
Some(CliCommand::Config(config)) => match config.action { ConfigAction::Get => Command::ConfigGet, ConfigAction::Set { key, value } => Command::ConfigSet { key, value }, ConfigAction::Reload => Command::ConfigReload },
|
||||
Some(CliCommand::Users(users)) => match users.action {
|
||||
UsersAction::List => Command::UsersList,
|
||||
UsersAction::Show { user_id } => Command::UsersShow { user_id },
|
||||
UsersAction::Add { username } => Command::UsersAdd { username },
|
||||
UsersAction::Remove { user_id, yes } => Command::UsersRemove {
|
||||
user_id,
|
||||
confirmed: yes,
|
||||
},
|
||||
UsersAction::Import { username } => Command::UsersImport { username },
|
||||
},
|
||||
Some(CliCommand::Omikron(omikron)) => match omikron.action {
|
||||
OmikronAction::Reconnect => Command::OmikronReconnect,
|
||||
OmikronAction::Status => Command::OmikronStatus,
|
||||
},
|
||||
Some(CliCommand::Identity(identity)) => match identity.action {
|
||||
IdentityAction::Rotate { yes } => Command::IdentityRotate { confirmed: yes },
|
||||
},
|
||||
Some(CliCommand::Config(config)) => match config.action {
|
||||
ConfigAction::Get => Command::ConfigGet,
|
||||
ConfigAction::Set { key, value } => Command::ConfigSet { key, value },
|
||||
ConfigAction::Reload => Command::ConfigReload,
|
||||
},
|
||||
Some(CliCommand::RegenerateKeys { yes }) => Command::RegenerateKeys { confirmed: yes },
|
||||
Some(CliCommand::Logs { limit }) => Command::Logs { limit },
|
||||
Some(CliCommand::Update(update)) => match update.action { UpdateAction::Check => Command::UpdateCheck },
|
||||
Some(CliCommand::Community(community)) => match community.action { CommunityAction::List => Command::CommunityList },
|
||||
Some(CliCommand::Update(update)) => match update.action {
|
||||
UpdateAction::Check => Command::UpdateCheck,
|
||||
},
|
||||
Some(CliCommand::Community(community)) => match community.action {
|
||||
CommunityAction::List => Command::CommunityList,
|
||||
},
|
||||
Some(CliCommand::Terms(terms)) => match terms.action {
|
||||
TermsAction::Status { system } => Command::TermsStatus { system },
|
||||
TermsAction::Show { document } => Command::TermsShow {
|
||||
document: document.into(),
|
||||
},
|
||||
TermsAction::Accept { system } => Command::TermsAccept { system },
|
||||
},
|
||||
Some(CliCommand::Daemon(daemon)) => match daemon.action {
|
||||
DaemonAction::Restart { yes } => Command::DaemonRestart { confirmed: yes }, DaemonAction::Stop { yes } => Command::DaemonStop { confirmed: yes },
|
||||
DaemonAction::Enable { mode } => Command::DaemonEnable { mode }, DaemonAction::DisableStartup => Command::DaemonDisableStartup,
|
||||
DaemonAction::Status => Command::DaemonDaemonStatus, DaemonAction::StartupStatus => Command::DaemonStartupStatus,
|
||||
DaemonAction::Start => Command::DaemonStart, DaemonAction::RestartService => Command::DaemonRestartService, DaemonAction::StopService => Command::DaemonStopService,
|
||||
DaemonAction::Restart { yes } => Command::DaemonRestart { confirmed: yes },
|
||||
DaemonAction::Stop { yes } => Command::DaemonStop { confirmed: yes },
|
||||
DaemonAction::Enable { mode } => Command::DaemonEnable { mode },
|
||||
DaemonAction::DisableStartup => Command::DaemonDisableStartup,
|
||||
DaemonAction::Status => Command::DaemonDaemonStatus,
|
||||
DaemonAction::StartupStatus => Command::DaemonStartupStatus,
|
||||
DaemonAction::Start => Command::DaemonStart,
|
||||
DaemonAction::RestartService => Command::DaemonRestartService,
|
||||
DaemonAction::StopService => Command::DaemonStopService,
|
||||
DaemonAction::Install { bundle, operator } => Command::Install { bundle, operator },
|
||||
},
|
||||
};
|
||||
Ok(Self {
|
||||
theme_override: parsed.theme.map(Into::into),
|
||||
output: parsed.output,
|
||||
color: if parsed.no_color { CapabilityPolicy::Never } else { parsed.color },
|
||||
color: if parsed.no_color {
|
||||
CapabilityPolicy::Never
|
||||
} else {
|
||||
parsed.color
|
||||
},
|
||||
unicode: parsed.unicode,
|
||||
command,
|
||||
})
|
||||
}
|
||||
|
||||
fn special(command: Command) -> Self {
|
||||
Self { theme_override: None, output: OutputFormat::Text, color: CapabilityPolicy::Auto, unicode: CapabilityPolicy::Auto, command }
|
||||
Self {
|
||||
theme_override: None,
|
||||
output: OutputFormat::Text,
|
||||
color: CapabilityPolicy::Auto,
|
||||
unicode: CapabilityPolicy::Auto,
|
||||
command,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help_text() -> String {
|
||||
|
|
@ -237,12 +460,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parses_terminal_capability_overrides() {
|
||||
let invocation = CliInvocation::parse([
|
||||
"--color=never".into(),
|
||||
"--unicode".into(),
|
||||
"always".into(),
|
||||
])
|
||||
.unwrap();
|
||||
let invocation =
|
||||
CliInvocation::parse(["--color=never".into(), "--unicode".into(), "always".into()])
|
||||
.unwrap();
|
||||
assert_eq!(invocation.color, CapabilityPolicy::Never);
|
||||
assert_eq!(invocation.unicode, CapabilityPolicy::Always);
|
||||
assert_eq!(invocation.command, Command::Dashboard);
|
||||
|
|
|
|||
|
|
@ -65,19 +65,23 @@ pub async fn run(
|
|||
endpoints: &DaemonEndpoints,
|
||||
caps: Capabilities,
|
||||
) -> Result<ConnectionContext, StartupError> {
|
||||
// Try connecting to an already-running daemon before starting a new one.
|
||||
// The initial dashboard probe may have raced a daemon that was still
|
||||
// accepting connections. Re-check both endpoints before offering setup:
|
||||
// a system-managed daemon normally listens on a different socket from a
|
||||
// locally launched one.
|
||||
if let Ok(ipc) = IpcClient::connect(&endpoints.local).await {
|
||||
return Ok(ConnectionContext { ipc });
|
||||
}
|
||||
if endpoints.system != endpoints.local {
|
||||
if let Ok(ipc) = IpcClient::connect(&endpoints.system).await {
|
||||
return Ok(ConnectionContext { ipc });
|
||||
}
|
||||
}
|
||||
let options = caps.options();
|
||||
if !options.iter().any(|o| o.enabled) {
|
||||
let _ = show(
|
||||
ui,
|
||||
options,
|
||||
"Daemon cannot be started. Correct the reported problem, then Retry, or Exit.",
|
||||
)
|
||||
.await?;
|
||||
return Err(StartupError::Cancelled);
|
||||
return Err(StartupError::Other(
|
||||
"No running daemon could be reached, and no daemon launch method is available.".into(),
|
||||
));
|
||||
}
|
||||
if UiConfig::load()
|
||||
.map(|c| c.daemon_start_policy == iota_cli::theme::DaemonStartPolicy::WithUi)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ mod cli_args;
|
|||
mod daemon_setup_flow;
|
||||
mod local_daemon;
|
||||
mod startup_error;
|
||||
mod terms;
|
||||
|
||||
use cli_args::{CapabilityPolicy, CliInvocation, Command, OutputFormat};
|
||||
use startup_error::StartupError;
|
||||
|
|
@ -78,6 +79,9 @@ async fn run() -> Result<(), StartupError> {
|
|||
print_man_page();
|
||||
Ok(())
|
||||
}
|
||||
Command::TermsStatus { system } => terms::run(terms::TermsCommand::Status { system }).await,
|
||||
Command::TermsShow { document } => terms::run(terms::TermsCommand::Show { document }).await,
|
||||
Command::TermsAccept { system } => terms::run(terms::TermsCommand::Accept { system }).await,
|
||||
Command::Install { bundle, operator } => {
|
||||
iota_installer::install_linux_bundle_with_operator(
|
||||
Path::new(&bundle),
|
||||
|
|
@ -98,11 +102,13 @@ async fn run() -> Result<(), StartupError> {
|
|||
return run_startup_command(command).await;
|
||||
}
|
||||
if !matches!(command, Command::Dashboard) {
|
||||
match iota_core::consent_state::non_interactive_consent() {
|
||||
iota_core::consent_state::NonInteractiveConsent::Accepted => {}
|
||||
iota_core::consent_state::NonInteractiveConsent::RequiresInteractiveAcceptance => {
|
||||
return Err(StartupError::Consent("Run `iota` in an interactive terminal to review and accept the required terms.".into()));
|
||||
}
|
||||
let state_dir = iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
|
||||
.map_err(|error| {
|
||||
StartupError::Other(format!("Cannot resolve consent storage: {error}"))
|
||||
})?
|
||||
.state_dir;
|
||||
if !iota_terms::consent::load(&state_dir).has_all_required() {
|
||||
return Err(StartupError::Consent("Run `iota terms accept` in an interactive terminal to review and accept the required terms.".into()));
|
||||
}
|
||||
let ipc = tokio::select! {
|
||||
result = connect_available(&endpoints) => result?,
|
||||
|
|
@ -242,8 +248,7 @@ async fn run_dashboard(
|
|||
CapabilityPolicy::Always => true,
|
||||
CapabilityPolicy::Never => false,
|
||||
CapabilityPolicy::Auto => {
|
||||
std::env::var_os("NO_COLOR").is_none()
|
||||
&& std::env::var("TERM").as_deref() != Ok("dumb")
|
||||
std::env::var_os("NO_COLOR").is_none() && std::env::var("TERM").as_deref() != Ok("dumb")
|
||||
}
|
||||
};
|
||||
let unicode_enabled = match unicode_policy {
|
||||
|
|
@ -278,6 +283,7 @@ async fn run_dashboard(
|
|||
if consent != (true, true) {
|
||||
return Err(StartupError::Consent("Cannot continue until the required terms are accepted.".into()));
|
||||
}
|
||||
persist_dashboard_consent().await?;
|
||||
let initial = tokio::select! {
|
||||
result = connect_available(&endpoints) => result,
|
||||
_ = ui.wait_for_shutdown() => Err(StartupError::Cancelled),
|
||||
|
|
@ -328,6 +334,20 @@ async fn run_dashboard(
|
|||
}
|
||||
}
|
||||
|
||||
async fn persist_dashboard_consent() -> Result<(), StartupError> {
|
||||
let docs = iota_terms::get_current_docs().await.ok_or_else(|| {
|
||||
StartupError::Consent("Could not verify the current agreements after acceptance.".into())
|
||||
})?;
|
||||
let paths = iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
|
||||
.map_err(|error| StartupError::Other(format!("Cannot resolve consent storage: {error}")))?;
|
||||
let mut record = iota_terms::consent::load(&paths.state_dir);
|
||||
for document in [&docs.0, &docs.1, &docs.2] {
|
||||
record.accept(document);
|
||||
}
|
||||
iota_terms::consent::save(&paths.state_dir, &record)
|
||||
.map_err(|error| StartupError::Consent(format!("Could not save consent: {error}")))
|
||||
}
|
||||
|
||||
fn map_process_manager_error(error: iota_process_manager::ProcessManagerError) -> StartupError {
|
||||
use iota_process_manager::ProcessManagerErrorKind::*;
|
||||
match error.kind() {
|
||||
|
|
@ -457,11 +477,21 @@ async fn run_command(
|
|||
"Refusing destructive command without --yes.".into(),
|
||||
));
|
||||
}
|
||||
Command::Dashboard | Command::Help | Command::Version | Command::Completions { .. }
|
||||
| Command::ManPage | Command::Install { .. }
|
||||
| Command::DaemonEnable { .. } | Command::DaemonDisableStartup
|
||||
| Command::DaemonStartupStatus | Command::DaemonStart
|
||||
| Command::DaemonRestartService | Command::DaemonStopService => {
|
||||
Command::Dashboard
|
||||
| Command::Help
|
||||
| Command::Version
|
||||
| Command::Completions { .. }
|
||||
| Command::ManPage
|
||||
| Command::Install { .. }
|
||||
| Command::TermsStatus { .. }
|
||||
| Command::TermsShow { .. }
|
||||
| Command::TermsAccept { .. }
|
||||
| Command::DaemonEnable { .. }
|
||||
| Command::DaemonDisableStartup
|
||||
| Command::DaemonStartupStatus
|
||||
| Command::DaemonStart
|
||||
| Command::DaemonRestartService
|
||||
| Command::DaemonStopService => {
|
||||
return Err(StartupError::InvalidCommand(
|
||||
"Command cannot be run headlessly.".into(),
|
||||
));
|
||||
|
|
|
|||
118
iota/src/terms.rs
Normal file
118
iota/src/terms.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
use crate::startup_error::StartupError;
|
||||
use iota_terms::{Doc, TermsType, consent, get_current_docs, get_terms};
|
||||
use std::io::{self, IsTerminal, Write};
|
||||
|
||||
pub enum TermsCommand {
|
||||
Status { system: bool },
|
||||
Show { document: TermsType },
|
||||
Accept { system: bool },
|
||||
}
|
||||
|
||||
fn state_dir(system: bool) -> Result<std::path::PathBuf, StartupError> {
|
||||
let scope = if system {
|
||||
iota_paths::Scope::System
|
||||
} else {
|
||||
iota_paths::Scope::User
|
||||
};
|
||||
iota_paths::IotaPaths::resolve(scope)
|
||||
.map(|paths| paths.state_dir)
|
||||
.map_err(|error| StartupError::Other(format!("Cannot resolve consent storage: {error}")))
|
||||
}
|
||||
|
||||
pub async fn run(command: TermsCommand) -> Result<(), StartupError> {
|
||||
match command {
|
||||
TermsCommand::Status { system } => {
|
||||
let record = consent::load(&state_dir(system)?);
|
||||
println!(
|
||||
"EULA: {}",
|
||||
if record.eula.is_some() {
|
||||
"accepted"
|
||||
} else {
|
||||
"not accepted"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"Terms of Service: {}",
|
||||
if record.tos.is_some() {
|
||||
"accepted"
|
||||
} else {
|
||||
"not accepted"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"Privacy Policy: {}",
|
||||
if record.privacy.is_some() {
|
||||
"accepted"
|
||||
} else {
|
||||
"not accepted"
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
TermsCommand::Show { document } => {
|
||||
let text = get_terms(document).await.ok_or_else(|| {
|
||||
StartupError::Consent("Could not fetch the requested terms document.".into())
|
||||
})?;
|
||||
print!("{text}");
|
||||
Ok(())
|
||||
}
|
||||
TermsCommand::Accept { system } => accept(system).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn accept(system: bool) -> Result<(), StartupError> {
|
||||
if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
|
||||
return Err(StartupError::Consent("`iota terms accept` requires an interactive terminal so the documents can be reviewed.".into()));
|
||||
}
|
||||
let documents = get_current_docs().await.ok_or_else(|| {
|
||||
StartupError::Consent(
|
||||
"Could not fetch the current agreements from the legal endpoint.".into(),
|
||||
)
|
||||
})?;
|
||||
let mut record = consent::load(&state_dir(system)?);
|
||||
for document in [&documents.0, &documents.1, &documents.2] {
|
||||
let text = get_terms(document.doc_type).await.ok_or_else(|| {
|
||||
StartupError::Consent(format!(
|
||||
"Could not fetch {}.",
|
||||
document.doc_type.to_string()
|
||||
))
|
||||
})?;
|
||||
println!(
|
||||
"\n===== {} =====\nVersion: {}\nDocument hash: {}\n",
|
||||
document.doc_type.to_string(),
|
||||
document.get_version(),
|
||||
document.get_hash()
|
||||
);
|
||||
print!("{text}\n");
|
||||
if !confirm(document)? {
|
||||
return Err(StartupError::Consent(
|
||||
"No terms were accepted. Iota remains inactive.".into(),
|
||||
));
|
||||
}
|
||||
record.accept(document);
|
||||
}
|
||||
consent::save(&state_dir(system)?, &record)
|
||||
.map_err(|error| StartupError::Consent(format!("Could not save consent: {error}")))?;
|
||||
println!("Terms accepted. Start Iota again to enable services.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn confirm(document: &Doc) -> Result<bool, StartupError> {
|
||||
let hash = document.get_hash();
|
||||
let prefix = hash.get(..10).unwrap_or(&hash);
|
||||
let expected = format!(
|
||||
"ACCEPT {} {} {}",
|
||||
document.doc_type.to_str().to_ascii_uppercase(),
|
||||
document.get_version(),
|
||||
prefix
|
||||
);
|
||||
print!("To accept this exact document, type:\n{expected}\n> ");
|
||||
io::stdout()
|
||||
.flush()
|
||||
.map_err(|error| StartupError::Consent(error.to_string()))?;
|
||||
let mut response = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut response)
|
||||
.map_err(|error| StartupError::Consent(error.to_string()))?;
|
||||
Ok(response.trim() == expected)
|
||||
}
|
||||
Loading…
Reference in a new issue