[WIP] paths
This commit is contained in:
parent
8b158108bb
commit
3bc5cc959a
20 changed files with 817 additions and 241 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -2222,6 +2222,7 @@ dependencies = [
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"iota-cli",
|
"iota-cli",
|
||||||
"iota-logger",
|
"iota-logger",
|
||||||
|
"iota-paths",
|
||||||
"iota-state",
|
"iota-state",
|
||||||
"iota-storage",
|
"iota-storage",
|
||||||
"iota-terms",
|
"iota-terms",
|
||||||
|
|
@ -2250,6 +2251,7 @@ dependencies = [
|
||||||
"iota-paths",
|
"iota-paths",
|
||||||
"iota-state",
|
"iota-state",
|
||||||
"iota-storage",
|
"iota-storage",
|
||||||
|
"iota-util",
|
||||||
"omikron-connector",
|
"omikron-connector",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|
@ -2301,6 +2303,7 @@ dependencies = [
|
||||||
name = "iota-logger"
|
name = "iota-logger"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"iota-paths",
|
||||||
"iota-state",
|
"iota-state",
|
||||||
"iota-util",
|
"iota-util",
|
||||||
"json",
|
"json",
|
||||||
|
|
@ -2345,6 +2348,7 @@ dependencies = [
|
||||||
"hex",
|
"hex",
|
||||||
"hkdf 0.12.4",
|
"hkdf 0.12.4",
|
||||||
"iota-logger",
|
"iota-logger",
|
||||||
|
"iota-paths",
|
||||||
"iota-state",
|
"iota-state",
|
||||||
"iota-util",
|
"iota-util",
|
||||||
"json",
|
"json",
|
||||||
|
|
@ -2390,6 +2394,7 @@ dependencies = [
|
||||||
"hex",
|
"hex",
|
||||||
"hkdf 0.12.4",
|
"hkdf 0.12.4",
|
||||||
"iota-logger",
|
"iota-logger",
|
||||||
|
"iota-paths",
|
||||||
"json",
|
"json",
|
||||||
"mtp",
|
"mtp",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
|
||||||
55
flake.nix
55
flake.nix
|
|
@ -108,22 +108,26 @@
|
||||||
cfg = config.services.iota;
|
cfg = config.services.iota;
|
||||||
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.iota-daemon or (throw "iota: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}");
|
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.iota-daemon or (throw "iota: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}");
|
||||||
|
|
||||||
|
configFormat = pkgs.formats.yaml {};
|
||||||
configFile =
|
configFile =
|
||||||
if cfg.settingsFile != null
|
if cfg.settingsFile != null
|
||||||
then cfg.settingsFile
|
then cfg.settingsFile
|
||||||
else pkgs.writeText "iota-config.json" (builtins.toJSON cfg.settings);
|
else configFormat.generate "iota-config.yaml" cfg.settings;
|
||||||
|
|
||||||
descriptionText = "Tensamin Iota daemon";
|
descriptionText = "Tensamin Iota daemon";
|
||||||
in {
|
in {
|
||||||
options.services.iota = {
|
options.services.iota = {
|
||||||
enable = lib.mkEnableOption "Enable the Iota service.";
|
enable = lib.mkEnableOption "Enable the Iota service.";
|
||||||
|
|
||||||
dataDir = lib.mkOption {
|
stateDir = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = cfg.package.passthru.dataDir or "/var/lib/iota";
|
default = "/var/lib/iota";
|
||||||
defaultText = lib.literalExpression ''config.services.iota.package.passthru.dataDir or "/var/lib/iota"'';
|
description = "Persistent mutable Iota state.";
|
||||||
description = "Directory where Iota stores its data, config, and certificates.";
|
|
||||||
};
|
};
|
||||||
|
cacheDir = lib.mkOption { type = lib.types.str; default = "/var/cache/iota"; };
|
||||||
|
runtimeDir = lib.mkOption { type = lib.types.str; default = "/run/iota"; };
|
||||||
|
logDir = lib.mkOption { type = lib.types.str; default = "/var/log/iota"; };
|
||||||
|
assetDir = lib.mkOption { type = lib.types.str; default = "${cfg.package}/share/iota/web"; };
|
||||||
|
|
||||||
certFile = lib.mkOption {
|
certFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
|
@ -164,13 +168,13 @@
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
type = lib.types.attrs;
|
type = lib.types.attrs;
|
||||||
default = {};
|
default = {};
|
||||||
description = "Configuration attributes for Iota, written to config.json.";
|
description = "Configuration attributes for Iota, written to YAML.";
|
||||||
};
|
};
|
||||||
|
|
||||||
settingsFile = lib.mkOption {
|
settingsFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = "Path to an existing config.json file to use instead of generating from settings.";
|
description = "Path to an existing YAML file to use instead of generating from settings.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -178,7 +182,7 @@
|
||||||
users.users.iota = {
|
users.users.iota = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = "iota";
|
group = "iota";
|
||||||
home = cfg.dataDir;
|
home = cfg.stateDir;
|
||||||
createHome = true;
|
createHome = true;
|
||||||
description = "Iota service user";
|
description = "Iota service user";
|
||||||
shell = pkgs.bash;
|
shell = pkgs.bash;
|
||||||
|
|
@ -194,6 +198,7 @@
|
||||||
SocketMode = "0660";
|
SocketMode = "0660";
|
||||||
SocketUser = "iota";
|
SocketUser = "iota";
|
||||||
SocketGroup = "iota";
|
SocketGroup = "iota";
|
||||||
|
DirectoryMode = "0750";
|
||||||
Backlog = 5;
|
Backlog = 5;
|
||||||
RemoveOnStop = "true";
|
RemoveOnStop = "true";
|
||||||
NonBlocking = true;
|
NonBlocking = true;
|
||||||
|
|
@ -210,28 +215,18 @@
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
User = "iota";
|
User = "iota";
|
||||||
Group = "iota";
|
Group = "iota";
|
||||||
WorkingDirectory = cfg.dataDir;
|
|
||||||
|
|
||||||
ExecStart = "${cfg.package}/bin/iota-daemon";
|
ExecStart = "${cfg.package}/bin/iota-daemon";
|
||||||
|
|
||||||
ExecStartPre = [
|
|
||||||
("+"
|
|
||||||
+ pkgs.writeShellScript "iota-setup" ''
|
|
||||||
mkdir -p ${cfg.dataDir}/certs
|
|
||||||
|
|
||||||
${lib.optionalString (cfg.certFile != null) "ln -sf ${cfg.certFile} ${cfg.dataDir}/certs/cert.pem"}
|
|
||||||
${lib.optionalString (cfg.keyFile != null) "ln -sf ${cfg.keyFile} ${cfg.dataDir}/certs/cert.key"}
|
|
||||||
|
|
||||||
install -m 644 ${configFile} ${cfg.dataDir}/config.json
|
|
||||||
|
|
||||||
chown -R iota:iota ${cfg.dataDir}
|
|
||||||
'')
|
|
||||||
];
|
|
||||||
|
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
RuntimeDirectory = "iota";
|
RuntimeDirectory = "iota";
|
||||||
RuntimeDirectoryMode = "0750";
|
RuntimeDirectoryMode = "0750";
|
||||||
|
StateDirectory = "iota";
|
||||||
|
StateDirectoryMode = "0750";
|
||||||
|
CacheDirectory = "iota";
|
||||||
|
CacheDirectoryMode = "0750";
|
||||||
|
LogsDirectory = "iota";
|
||||||
|
LogsDirectoryMode = "0750";
|
||||||
|
|
||||||
# Exit code 75 = restart requested
|
# Exit code 75 = restart requested
|
||||||
RestartPreventExitStatus = "0";
|
RestartPreventExitStatus = "0";
|
||||||
|
|
@ -248,7 +243,8 @@
|
||||||
ProtectHome = true;
|
ProtectHome = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
ReadWritePaths = [cfg.dataDir];
|
ReadWritePaths = [cfg.stateDir cfg.cacheDir cfg.runtimeDir cfg.logDir];
|
||||||
|
ReadOnlyPaths = [configFile cfg.assetDir];
|
||||||
ProtectKernelTunables = true;
|
ProtectKernelTunables = true;
|
||||||
ProtectKernelModules = true;
|
ProtectKernelModules = true;
|
||||||
ProtectControlGroups = true;
|
ProtectControlGroups = true;
|
||||||
|
|
@ -259,7 +255,14 @@
|
||||||
Environment = [
|
Environment = [
|
||||||
"BIND_ADDRESS=${cfg.bindAddress}"
|
"BIND_ADDRESS=${cfg.bindAddress}"
|
||||||
"IOTA_SOCKET=/run/iota/iota.sock"
|
"IOTA_SOCKET=/run/iota/iota.sock"
|
||||||
"IOTA_DATA_DIR=${cfg.dataDir}"
|
"IOTA_CONFIG_FILE=${configFile}"
|
||||||
|
"IOTA_STATE_DIR=${cfg.stateDir}"
|
||||||
|
"IOTA_CACHE_DIR=${cfg.cacheDir}"
|
||||||
|
"IOTA_RUNTIME_DIR=${cfg.runtimeDir}"
|
||||||
|
"IOTA_LOG_DIR=${cfg.logDir}"
|
||||||
|
"IOTA_ASSET_DIR=${cfg.assetDir}"
|
||||||
|
"IOTA_DEPLOYMENT_MODE=system_socket_activated"
|
||||||
|
"IOTA_SUPERVISOR=systemd"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (cfg.environmentFiles != []) {
|
// lib.optionalAttrs (cfg.environmentFiles != []) {
|
||||||
|
|
|
||||||
|
|
@ -387,8 +387,7 @@ impl IpcClient {
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(message) => self.apply(message).await,
|
Ok(message) => self.apply(message).await,
|
||||||
Err(error) => {
|
Err(_) => {
|
||||||
eprintln!("IPC reader for generation {generation} stopped: {error}");
|
|
||||||
self.mark_disconnected(generation).await;
|
self.mark_disconnected(generation).await;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ iota-storage = { path = "../iota-storage" }
|
||||||
iota-terms = { path = "../iota-terms" }
|
iota-terms = { path = "../iota-terms" }
|
||||||
iota-updater = { path = "../iota-updater" }
|
iota-updater = { path = "../iota-updater" }
|
||||||
iota-util = { path = "../iota-util" }
|
iota-util = { path = "../iota-util" }
|
||||||
|
iota-paths = { path = "../iota-paths" }
|
||||||
omikron-connector = { path = "../omikron-connector" }
|
omikron-connector = { path = "../omikron-connector" }
|
||||||
web-server = { path = "../web-server" }
|
web-server = { path = "../web-server" }
|
||||||
web-ui = { path = "../web-ui" }
|
web-ui = { path = "../web-ui" }
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,16 @@ async fn main() {
|
||||||
*state.reload.write().await = false;
|
*state.reload.write().await = false;
|
||||||
*state.shutdown.write().await = false;
|
*state.shutdown.write().await = false;
|
||||||
|
|
||||||
let ipc = IpcClient::connect("/run/iota/iota.sock")
|
let socket = match iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
|
||||||
|
.expect("resolve Iota user paths")
|
||||||
|
.ipc_endpoint
|
||||||
|
{
|
||||||
|
iota_paths::IpcEndpoint::UnixSocket(path) => path,
|
||||||
|
iota_paths::IpcEndpoint::WindowsPipe(_) => {
|
||||||
|
panic!("Windows IPC client transport is not implemented yet")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let ipc = IpcClient::connect(socket)
|
||||||
.await
|
.await
|
||||||
.expect("iota-daemon must be running before starting iota-core");
|
.expect("iota-daemon must be running before starting iota-core");
|
||||||
let session = start_tui(ipc).expect("interactive terminal initialization failed");
|
let session = start_tui(ipc).expect("interactive terminal initialization failed");
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,22 @@ impl IpcServer {
|
||||||
let listener = match activated_listener()? {
|
let listener = match activated_listener()? {
|
||||||
Some(listener) => listener,
|
Some(listener) => listener,
|
||||||
None => {
|
None => {
|
||||||
if let Some(parent) = path.parent() {
|
let parent = path.parent().ok_or_else(|| {
|
||||||
tokio::fs::create_dir_all(parent).await?;
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"IPC socket has no parent directory",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !parent.is_dir() {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
format!("IPC runtime directory does not exist: {}", parent.display()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let lock_path = path.with_extension("sock.lock");
|
let lock_path = path
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new("/tmp"))
|
||||||
|
.join("daemon.lock");
|
||||||
let lock = File::options()
|
let lock = File::options()
|
||||||
.create(true)
|
.create(true)
|
||||||
.mode(0o600)
|
.mode(0o600)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ iota-logger = { path = "../iota-logger" }
|
||||||
iota-state = { path = "../iota-state" }
|
iota-state = { path = "../iota-state" }
|
||||||
iota-paths = { path = "../iota-paths" }
|
iota-paths = { path = "../iota-paths" }
|
||||||
iota-storage = { path = "../iota-storage" }
|
iota-storage = { path = "../iota-storage" }
|
||||||
|
iota-util = { path = "../iota-util" }
|
||||||
omikron-connector = { path = "../omikron-connector" }
|
omikron-connector = { path = "../omikron-connector" }
|
||||||
web-server = { path = "../web-server" }
|
web-server = { path = "../web-server" }
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,33 @@ use std::time::Duration;
|
||||||
use tokio::sync::{broadcast, watch};
|
use tokio::sync::{broadcast, watch};
|
||||||
#[tokio::main(flavor = "multi_thread")]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
logger::startup();
|
let scope = match std::env::var("IOTA_DEPLOYMENT_MODE").ok().as_deref() {
|
||||||
iota_storage::util::config_util::load_config();
|
Some("system_socket_activated") | Some("system_always_on") => iota_paths::Scope::System,
|
||||||
|
_ => iota_paths::Scope::User,
|
||||||
|
};
|
||||||
|
let paths = match iota_paths::IotaPaths::resolve(scope) {
|
||||||
|
Ok(paths) => paths,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Cannot resolve Iota paths: {error}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(error) = paths.migrate_legacy_layout() {
|
||||||
|
eprintln!("Cannot migrate legacy Iota layout: {error}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
if let Err(error) = paths.prepare_writable_directories() {
|
||||||
|
eprintln!("Cannot prepare Iota directories: {error}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
iota_util::file_util::configure_storage_directory(paths.storage_dir.clone());
|
||||||
|
iota_storage::util::config_util::configure_config_path(paths.config_file.clone());
|
||||||
|
iota_storage::util::config_util::load_config_from(&paths.config_file);
|
||||||
|
omikron_connector::omikron_connection::configure_identity_path(paths.keyring_file());
|
||||||
|
match paths.scope {
|
||||||
|
iota_paths::Scope::User => logger::startup_with_log_dir(Some(paths.log_dir.clone())),
|
||||||
|
iota_paths::Scope::System => logger::startup_with_log_dir(None),
|
||||||
|
}
|
||||||
|
|
||||||
let runtime = Arc::new(DaemonRuntime::new());
|
let runtime = Arc::new(DaemonRuntime::new());
|
||||||
// --- IPC infrastructure ---
|
// --- IPC infrastructure ---
|
||||||
|
|
@ -36,7 +61,13 @@ async fn main() -> ExitCode {
|
||||||
|
|
||||||
// Bind before migration and service startup: a successful bind is the
|
// Bind before migration and service startup: a successful bind is the
|
||||||
// readiness boundary visible to clients and socket activation.
|
// readiness boundary visible to clients and socket activation.
|
||||||
let socket = iota_paths::socket_path(iota_paths::SocketScope::User);
|
let socket = match &paths.ipc_endpoint {
|
||||||
|
iota_paths::IpcEndpoint::UnixSocket(path) => path.clone(),
|
||||||
|
iota_paths::IpcEndpoint::WindowsPipe(_) => {
|
||||||
|
eprintln!("Windows named-pipe daemon transport is not implemented yet");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
let omikron = match omikron_connector::omikron_connection::connect_initial(
|
let omikron = match omikron_connector::omikron_connection::connect_initial(
|
||||||
runtime.cancellation.clone(),
|
runtime.cancellation.clone(),
|
||||||
runtime.state.active_tasks.clone(),
|
runtime.state.active_tasks.clone(),
|
||||||
|
|
@ -65,18 +96,22 @@ async fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let services = DaemonServices::new(omikron);
|
let services = DaemonServices::new(omikron);
|
||||||
let ipc_server =
|
let ipc_server = match IpcServer::bind(
|
||||||
match IpcServer::bind(socket, runtime.clone(), services, log_tx.clone(), state_rx).await {
|
socket.clone(),
|
||||||
|
runtime.clone(),
|
||||||
|
services,
|
||||||
|
log_tx.clone(),
|
||||||
|
state_rx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(server) => server,
|
Ok(server) => server,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Cannot bind daemon IPC socket: {error}");
|
eprintln!("Cannot bind daemon IPC socket: {error}");
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
eprintln!(
|
eprintln!("iota-daemon IPC listener ready at {}", socket.display());
|
||||||
"iota-daemon IPC listener ready at {}",
|
|
||||||
iota_paths::socket_path(iota_paths::SocketScope::User).display()
|
|
||||||
);
|
|
||||||
runtime.set_component_healthy(iota_ipc::ComponentId::Ipc, None);
|
runtime.set_component_healthy(iota_ipc::ComponentId::Ipc, None);
|
||||||
let listener_runtime = runtime.clone();
|
let listener_runtime = runtime.clone();
|
||||||
runtime
|
runtime
|
||||||
|
|
@ -128,13 +163,17 @@ async fn main() -> ExitCode {
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)),
|
.unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)),
|
||||||
port: web.port,
|
port: web.port,
|
||||||
asset_dir: std::path::PathBuf::from(web.asset_dir),
|
asset_dir: resolve_config_path(&paths.config_file, &web.asset_dir, &paths.asset_dir),
|
||||||
tls: web
|
tls: web
|
||||||
.certificate
|
.certificate
|
||||||
.zip(web.key)
|
.zip(web.key)
|
||||||
.map(|(certificate, key)| web_server::TlsConfig {
|
.map(|(certificate, key)| web_server::TlsConfig {
|
||||||
certificate: certificate.into(),
|
certificate: resolve_config_path(
|
||||||
key: key.into(),
|
&paths.config_file,
|
||||||
|
&certificate,
|
||||||
|
&paths.config_dir,
|
||||||
|
),
|
||||||
|
key: resolve_config_path(&paths.config_file, &key, &paths.config_dir),
|
||||||
}),
|
}),
|
||||||
required: web.required,
|
required: web.required,
|
||||||
};
|
};
|
||||||
|
|
@ -196,3 +235,22 @@ async fn main() -> ExitCode {
|
||||||
log!("iota-daemon exited (code: {})", exit_code);
|
log!("iota-daemon exited (code: {})", exit_code);
|
||||||
ExitCode::from(exit_code as u8)
|
ExitCode::from(exit_code as u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_config_path(
|
||||||
|
config_file: &std::path::Path,
|
||||||
|
value: &str,
|
||||||
|
default: &std::path::Path,
|
||||||
|
) -> std::path::PathBuf {
|
||||||
|
if value.is_empty() {
|
||||||
|
return default.to_path_buf();
|
||||||
|
}
|
||||||
|
let path = std::path::PathBuf::from(value);
|
||||||
|
if path.is_absolute() {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
config_file
|
||||||
|
.parent()
|
||||||
|
.expect("absolute configuration file has a parent")
|
||||||
|
.join(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
||||||
] {
|
] {
|
||||||
install(
|
install(
|
||||||
&staging.path().join("systemd").join(unit),
|
&staging.path().join("systemd").join(unit),
|
||||||
&format!("/etc/systemd/system/{unit}"),
|
&format!("/usr/local/lib/systemd/system/{unit}"),
|
||||||
"0644",
|
"0644",
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +114,7 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
||||||
"{}/current/bin/iota-daemon",
|
"{}/current/bin/iota-daemon",
|
||||||
iota_paths::install_root().display()
|
iota_paths::install_root().display()
|
||||||
),
|
),
|
||||||
"/usr/local/lib/iota/iota-daemon",
|
"/usr/local/libexec/iota/iota-daemon",
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
run("systemd-sysusers", &[])?;
|
run("systemd-sysusers", &[])?;
|
||||||
|
|
@ -130,8 +130,12 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
||||||
run("systemctl", &["enable", "--now", "iota-daemon.socket"])?;
|
run("systemctl", &["enable", "--now", "iota-daemon.socket"])?;
|
||||||
run("systemctl", &["is-active", "iota-daemon.socket"])?;
|
run("systemctl", &["is-active", "iota-daemon.socket"])?;
|
||||||
run("systemctl", &["is-enabled", "iota-daemon.socket"])?;
|
run("systemctl", &["is-enabled", "iota-daemon.socket"])?;
|
||||||
if !Path::new("/run/iota/iota.sock").exists() {
|
let socket = iota_paths::socket_path(iota_paths::Scope::System);
|
||||||
bail!("systemd socket is active but /run/iota/iota.sock was not created");
|
if !socket.exists() {
|
||||||
|
bail!(
|
||||||
|
"systemd socket is active but {} was not created",
|
||||||
|
socket.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -175,7 +179,7 @@ mod tests {
|
||||||
let service = include_str!("../../systemd/iota-daemon.service");
|
let service = include_str!("../../systemd/iota-daemon.service");
|
||||||
let socket = include_str!("../../systemd/iota-daemon.socket");
|
let socket = include_str!("../../systemd/iota-daemon.socket");
|
||||||
let sysusers = include_str!("../../systemd/sysusers.d/iota.conf");
|
let sysusers = include_str!("../../systemd/sysusers.d/iota.conf");
|
||||||
assert!(service.contains("ExecStart=/usr/local/lib/iota/iota-daemon"));
|
assert!(service.contains("ExecStart=/usr/local/libexec/iota/iota-daemon"));
|
||||||
assert!(service.contains("User=iota"));
|
assert!(service.contains("User=iota"));
|
||||||
assert!(service.contains("Group=iota"));
|
assert!(service.contains("Group=iota"));
|
||||||
assert!(socket.contains("SocketUser=iota"));
|
assert!(socket.contains("SocketUser=iota"));
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
iota-paths = { path = "../iota-paths" }
|
||||||
iota-state = { path = "../iota-state" }
|
iota-state = { path = "../iota-state" }
|
||||||
iota-util = { path = "../iota-util" }
|
iota-util = { path = "../iota-util" }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, OpenOptions},
|
fs::{self, OpenOptions},
|
||||||
io::Write,
|
io::Write,
|
||||||
path::Path,
|
|
||||||
sync::{OnceLock, atomic::Ordering, mpsc},
|
sync::{OnceLock, atomic::Ordering, mpsc},
|
||||||
thread,
|
thread,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
|
@ -56,6 +55,15 @@ struct LogMessage {
|
||||||
/* The logger owns file persistence while consumers receive rendered entries
|
/* The logger owns file persistence while consumers receive rendered entries
|
||||||
* through a process-local broadcast subscription. */
|
* through a process-local broadcast subscription. */
|
||||||
pub fn startup() {
|
pub fn startup() {
|
||||||
|
startup_with_log_dir(Some(
|
||||||
|
iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
|
||||||
|
.expect("resolve Iota user paths")
|
||||||
|
.log_dir,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `None` keeps logging on stderr only (the systemd default).
|
||||||
|
pub fn startup_with_log_dir(log_dir: Option<std::path::PathBuf>) {
|
||||||
let (tx, rx) = mpsc::channel::<LogMessage>();
|
let (tx, rx) = mpsc::channel::<LogMessage>();
|
||||||
if LOGGER.set(tx).is_err() {
|
if LOGGER.set(tx).is_err() {
|
||||||
return;
|
return;
|
||||||
|
|
@ -64,22 +72,15 @@ pub fn startup() {
|
||||||
let _ = LOG_BROADCASTER.set(broadcast_tx.clone());
|
let _ = LOG_BROADCASTER.set(broadcast_tx.clone());
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let working_dir = iota_util::file_util::get_directory();
|
let mut file = log_dir.and_then(|log_dir| {
|
||||||
let base_dir = Path::new(&working_dir);
|
fs::create_dir_all(&log_dir).ok()?;
|
||||||
let log_dir = base_dir.join("logs");
|
let start_ts = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
|
||||||
fs::create_dir_all(&log_dir).expect("Failed to create log directory");
|
OpenOptions::new()
|
||||||
|
|
||||||
let start_ts = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
let path = log_dir.join(format!("log_{}.txt", start_ts));
|
|
||||||
let mut file = OpenOptions::new()
|
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.open(path)
|
.open(log_dir.join(format!("log_{start_ts}.txt")))
|
||||||
.expect("Failed to open log file");
|
.ok()
|
||||||
|
});
|
||||||
|
|
||||||
for msg in rx {
|
for msg in rx {
|
||||||
let resolved_message = if let Some(key) = msg.translation_key {
|
let resolved_message = if let Some(key) = msg.translation_key {
|
||||||
|
|
@ -105,7 +106,9 @@ pub fn startup() {
|
||||||
);
|
);
|
||||||
let line2 = format!(" {}", timestamp);
|
let line2 = format!(" {}", timestamp);
|
||||||
|
|
||||||
|
if let Some(file) = file.as_mut() {
|
||||||
let _ = writeln!(file, "{}\n{}", line1, line2);
|
let _ = writeln!(file, "{}\n{}", line1, line2);
|
||||||
|
}
|
||||||
|
|
||||||
let _ = writeln!(std::io::stderr(), "{}\n{}", line1, line2);
|
let _ = writeln!(std::io::stderr(), "{}\n{}", line1, line2);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,118 +1,412 @@
|
||||||
use std::path::PathBuf;
|
//! Platform and deployment aware locations used by Iota.
|
||||||
|
//!
|
||||||
|
//! This module deliberately keeps environment handling in one place. In
|
||||||
|
//! particular, an override is never interpreted relative to the process
|
||||||
|
//! working directory.
|
||||||
|
use std::env;
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum SocketScope {
|
pub enum Scope {
|
||||||
User,
|
User,
|
||||||
System,
|
System,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn home_dir() -> PathBuf {
|
/// Compatibility name retained for callers which have not yet been migrated.
|
||||||
std::env::var_os("HOME")
|
pub type SocketScope = Scope;
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum IpcEndpoint {
|
||||||
|
UnixSocket(PathBuf),
|
||||||
|
WindowsPipe(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data_dir() -> PathBuf {
|
#[derive(Debug)]
|
||||||
if let Some(path) = std::env::var_os("IOTA_DATA_DIR") {
|
pub enum PathError {
|
||||||
return PathBuf::from(path);
|
MissingPlatformDirectory(&'static str),
|
||||||
|
EmptyOverride(&'static str),
|
||||||
|
RelativeOverride {
|
||||||
|
variable: &'static str,
|
||||||
|
value: PathBuf,
|
||||||
|
},
|
||||||
|
InvalidPipeName(String),
|
||||||
|
UnsupportedScope,
|
||||||
|
}
|
||||||
|
impl fmt::Display for PathError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::MissingPlatformDirectory(name) => write!(f, "missing platform directory: {name}"),
|
||||||
|
Self::EmptyOverride(name) => write!(f, "{name} must not be empty"),
|
||||||
|
Self::RelativeOverride { variable, value } => {
|
||||||
|
write!(f, "{variable} must be absolute, got {}", value.display())
|
||||||
|
}
|
||||||
|
Self::InvalidPipeName(name) => write!(f, "invalid Windows pipe name: {name}"),
|
||||||
|
Self::UnsupportedScope => write!(f, "this path scope is unsupported on this platform"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for PathError {}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct IotaPaths {
|
||||||
|
pub scope: Scope,
|
||||||
|
pub config_dir: PathBuf,
|
||||||
|
pub config_file: PathBuf,
|
||||||
|
pub state_dir: PathBuf,
|
||||||
|
pub storage_dir: PathBuf,
|
||||||
|
pub identity_dir: PathBuf,
|
||||||
|
pub cache_dir: PathBuf,
|
||||||
|
pub runtime_dir: Option<PathBuf>,
|
||||||
|
pub log_dir: PathBuf,
|
||||||
|
/// Directory containing static web assets (not its parent).
|
||||||
|
pub asset_dir: PathBuf,
|
||||||
|
pub install_root: PathBuf,
|
||||||
|
pub ipc_endpoint: IpcEndpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IotaPaths {
|
||||||
|
pub fn resolve(scope: Scope) -> Result<Self, PathError> {
|
||||||
|
let defaults = Defaults::for_scope(scope)?;
|
||||||
|
let config_dir = override_first(&["IOTA_CONFIG_DIR"])?.unwrap_or(defaults.config_dir);
|
||||||
|
// IOTA_DATA_DIR is intentionally only a compatibility alias. Parse it
|
||||||
|
// exactly like every other override; do not hide an invalid value.
|
||||||
|
let state_dir =
|
||||||
|
override_first(&["IOTA_STATE_DIR", "IOTA_DATA_DIR"])?.unwrap_or(defaults.state_dir);
|
||||||
|
let cache_dir = override_first(&["IOTA_CACHE_DIR"])?.unwrap_or(defaults.cache_dir);
|
||||||
|
let runtime_dir = override_first(&["IOTA_RUNTIME_DIR"])?.or(defaults.runtime_dir);
|
||||||
|
let log_dir = override_first(&["IOTA_LOG_DIR"])?.unwrap_or(defaults.log_dir);
|
||||||
|
let asset_dir = override_first(&["IOTA_ASSET_DIR", "IOTA_WEB_ASSET_DIR"])?
|
||||||
|
.unwrap_or(defaults.asset_dir);
|
||||||
|
let install_root = override_first(&["IOTA_INSTALL_ROOT"])?.unwrap_or(defaults.install_root);
|
||||||
|
let config_file = override_first(&["IOTA_CONFIG_FILE"])?
|
||||||
|
.unwrap_or_else(|| config_dir.join("config.yaml"));
|
||||||
|
let ipc_endpoint = resolve_ipc(scope, runtime_dir.as_deref(), defaults.ipc_endpoint)?;
|
||||||
|
Ok(Self {
|
||||||
|
scope,
|
||||||
|
config_dir,
|
||||||
|
config_file,
|
||||||
|
storage_dir: state_dir.join("storage"),
|
||||||
|
identity_dir: state_dir.join("identity"),
|
||||||
|
state_dir,
|
||||||
|
cache_dir,
|
||||||
|
runtime_dir,
|
||||||
|
log_dir,
|
||||||
|
asset_dir,
|
||||||
|
install_root,
|
||||||
|
ipc_endpoint,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn database_file(&self) -> PathBuf {
|
||||||
|
self.storage_dir.join("messages.sqlite3")
|
||||||
|
}
|
||||||
|
pub fn keyring_file(&self) -> PathBuf {
|
||||||
|
self.identity_dir.join("iota.mk")
|
||||||
|
}
|
||||||
|
pub fn update_staging_dir(&self) -> PathBuf {
|
||||||
|
self.cache_dir.join("updates/staging")
|
||||||
|
}
|
||||||
|
pub fn update_status_file(&self) -> PathBuf {
|
||||||
|
self.state_dir.join("update-status.json")
|
||||||
|
}
|
||||||
|
pub fn update_lock_file(&self) -> Result<PathBuf, PathError> {
|
||||||
|
self.runtime_dir
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.join("update.lock"))
|
||||||
|
.ok_or(PathError::MissingPlatformDirectory("runtime directory"))
|
||||||
|
}
|
||||||
|
pub fn daemon_lock_file(&self) -> Result<PathBuf, PathError> {
|
||||||
|
self.runtime_dir
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.join("daemon.lock"))
|
||||||
|
.ok_or(PathError::MissingPlatformDirectory("runtime directory"))
|
||||||
|
}
|
||||||
|
pub fn prepare_writable_directories(&self) -> std::io::Result<()> {
|
||||||
|
for directory in [
|
||||||
|
&self.state_dir,
|
||||||
|
&self.storage_dir,
|
||||||
|
&self.identity_dir,
|
||||||
|
&self.cache_dir,
|
||||||
|
&self.log_dir,
|
||||||
|
] {
|
||||||
|
create_directory(directory, self.scope == Scope::User)?;
|
||||||
|
}
|
||||||
|
if let Some(runtime) = &self.runtime_dir {
|
||||||
|
create_directory(runtime, self.scope == Scope::User)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the pre-v2 resources that were all placed directly below the
|
||||||
|
/// state root. This is deliberately idempotent: an existing destination
|
||||||
|
/// is never overwritten and the marker is only written after the moves.
|
||||||
|
pub fn migrate_legacy_layout(&self) -> std::io::Result<()> {
|
||||||
|
let marker = self.state_dir.join("path-layout-v2.json");
|
||||||
|
if marker.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
move_if_absent(&self.state_dir.join("config.yaml"), &self.config_file)?;
|
||||||
|
move_if_absent(&self.state_dir.join("certs"), &self.config_dir.join("tls"))?;
|
||||||
|
for suffix in [
|
||||||
|
"messages.sqlite3",
|
||||||
|
"messages.sqlite3-wal",
|
||||||
|
"messages.sqlite3-shm",
|
||||||
|
] {
|
||||||
|
move_if_absent(&self.state_dir.join(suffix), &self.storage_dir.join(suffix))?;
|
||||||
|
}
|
||||||
|
for name in ["users", "communities"] {
|
||||||
|
move_if_absent(&self.state_dir.join(name), &self.storage_dir.join(name))?;
|
||||||
|
}
|
||||||
|
move_if_absent(&self.state_dir.join("iota.mk"), &self.keyring_file())?;
|
||||||
|
move_if_absent(
|
||||||
|
&self.state_dir.join("update-staging"),
|
||||||
|
&self.update_staging_dir(),
|
||||||
|
)?;
|
||||||
|
// Runtime objects must not survive a layout migration or reboot.
|
||||||
|
for name in ["update.lock", "iota.sock", "iota.sock.lock"] {
|
||||||
|
let path = self.state_dir.join(name);
|
||||||
|
if path.is_file() || path.is_symlink() {
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(&self.state_dir)?;
|
||||||
|
std::fs::write(marker, "{\"version\":2}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_if_absent(source: &Path, destination: &Path) -> std::io::Result<()> {
|
||||||
|
if !source.exists() || destination.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let metadata = std::fs::symlink_metadata(source)?;
|
||||||
|
if metadata.file_type().is_symlink() {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
format!("refusing symlink migration source {}", source.display()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(parent) = destination.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
match std::fs::rename(source, destination) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(error) if error.raw_os_error() == Some(libc_exdev()) => {
|
||||||
|
copy_recursively(source, destination)?;
|
||||||
|
if source.is_dir() {
|
||||||
|
std::fs::remove_dir_all(source)
|
||||||
|
} else {
|
||||||
|
std::fs::remove_file(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXDEV is stable on Unix. A literal is used on non-Unix where the fallback
|
||||||
|
// copy is harmlessly skipped because rename normally remains on one volume.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn libc_exdev() -> i32 {
|
||||||
|
18
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn libc_exdev() -> i32 {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
fn copy_recursively(source: &Path, destination: &Path) -> std::io::Result<()> {
|
||||||
|
if source.is_dir() {
|
||||||
|
std::fs::create_dir_all(destination)?;
|
||||||
|
for entry in std::fs::read_dir(source)? {
|
||||||
|
let entry = entry?;
|
||||||
|
copy_recursively(&entry.path(), &destination.join(entry.file_name()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
std::fs::copy(source, destination).map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Defaults {
|
||||||
|
config_dir: PathBuf,
|
||||||
|
state_dir: PathBuf,
|
||||||
|
cache_dir: PathBuf,
|
||||||
|
runtime_dir: Option<PathBuf>,
|
||||||
|
log_dir: PathBuf,
|
||||||
|
asset_dir: PathBuf,
|
||||||
|
install_root: PathBuf,
|
||||||
|
ipc_endpoint: IpcEndpoint,
|
||||||
|
}
|
||||||
|
impl Defaults {
|
||||||
|
fn for_scope(scope: Scope) -> Result<Self, PathError> {
|
||||||
|
match scope {
|
||||||
|
Scope::System => {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
return std::env::var_os("XDG_STATE_HOME")
|
Ok(Self {
|
||||||
.map(PathBuf::from)
|
config_dir: "/etc/iota".into(),
|
||||||
.unwrap_or_else(|| home_dir().join(".local/state"))
|
state_dir: "/var/lib/iota".into(),
|
||||||
|
cache_dir: "/var/cache/iota".into(),
|
||||||
|
runtime_dir: Some("/run/iota".into()),
|
||||||
|
log_dir: "/var/log/iota".into(),
|
||||||
|
asset_dir: "/usr/local/share/iota/web".into(),
|
||||||
|
install_root: "/usr/local/libexec/iota".into(),
|
||||||
|
ipc_endpoint: IpcEndpoint::UnixSocket("/run/iota/iota.sock".into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
Err(PathError::UnsupportedScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Scope::User => user_defaults(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn user_defaults() -> Result<Defaults, PathError> {
|
||||||
|
let home =
|
||||||
|
absolute_env("HOME")?.ok_or(PathError::MissingPlatformDirectory("home directory"))?;
|
||||||
|
let config_base = xdg_or_home("XDG_CONFIG_HOME", &home, ".config")?;
|
||||||
|
let state_base = xdg_or_home("XDG_STATE_HOME", &home, ".local/state")?;
|
||||||
|
let cache_base = xdg_or_home("XDG_CACHE_HOME", &home, ".cache")?;
|
||||||
|
let data_base = xdg_or_home("XDG_DATA_HOME", &home, ".local/share")?;
|
||||||
|
let runtime = absolute_env("XDG_RUNTIME_DIR")?
|
||||||
|
.ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"))?
|
||||||
.join("iota");
|
.join("iota");
|
||||||
}
|
Ok(Defaults {
|
||||||
#[cfg(target_os = "macos")]
|
config_dir: config_base.join("iota"),
|
||||||
{
|
state_dir: state_base.join("iota"),
|
||||||
return home_dir().join("Library/Application Support/Iota");
|
cache_dir: cache_base.join("iota"),
|
||||||
}
|
runtime_dir: Some(runtime.clone()),
|
||||||
#[cfg(target_os = "windows")]
|
log_dir: state_base.join("iota/logs"),
|
||||||
{
|
asset_dir: data_base.join("iota/web"),
|
||||||
return std::env::var_os("LOCALAPPDATA")
|
install_root: data_base.join("iota/bin"),
|
||||||
.map(PathBuf::from)
|
ipc_endpoint: IpcEndpoint::UnixSocket(runtime.join("iota.sock")),
|
||||||
.unwrap_or_else(home_dir)
|
})
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn user_defaults() -> Result<Defaults, PathError> {
|
||||||
|
let config = absolute_env("APPDATA")?
|
||||||
|
.ok_or(PathError::MissingPlatformDirectory("Roaming AppData"))?
|
||||||
|
.join("Tensamin/Iota/config");
|
||||||
|
let local = absolute_env("LOCALAPPDATA")?
|
||||||
|
.ok_or(PathError::MissingPlatformDirectory("Local AppData"))?
|
||||||
.join("Tensamin/Iota");
|
.join("Tensamin/Iota");
|
||||||
|
Ok(Defaults {
|
||||||
|
config_dir: config,
|
||||||
|
state_dir: local.join("state"),
|
||||||
|
cache_dir: local.join("cache"),
|
||||||
|
runtime_dir: None,
|
||||||
|
log_dir: local.join("logs"),
|
||||||
|
asset_dir: local.join("data"),
|
||||||
|
install_root: local.join("bin"),
|
||||||
|
ipc_endpoint: IpcEndpoint::WindowsPipe(r"\\.\pipe\Tensamin.Iota.User".into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xdg_or_home(variable: &'static str, home: &Path, fallback: &str) -> Result<PathBuf, PathError> {
|
||||||
|
Ok(absolute_env(variable)?.unwrap_or_else(|| home.join(fallback)))
|
||||||
|
}
|
||||||
|
fn absolute_env(name: &'static str) -> Result<Option<PathBuf>, PathError> {
|
||||||
|
let Some(value) = env::var_os(name) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if value.is_empty() {
|
||||||
|
return Err(PathError::EmptyOverride(name));
|
||||||
}
|
}
|
||||||
#[allow(unreachable_code)]
|
let path = PathBuf::from(value);
|
||||||
home_dir().join(".iota")
|
if !path.is_absolute() {
|
||||||
|
return Err(PathError::RelativeOverride {
|
||||||
|
variable: name,
|
||||||
|
value: path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Some(path))
|
||||||
|
}
|
||||||
|
fn override_first(names: &[&'static str]) -> Result<Option<PathBuf>, PathError> {
|
||||||
|
for name in names {
|
||||||
|
if let Some(value) = absolute_env(name)? {
|
||||||
|
return Ok(Some(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
fn resolve_ipc(
|
||||||
|
scope: Scope,
|
||||||
|
runtime: Option<&Path>,
|
||||||
|
default: IpcEndpoint,
|
||||||
|
) -> Result<IpcEndpoint, PathError> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
if let Some(path) = absolute_env("IOTA_SOCKET")? {
|
||||||
|
return Ok(IpcEndpoint::UnixSocket(path));
|
||||||
|
}
|
||||||
|
if scope == Scope::User {
|
||||||
|
return runtime
|
||||||
|
.map(|dir| IpcEndpoint::UnixSocket(dir.join("iota.sock")))
|
||||||
|
.ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
if let Some(name) = env::var_os("IOTA_PIPE") {
|
||||||
|
let name = name.to_string_lossy().into_owned();
|
||||||
|
if !name.starts_with(r"\\.\pipe\") {
|
||||||
|
return Err(PathError::InvalidPipeName(name));
|
||||||
|
}
|
||||||
|
return Ok(IpcEndpoint::WindowsPipe(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(default)
|
||||||
|
}
|
||||||
|
fn create_directory(path: &Path, private: bool) -> std::io::Result<()> {
|
||||||
|
std::fs::create_dir_all(path)?;
|
||||||
|
#[cfg(unix)]
|
||||||
|
if private {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compatibility helpers. New code should resolve IotaPaths once and pass it
|
||||||
|
// to its dependencies instead of calling these independently.
|
||||||
|
pub fn data_dir() -> PathBuf {
|
||||||
|
IotaPaths::resolve(Scope::User)
|
||||||
|
.expect("resolve Iota user paths")
|
||||||
|
.state_dir
|
||||||
}
|
}
|
||||||
pub fn config_dir() -> PathBuf {
|
pub fn config_dir() -> PathBuf {
|
||||||
if let Some(path) = std::env::var_os("IOTA_CONFIG_DIR") {
|
IotaPaths::resolve(Scope::User)
|
||||||
return PathBuf::from(path);
|
.expect("resolve Iota user paths")
|
||||||
}
|
.config_dir
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
return std::env::var_os("XDG_CONFIG_HOME")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|| home_dir().join(".config"))
|
|
||||||
.join("iota");
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
return home_dir().join("Library/Application Support/Iota");
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
return std::env::var_os("APPDATA")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(home_dir)
|
|
||||||
.join("Tensamin/Iota");
|
|
||||||
}
|
|
||||||
#[allow(unreachable_code)]
|
|
||||||
home_dir().join(".iota")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn socket_override() -> Option<PathBuf> {
|
pub fn socket_override() -> Option<PathBuf> {
|
||||||
std::env::var_os("IOTA_SOCKET").map(PathBuf::from)
|
absolute_env("IOTA_SOCKET").ok().flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn socket_path(scope: SocketScope) -> PathBuf {
|
pub fn socket_path(scope: SocketScope) -> PathBuf {
|
||||||
if let Some(path) = socket_override() {
|
match IotaPaths::resolve(scope)
|
||||||
return path;
|
.expect("resolve Iota paths")
|
||||||
}
|
.ipc_endpoint
|
||||||
match scope {
|
|
||||||
SocketScope::User => data_dir().join("iota.sock"),
|
|
||||||
SocketScope::System => PathBuf::from("/run/iota/iota.sock"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn socket_lock_path(scope: SocketScope) -> PathBuf {
|
|
||||||
let socket = socket_path(scope);
|
|
||||||
PathBuf::from(format!("{}.lock", socket.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn daemon_executable() -> PathBuf {
|
|
||||||
if let Some(path) = std::env::var_os("IOTA_DAEMON_PATH") {
|
|
||||||
return PathBuf::from(path);
|
|
||||||
}
|
|
||||||
if let Ok(exe) = std::env::current_exe() {
|
|
||||||
if let Some(path) = exe.parent().map(|p| p.join("iota-daemon")) {
|
|
||||||
if path.is_file() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
{
|
||||||
let installed = PathBuf::from("/usr/local/lib/iota/iota-daemon");
|
IpcEndpoint::UnixSocket(path) => path,
|
||||||
if installed.is_file() {
|
IpcEndpoint::WindowsPipe(_) => panic!("Windows IPC endpoint is not a filesystem path"),
|
||||||
return installed;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
PathBuf::from("iota-daemon")
|
|
||||||
}
|
}
|
||||||
|
pub fn socket_lock_path(scope: SocketScope) -> PathBuf {
|
||||||
pub fn updater_executable() -> PathBuf {
|
IotaPaths::resolve(scope)
|
||||||
std::env::var_os("IOTA_UPDATER_PATH")
|
.expect("resolve Iota paths")
|
||||||
.map(PathBuf::from)
|
.daemon_lock_file()
|
||||||
.unwrap_or_else(|| PathBuf::from("iota-updater"))
|
.expect("runtime directory")
|
||||||
}
|
}
|
||||||
|
/// The compatibility installation helpers describe the machine installation,
|
||||||
|
/// not a user's data directory. Per-user launchers should keep an
|
||||||
|
/// `IotaPaths` instance and use its `install_root` directly.
|
||||||
pub fn install_root() -> PathBuf {
|
pub fn install_root() -> PathBuf {
|
||||||
std::env::var_os("IOTA_INSTALL_ROOT")
|
IotaPaths::resolve(Scope::System)
|
||||||
.map(PathBuf::from)
|
.expect("resolve Iota system paths")
|
||||||
.unwrap_or_else(|| PathBuf::from("/usr/local/lib/iota"))
|
.install_root
|
||||||
}
|
}
|
||||||
pub fn versions_dir() -> PathBuf {
|
pub fn versions_dir() -> PathBuf {
|
||||||
install_root().join("versions")
|
install_root().join("versions")
|
||||||
|
|
@ -121,36 +415,91 @@ pub fn current_version_link() -> PathBuf {
|
||||||
install_root().join("current")
|
install_root().join("current")
|
||||||
}
|
}
|
||||||
pub fn updater_lock_path() -> PathBuf {
|
pub fn updater_lock_path() -> PathBuf {
|
||||||
data_dir().join("update.lock")
|
IotaPaths::resolve(Scope::User)
|
||||||
|
.expect("resolve Iota user paths")
|
||||||
|
.update_lock_file()
|
||||||
|
.expect("runtime directory")
|
||||||
}
|
}
|
||||||
pub fn updater_status_path() -> PathBuf {
|
pub fn updater_status_path() -> PathBuf {
|
||||||
data_dir().join("update-status.json")
|
IotaPaths::resolve(Scope::User)
|
||||||
|
.expect("resolve Iota user paths")
|
||||||
|
.update_status_file()
|
||||||
}
|
}
|
||||||
pub fn updater_staging_dir() -> PathBuf {
|
pub fn updater_staging_dir() -> PathBuf {
|
||||||
data_dir().join("update-staging")
|
IotaPaths::resolve(Scope::User)
|
||||||
|
.expect("resolve Iota user paths")
|
||||||
|
.update_staging_dir()
|
||||||
}
|
}
|
||||||
pub fn web_asset_dir() -> PathBuf {
|
pub fn web_asset_dir() -> PathBuf {
|
||||||
std::env::var_os("IOTA_WEB_ASSET_DIR")
|
IotaPaths::resolve(Scope::User)
|
||||||
.map(PathBuf::from)
|
.expect("resolve Iota user paths")
|
||||||
.unwrap_or_else(|| data_dir().join("web"))
|
.asset_dir
|
||||||
|
}
|
||||||
|
pub fn daemon_executable() -> PathBuf {
|
||||||
|
absolute_env("IOTA_DAEMON_PATH")
|
||||||
|
.expect("valid IOTA_DAEMON_PATH")
|
||||||
|
.unwrap_or_else(|| install_root().join("current/bin/iota-daemon"))
|
||||||
|
}
|
||||||
|
pub fn updater_executable() -> PathBuf {
|
||||||
|
absolute_env("IOTA_UPDATER_PATH")
|
||||||
|
.expect("valid IOTA_UPDATER_PATH")
|
||||||
|
.unwrap_or_else(|| install_root().join("current/bin/iota-updater"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn daemon_endpoints() -> Vec<PathBuf> {
|
pub fn daemon_endpoints() -> Vec<PathBuf> {
|
||||||
if let Some(path) = socket_override() {
|
vec![socket_path(Scope::User), socket_path(Scope::System)]
|
||||||
return vec![path];
|
|
||||||
}
|
|
||||||
vec![
|
|
||||||
socket_path(SocketScope::User),
|
|
||||||
socket_path(SocketScope::System),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
static TEST_ID: AtomicU64 = AtomicU64::new(0);
|
||||||
|
#[test]
|
||||||
|
fn system_layout_is_fhs() {
|
||||||
|
let p = IotaPaths::resolve(Scope::System).unwrap();
|
||||||
|
assert_eq!(p.config_file, PathBuf::from("/etc/iota/config.yaml"));
|
||||||
|
assert_eq!(
|
||||||
|
p.database_file(),
|
||||||
|
PathBuf::from("/var/lib/iota/storage/messages.sqlite3")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
p.update_staging_dir(),
|
||||||
|
PathBuf::from("/var/cache/iota/updates/staging")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn explicit_data_directory_wins() {
|
fn migration_moves_state_resources_without_overwriting_destination() {
|
||||||
assert!(!data_dir().as_os_str().is_empty());
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"iota-paths-test-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
TEST_ID.fetch_add(1, Ordering::Relaxed)
|
||||||
|
));
|
||||||
|
let state = root.join("state");
|
||||||
|
let config = root.join("config");
|
||||||
|
let paths = IotaPaths {
|
||||||
|
scope: Scope::User,
|
||||||
|
config_dir: config.clone(),
|
||||||
|
config_file: config.join("config.yaml"),
|
||||||
|
storage_dir: state.join("storage"),
|
||||||
|
identity_dir: state.join("identity"),
|
||||||
|
cache_dir: root.join("cache"),
|
||||||
|
runtime_dir: Some(root.join("runtime")),
|
||||||
|
log_dir: state.join("logs"),
|
||||||
|
asset_dir: root.join("data/web"),
|
||||||
|
install_root: root.join("bin"),
|
||||||
|
ipc_endpoint: IpcEndpoint::UnixSocket(root.join("runtime/iota.sock")),
|
||||||
|
state_dir: state.clone(),
|
||||||
|
};
|
||||||
|
std::fs::create_dir_all(state.join("users")).unwrap();
|
||||||
|
std::fs::write(state.join("messages.sqlite3"), b"db").unwrap();
|
||||||
|
std::fs::write(state.join("config.yaml"), b"web: {}\n").unwrap();
|
||||||
|
paths.migrate_legacy_layout().unwrap();
|
||||||
|
assert!(paths.database_file().is_file());
|
||||||
|
assert!(paths.storage_dir.join("users").is_dir());
|
||||||
|
assert!(paths.config_file.is_file());
|
||||||
|
assert!(state.join("path-layout-v2.json").is_file());
|
||||||
|
let _ = std::fs::remove_dir_all(root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ edition = "2024"
|
||||||
iota-logger = { path = "../iota-logger" }
|
iota-logger = { path = "../iota-logger" }
|
||||||
iota-state = { path = "../iota-state" }
|
iota-state = { path = "../iota-state" }
|
||||||
iota-util = { path = "../iota-util" }
|
iota-util = { path = "../iota-util" }
|
||||||
|
iota-paths = { path = "../iota-paths" }
|
||||||
|
|
||||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use iota_util::file_util::{load_file, save_file};
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
pub static CONFIG: Lazy<ArcSwap<IotaConfig>> =
|
pub static CONFIG: Lazy<ArcSwap<IotaConfig>> =
|
||||||
Lazy::new(|| ArcSwap::new(Arc::new(IotaConfig::default())));
|
Lazy::new(|| ArcSwap::new(Arc::new(IotaConfig::default())));
|
||||||
|
|
@ -19,11 +21,11 @@ pub struct IotaConfig {
|
||||||
pub omikron_host: Option<String>,
|
pub omikron_host: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub omikron_port: Option<u16>,
|
pub omikron_port: Option<u16>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing)]
|
||||||
pub keyring: Option<String>,
|
pub keyring: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing)]
|
||||||
pub public_key: Option<String>,
|
pub public_key: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing)]
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
#[serde(default = "default_read_receipts_enabled")]
|
#[serde(default = "default_read_receipts_enabled")]
|
||||||
pub read_receipts_enabled: bool,
|
pub read_receipts_enabled: bool,
|
||||||
|
|
@ -61,7 +63,7 @@ fn default_web_bind() -> String {
|
||||||
"127.0.0.1".into()
|
"127.0.0.1".into()
|
||||||
}
|
}
|
||||||
fn default_web_asset_dir() -> String {
|
fn default_web_asset_dir() -> String {
|
||||||
"web".into()
|
String::new()
|
||||||
}
|
}
|
||||||
impl Default for WebSettings {
|
impl Default for WebSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
|
@ -102,17 +104,27 @@ impl Default for IotaConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_config() {
|
pub fn load_config() {
|
||||||
let s = load_file("", "config.yaml");
|
load_config_from(&default_config_path());
|
||||||
if s.is_empty() {
|
}
|
||||||
|
|
||||||
|
/// Loading is intentionally side-effect free: a missing configuration means
|
||||||
|
/// documented defaults, not a newly-created file.
|
||||||
|
pub fn load_config_from(path: &Path) {
|
||||||
|
let s = match fs::read_to_string(path) {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Failed to read {}: {error}", path.display());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match serde_yaml::from_str::<IotaConfig>(&s) {
|
match serde_yaml::from_str::<IotaConfig>(&s) {
|
||||||
Ok(parsed) => {
|
Ok(parsed) => {
|
||||||
CONFIG.store(Arc::new(parsed));
|
CONFIG.store(Arc::new(parsed));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to parse config.yaml: {}. Content: '{}'", e, s);
|
eprintln!("Failed to parse {}: {e}", path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,9 +135,35 @@ pub fn clear_config() {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_config() {
|
pub fn save_config() {
|
||||||
|
save_config_to(&default_config_path());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config_to(path: &Path) {
|
||||||
if let Ok(yaml) = serde_yaml::to_string(&**CONFIG.load()) {
|
if let Ok(yaml) = serde_yaml::to_string(&**CONFIG.load()) {
|
||||||
save_file("", "config.yaml", &yaml);
|
if let Some(parent) = path.parent() {
|
||||||
|
if let Err(error) = fs::create_dir_all(parent) {
|
||||||
|
eprintln!(
|
||||||
|
"Cannot create configuration directory {}: {error}",
|
||||||
|
parent.display()
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
let temporary = path.with_extension("yaml.tmp");
|
||||||
|
if let Err(error) = fs::write(&temporary, yaml).and_then(|_| fs::rename(&temporary, path)) {
|
||||||
|
eprintln!("Cannot save {}: {error}", path.display());
|
||||||
|
let _ = fs::remove_file(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_config_path() -> PathBuf {
|
||||||
|
if let Some(path) = CONFIG_PATH.get() {
|
||||||
|
return path.clone();
|
||||||
|
}
|
||||||
|
iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
|
||||||
|
.expect("resolve Iota user paths")
|
||||||
|
.config_file
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn modify_config(f: impl FnOnce(&mut IotaConfig)) {
|
pub fn modify_config(f: impl FnOnce(&mut IotaConfig)) {
|
||||||
|
|
@ -134,3 +172,8 @@ pub fn modify_config(f: impl FnOnce(&mut IotaConfig)) {
|
||||||
CONFIG.store(Arc::new(cfg));
|
CONFIG.store(Arc::new(cfg));
|
||||||
save_config();
|
save_config();
|
||||||
}
|
}
|
||||||
|
static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn configure_config_path(path: PathBuf) {
|
||||||
|
let _ = CONFIG_PATH.set(path);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use iota_util::file_util::get_directory;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use r2d2::ManageConnection;
|
use r2d2::ManageConnection;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
|
@ -56,10 +55,12 @@ where
|
||||||
f(&conn)
|
f(&conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn db_file_path(db_name: &str) -> String {
|
fn db_file_path(db_name: &str) -> PathBuf {
|
||||||
let mut p = PathBuf::from(get_directory());
|
let storage_dir = iota_util::file_util::storage_directory();
|
||||||
p.push(format!("{db_name}.sqlite3"));
|
// Creating storage belongs to initialization/connection setup, never to a
|
||||||
p.to_string_lossy().to_string()
|
// configuration read.
|
||||||
|
std::fs::create_dir_all(&storage_dir).expect("create Iota storage directory");
|
||||||
|
storage_dir.join(format!("{db_name}.sqlite3"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_migrations(pool: &r2d2::Pool<SqliteManager>) -> Result<(), StorageError> {
|
fn run_migrations(pool: &r2d2::Pool<SqliteManager>) -> Result<(), StorageError> {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iota-logger = { path = "../iota-logger" }
|
iota-logger = { path = "../iota-logger" }
|
||||||
|
iota-paths = { path = "../iota-paths" }
|
||||||
|
|
||||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,32 @@ use std::{
|
||||||
pub struct UpdateTransaction {
|
pub struct UpdateTransaction {
|
||||||
pub root: PathBuf,
|
pub root: PathBuf,
|
||||||
pub staging: PathBuf,
|
pub staging: PathBuf,
|
||||||
|
pub lock_file: PathBuf,
|
||||||
}
|
}
|
||||||
impl UpdateTransaction {
|
impl UpdateTransaction {
|
||||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||||
let root = root.into();
|
let root = root.into();
|
||||||
Self {
|
Self {
|
||||||
staging: root.join(".staging"),
|
staging: root.join(".staging"),
|
||||||
|
lock_file: root.join("update.lock"),
|
||||||
root,
|
root,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn from_paths(paths: &iota_paths::IotaPaths) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
root: paths.install_root.clone(),
|
||||||
|
staging: paths.update_staging_dir(),
|
||||||
|
lock_file: paths.update_lock_file().map_err(|e| anyhow::anyhow!(e))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
pub fn acquire(&self) -> Result<fs::File> {
|
pub fn acquire(&self) -> Result<fs::File> {
|
||||||
fs::create_dir_all(&self.root)?;
|
if let Some(parent) = self.lock_file.parent() {
|
||||||
let path = self.root.join("update.lock");
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
let file = fs::OpenOptions::new()
|
let file = fs::OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.open(path)
|
.open(&self.lock_file)
|
||||||
.context("update already in progress")?;
|
.context("update already in progress")?;
|
||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use std::ffi::OsStr;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{self, BufReader, Read};
|
use std::io::{self, BufReader, Read};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::OnceLock;
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -40,34 +41,19 @@ pub fn delete_user_directory(user_id: i64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
|
pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
|
||||||
let dir = Path::new(&get_directory()).join(path);
|
let file_path = storage_file(path, name)?;
|
||||||
let file_path = dir.join(name);
|
|
||||||
|
|
||||||
// Ensure the directory exists, create if necessary
|
|
||||||
if !dir.exists() {
|
|
||||||
if let Err(_) = fs::create_dir_all(&dir) {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::NotFound,
|
|
||||||
"Directory creation failed",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the file if it doesn't exist
|
|
||||||
if !file_path.exists() {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::NotFound,
|
|
||||||
"File creation failed",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the file and return a BufReader for efficient reading
|
// Open the file and return a BufReader for efficient reading
|
||||||
let file = File::open(&file_path)?;
|
let file = File::open(&file_path)?;
|
||||||
Ok(BufReader::new(file))
|
Ok(BufReader::new(file))
|
||||||
}
|
}
|
||||||
pub fn has_file(path: &str, name: &str) -> bool {
|
pub fn has_file(path: &str, name: &str) -> bool {
|
||||||
let dir = Path::new(&get_directory()).join(path);
|
let Ok(file_path) = storage_file(path, name) else {
|
||||||
let file_path = dir.join(name);
|
return false;
|
||||||
|
};
|
||||||
|
let Some(dir) = file_path.parent() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -80,7 +66,9 @@ pub fn has_file(path: &str, name: &str) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
pub fn has_dir(path: &str) -> bool {
|
pub fn has_dir(path: &str) -> bool {
|
||||||
let dir = Path::new(&get_directory()).join(path);
|
let Ok(dir) = storage_child(path) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -90,8 +78,12 @@ pub fn has_dir(path: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_file(path: &str, name: &str) -> String {
|
pub fn load_file(path: &str, name: &str) -> String {
|
||||||
let dir = Path::new(&get_directory()).join(path);
|
let Ok(file_path) = storage_file(path, name) else {
|
||||||
let file_path = dir.join(name);
|
return String::new();
|
||||||
|
};
|
||||||
|
let Some(dir) = file_path.parent() else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
|
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
return String::new();
|
return String::new();
|
||||||
|
|
@ -109,15 +101,17 @@ pub fn load_file(path: &str, name: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_file_vec(path: &str, name: &str) -> Result<Vec<u8>, std::io::Error> {
|
pub fn load_file_vec(path: &str, name: &str) -> Result<Vec<u8>, std::io::Error> {
|
||||||
let dir = Path::new(&get_directory()).join(path);
|
std::fs::read(storage_file(path, name)?)
|
||||||
let file_path = dir.join(name);
|
|
||||||
|
|
||||||
std::fs::read(file_path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_file(path: &str, name: &str, value: &str) {
|
pub fn save_file(path: &str, name: &str, value: &str) {
|
||||||
let dir = Path::new(&get_directory()).join(path);
|
let Ok(file_path) = storage_file(path, name) else {
|
||||||
let file_path = dir.join(name);
|
eprintln!("[IMPORTANT] Refusing unsafe storage path");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(dir) = file_path.parent() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
if let Err(e) = fs::create_dir_all(&dir) {
|
if let Err(e) = fs::create_dir_all(&dir) {
|
||||||
|
|
@ -149,7 +143,9 @@ pub fn save_file(path: &str, name: &str, value: &str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_children(path: &str) -> Vec<String> {
|
pub fn get_children(path: &str) -> Vec<String> {
|
||||||
let dir = Path::new(&get_directory()).join(path);
|
let Ok(dir) = storage_child(path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
if let Ok(entries) = fs::read_dir(&dir) {
|
if let Ok(entries) = fs::read_dir(&dir) {
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
|
|
@ -161,8 +157,61 @@ pub fn get_children(path: &str) -> Vec<String> {
|
||||||
children
|
children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static STORAGE_DIRECTORY: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Set by the daemon immediately after resolving `IotaPaths`. This keeps the
|
||||||
|
/// legacy storage helpers working while preventing them from independently
|
||||||
|
/// discovering a different (user-scope) directory in a system daemon.
|
||||||
|
pub fn configure_storage_directory(path: PathBuf) {
|
||||||
|
let _ = STORAGE_DIRECTORY.set(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn storage_directory() -> PathBuf {
|
||||||
|
STORAGE_DIRECTORY.get().cloned().unwrap_or_else(|| {
|
||||||
|
iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
|
||||||
|
.expect("resolve Iota user paths")
|
||||||
|
.storage_dir
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a user supplied storage fragment without allowing it to escape the
|
||||||
|
/// resolved storage root. Legacy call sites may use nested fragments, but
|
||||||
|
/// never absolute paths or `..` components.
|
||||||
|
pub fn storage_child(path: impl AsRef<Path>) -> io::Result<PathBuf> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if path.is_absolute()
|
||||||
|
|| path.components().any(|c| {
|
||||||
|
matches!(
|
||||||
|
c,
|
||||||
|
std::path::Component::ParentDir
|
||||||
|
| std::path::Component::RootDir
|
||||||
|
| std::path::Component::Prefix(_)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"unsafe storage path",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(storage_directory().join(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn storage_file(path: impl AsRef<Path>, name: impl AsRef<Path>) -> io::Result<PathBuf> {
|
||||||
|
let name = name.as_ref();
|
||||||
|
if name.components().count() != 1 || name.is_absolute() || name == Path::new(".") {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"unsafe storage file name",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
storage_child(path).map(|dir| dir.join(name))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_directory() -> String {
|
pub fn get_directory() -> String {
|
||||||
iota_paths::data_dir().to_string_lossy().to_string()
|
// Legacy helpers are storage-only. Configuration, keys, logs and runtime
|
||||||
|
// files must use their dedicated path APIs instead.
|
||||||
|
storage_directory().to_string_lossy().into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to download the zip file content to a file on disk
|
// Helper to download the zip file content to a file on disk
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
use iota_cli::{
|
use iota_cli::{
|
||||||
ipc_client::IpcClient,
|
ipc_client::IpcClient, screens::main_screen::MainScreen, theme,
|
||||||
screens::main_screen::MainScreen,
|
|
||||||
theme,
|
|
||||||
ui::start_bootstrap_tui_with_theme,
|
ui::start_bootstrap_tui_with_theme,
|
||||||
};
|
};
|
||||||
use iota_ipc::{LocalRequest, ResponseResult};
|
use iota_ipc::{LocalRequest, ResponseResult};
|
||||||
|
|
@ -32,13 +30,28 @@ async fn main() -> ExitCode {
|
||||||
async fn run() -> Result<(), StartupError> {
|
async fn run() -> Result<(), StartupError> {
|
||||||
let invocation =
|
let invocation =
|
||||||
CliInvocation::parse(std::env::args().skip(1)).map_err(StartupError::InvalidCommand)?;
|
CliInvocation::parse(std::env::args().skip(1)).map_err(StartupError::InvalidCommand)?;
|
||||||
let mut endpoints_iter = iota_paths::daemon_endpoints().into_iter();
|
let local_endpoint = match iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
|
||||||
let local_endpoint = endpoints_iter
|
.map_err(|error| StartupError::Other(format!("Cannot resolve user IPC path: {error}")))?
|
||||||
.next()
|
.ipc_endpoint
|
||||||
.expect("path layer always returns an endpoint");
|
{
|
||||||
let system_endpoint = endpoints_iter
|
iota_paths::IpcEndpoint::UnixSocket(path) => path,
|
||||||
.next()
|
iota_paths::IpcEndpoint::WindowsPipe(name) => {
|
||||||
.unwrap_or_else(|| local_endpoint.clone());
|
return Err(StartupError::Other(format!(
|
||||||
|
"Windows IPC endpoint {name} is not supported by this client build"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let system_endpoint = match iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
|
||||||
|
.map_err(|error| StartupError::Other(format!("Cannot resolve system IPC path: {error}")))?
|
||||||
|
.ipc_endpoint
|
||||||
|
{
|
||||||
|
iota_paths::IpcEndpoint::UnixSocket(path) => path,
|
||||||
|
iota_paths::IpcEndpoint::WindowsPipe(name) => {
|
||||||
|
return Err(StartupError::Other(format!(
|
||||||
|
"Windows IPC endpoint {name} is not supported by this client build"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
let endpoints = daemon_setup_flow::DaemonEndpoints {
|
let endpoints = daemon_setup_flow::DaemonEndpoints {
|
||||||
local: local_endpoint,
|
local: local_endpoint,
|
||||||
system: system_endpoint,
|
system: system_endpoint,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use mtp::client::{Client, ClientConfig, Policy, Receiver, SendMode, Sender};
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
use mtp::crypto::{Keyring, PublicKeyBundle};
|
use mtp::crypto::{Keyring, PublicKeyBundle};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
@ -92,6 +93,19 @@ async fn is_read_receipts_enabled() -> bool {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const IOTA_KEYRING_PATH: &str = "iota.mk";
|
const IOTA_KEYRING_PATH: &str = "iota.mk";
|
||||||
|
static IDENTITY_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
/// Must be called by the daemon before any Omikron connection is attempted.
|
||||||
|
/// It keeps identity material independent from the working directory.
|
||||||
|
pub fn configure_identity_path(path: PathBuf) {
|
||||||
|
let _ = IDENTITY_PATH.set(path);
|
||||||
|
}
|
||||||
|
fn identity_path() -> &'static Path {
|
||||||
|
IDENTITY_PATH
|
||||||
|
.get()
|
||||||
|
.map(PathBuf::as_path)
|
||||||
|
.unwrap_or_else(|| Path::new(IOTA_KEYRING_PATH))
|
||||||
|
}
|
||||||
const OMIKRON_PUBLIC_KEY_PATH: &str = "omikron.mpkb";
|
const OMIKRON_PUBLIC_KEY_PATH: &str = "omikron.mpkb";
|
||||||
const RECONNECT_DELAY: Duration = Duration::from_secs(5);
|
const RECONNECT_DELAY: Duration = Duration::from_secs(5);
|
||||||
const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300);
|
const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300);
|
||||||
|
|
@ -436,7 +450,8 @@ impl OmikronConnection {
|
||||||
* that still read it directly.
|
* that still read it directly.
|
||||||
*/
|
*/
|
||||||
async fn load_or_migrate_keyring(&self) -> Keyring {
|
async fn load_or_migrate_keyring(&self) -> Keyring {
|
||||||
if let Ok(kr) = mtp::files::load_keyring_raw(IOTA_KEYRING_PATH) {
|
let path = identity_path();
|
||||||
|
if let Ok(kr) = mtp::files::load_keyring_raw(path) {
|
||||||
return kr;
|
return kr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -448,18 +463,18 @@ impl OmikronConnection {
|
||||||
"WARNING: No existing keyring found. Neither {} nor config.json \
|
"WARNING: No existing keyring found. Neither {} nor config.json \
|
||||||
contain a keyring; generating a new identity. If you already had \
|
contain a keyring; generating a new identity. If you already had \
|
||||||
an Iota identity, restore {} from a backup to avoid losing access.",
|
an Iota identity, restore {} from a backup to avoid losing access.",
|
||||||
IOTA_KEYRING_PATH,
|
path.display(),
|
||||||
IOTA_KEYRING_PATH
|
path.display()
|
||||||
);
|
);
|
||||||
crypto_helper::generate_keyring()
|
crypto_helper::generate_keyring()
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Err(e) = mtp::files::save_keyring_raw(&keyring, IOTA_KEYRING_PATH) {
|
if let Some(parent) = path.parent() {
|
||||||
log!("Failed to persist {}: {}", IOTA_KEYRING_PATH, e);
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
if let Err(e) = mtp::files::save_keyring_raw(&keyring, path) {
|
||||||
|
log!("Failed to persist {}: {}", path.display(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let b64 = crypto_helper::keyring_to_base64(&keyring);
|
|
||||||
modify_config(|cfg| cfg.keyring = Some(b64));
|
|
||||||
|
|
||||||
keyring
|
keyring
|
||||||
}
|
}
|
||||||
|
|
@ -1050,9 +1065,7 @@ impl OmikronConnection {
|
||||||
if let Some(app_pub_bundle) =
|
if let Some(app_pub_bundle) =
|
||||||
iota_util::crypto_helper::public_key_bundle_from_base64(&app_public_key)
|
iota_util::crypto_helper::public_key_bundle_from_base64(&app_public_key)
|
||||||
{
|
{
|
||||||
let kr_str = CONFIG.load().keyring.clone().unwrap_or_default();
|
if let Ok(keyring) = mtp::files::load_keyring_raw(identity_path()) {
|
||||||
|
|
||||||
if let Some(keyring) = keyring_from_base64(&kr_str) {
|
|
||||||
if let Ok(encrypted_challenge) =
|
if let Ok(encrypted_challenge) =
|
||||||
crypto_util::encrypt_challenge(&challenge, &app_pub_bundle)
|
crypto_util::encrypt_challenge(&challenge, &app_pub_bundle)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue