[Fix] IPC, cli, daemon
This commit is contained in:
parent
36a70e82a0
commit
56aad3a023
32 changed files with 1356 additions and 399 deletions
|
|
@ -37,16 +37,19 @@ jobs:
|
|||
DOCKER_PASSWD="$(printf '%s' "$DOCKER_PASSWD" | tr -d '\r\n')"
|
||||
printf '%s' "$DOCKER_PASSWD" | nix-shell -p docker --run "docker login docker.io --username \"$DOCKER_USER\" --password-stdin"
|
||||
|
||||
- name: Build & Push
|
||||
- name: Build & Push Docker image
|
||||
run: |
|
||||
nix-shell -p docker --run "docker build -f dockerfile -t tensamin/iota:latest . && docker push tensamin/iota:latest"
|
||||
|
||||
- name: Build release binary
|
||||
- name: Build release binaries
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
nix build .#iota --print-build-logs
|
||||
install -Dm755 result/bin/iota dist/iota
|
||||
nix build .#iota-daemon --print-build-logs
|
||||
install -Dm755 result/bin/iota-daemon dist/iota-daemon
|
||||
|
||||
nix build .#iota-ui --print-build-logs
|
||||
install -Dm755 result/bin/iota-ui dist/iota-ui
|
||||
|
||||
- name: Read release metadata
|
||||
id: version
|
||||
|
|
@ -55,7 +58,7 @@ jobs:
|
|||
run: |
|
||||
set -eu
|
||||
|
||||
VERSION="$(nix eval --raw .#iota.version)"
|
||||
VERSION="$(nix eval --raw .#iota-daemon.version)"
|
||||
SHORT_SHA="$(git rev-parse --short=7 HEAD)"
|
||||
|
||||
case "$RELEASE_TYPE" in
|
||||
|
|
@ -73,18 +76,15 @@ jobs:
|
|||
;;
|
||||
esac
|
||||
|
||||
ASSET_PATH="dist/iota"
|
||||
ASSET_NAME="iota"
|
||||
test -x "$ASSET_PATH"
|
||||
|
||||
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
|
||||
echo "tag=$TAG" >> "$FORGEJO_OUTPUT"
|
||||
echo "title=$TAG" >> "$FORGEJO_OUTPUT"
|
||||
echo "prerelease=$PRERELEASE" >> "$FORGEJO_OUTPUT"
|
||||
echo "asset_path=$ASSET_PATH" >> "$FORGEJO_OUTPUT"
|
||||
echo "asset_name=$ASSET_NAME" >> "$FORGEJO_OUTPUT"
|
||||
|
||||
- name: Create release and upload binary
|
||||
test -x "dist/iota-daemon"
|
||||
test -x "dist/iota-ui"
|
||||
|
||||
- name: Create release and upload binaries
|
||||
env:
|
||||
TOKEN: ${{ forgejo.token }}
|
||||
API: ${{ forgejo.api_url }}
|
||||
|
|
@ -93,8 +93,6 @@ jobs:
|
|||
TAG: ${{ steps.version.outputs.tag }}
|
||||
TITLE: ${{ steps.version.outputs.title }}
|
||||
PRERELEASE: ${{ steps.version.outputs.prerelease }}
|
||||
ASSET_PATH: ${{ steps.version.outputs.asset_path }}
|
||||
ASSET_NAME: ${{ steps.version.outputs.asset_name }}
|
||||
DESCRIPTION: ${{ inputs.description }}
|
||||
run: |
|
||||
nix-shell -p curl jq --run '
|
||||
|
|
@ -122,7 +120,11 @@ jobs:
|
|||
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
|
||||
fi
|
||||
|
||||
curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$ASSET_NAME" \
|
||||
curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=iota-daemon" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-F "attachment=@$ASSET_PATH"
|
||||
'
|
||||
-F "attachment=@dist/iota-daemon"
|
||||
|
||||
curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=iota-ui" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-F "attachment=@dist/iota-ui"
|
||||
'
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -2,3 +2,8 @@ target
|
|||
logs
|
||||
agreements
|
||||
languages/
|
||||
.envrc
|
||||
.direnv
|
||||
config.json
|
||||
*.mk
|
||||
*.sqlite*
|
||||
|
|
|
|||
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -2091,6 +2091,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"iota-cli",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2186,6 +2187,7 @@ dependencies = [
|
|||
"sysinfo",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"tungstenite",
|
||||
"walkdir",
|
||||
"warp",
|
||||
|
|
@ -2213,6 +2215,7 @@ dependencies = [
|
|||
"iota-storage",
|
||||
"omikron-connector",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"web-server",
|
||||
]
|
||||
|
||||
|
|
@ -2220,15 +2223,19 @@ dependencies = [
|
|||
name = "iota-daemon-lib"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"iota-ipc",
|
||||
"iota-logger",
|
||||
"iota-state",
|
||||
"iota-storage",
|
||||
"iota-util",
|
||||
"libc",
|
||||
"mtp",
|
||||
"omikron-connector",
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3082,6 +3089,7 @@ dependencies = [
|
|||
"reqwest",
|
||||
"sha2 0.10.9",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"uuid",
|
||||
"x448",
|
||||
]
|
||||
|
|
@ -4898,6 +4906,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
|
@ -5263,10 +5272,10 @@ dependencies = [
|
|||
"bytes",
|
||||
"http 1.4.2",
|
||||
"iota-logger",
|
||||
"iota-state",
|
||||
"iota-util",
|
||||
"mtp",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@ FROM rust:latest AS builder
|
|||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
RUN cargo build --release
|
||||
RUN cargo build --release -p iota-daemon
|
||||
|
||||
# Runtime stage
|
||||
FROM debian:sid
|
||||
|
|
@ -13,8 +13,10 @@ WORKDIR /app
|
|||
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /app/target/release/iota-core .
|
||||
COPY --from=builder /app/target/release/iota-daemon .
|
||||
|
||||
RUN useradd -r -s /bin/false iota && mkdir -p /run/iota && chown iota:iota /run/iota
|
||||
|
||||
EXPOSE 1984
|
||||
|
||||
CMD ["./iota-core"]
|
||||
CMD ["./iota-daemon"]
|
||||
|
|
|
|||
69
flake.nix
69
flake.nix
|
|
@ -38,10 +38,13 @@
|
|||
rustToolchain = rustPkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = ["rust-src" "rust-analyzer" "clippy" "rustfmt"];
|
||||
};
|
||||
commonBuildInputs = with pkgs; [openssl sqlite];
|
||||
commonNativeBuildInputs = with pkgs; [cmake perl pkg-config];
|
||||
in {
|
||||
packages = {
|
||||
default = self'.packages.iota;
|
||||
iota = pkgs.rustPlatform.buildRustPackage {
|
||||
default = self'.packages.iota-daemon;
|
||||
|
||||
iota-daemon = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "iota-daemon";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
|
|
@ -50,8 +53,8 @@
|
|||
lockFile = ./Cargo.lock;
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
nativeBuildInputs = with pkgs; [cmake perl pkg-config];
|
||||
buildInputs = with pkgs; [openssl sqlite];
|
||||
nativeBuildInputs = commonNativeBuildInputs;
|
||||
buildInputs = commonBuildInputs;
|
||||
dontUseCmakeConfigure = true;
|
||||
postInstall = ''
|
||||
for f in $out/bin/*; do
|
||||
|
|
@ -62,11 +65,36 @@
|
|||
'';
|
||||
passthru.dataDir = "/var/lib/iota";
|
||||
};
|
||||
|
||||
iota-ui = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "iota-ui";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
cargoBuildFlags = ["-p" "iota"];
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
nativeBuildInputs = commonNativeBuildInputs;
|
||||
buildInputs = commonBuildInputs;
|
||||
dontUseCmakeConfigure = true;
|
||||
postInstall = ''
|
||||
for f in $out/bin/*; do
|
||||
if [ "$(basename "$f")" != "iota" ]; then
|
||||
rm "$f"
|
||||
fi
|
||||
done
|
||||
# Rename to avoid confusion
|
||||
if [ -f "$out/bin/iota" ]; then
|
||||
mv "$out/bin/iota" "$out/bin/iota-ui"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [rustToolchain git cmake perl pkg-config];
|
||||
buildInputs = with pkgs; [openssl sqlite];
|
||||
buildInputs = commonBuildInputs;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -78,7 +106,7 @@
|
|||
...
|
||||
}: let
|
||||
cfg = config.services.iota;
|
||||
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default 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}");
|
||||
|
||||
configFile =
|
||||
if cfg.settingsFile != null
|
||||
|
|
@ -158,14 +186,27 @@
|
|||
|
||||
users.groups.iota = {};
|
||||
|
||||
systemd.services.iota = {
|
||||
systemd.sockets.iota-daemon = {
|
||||
description = "${descriptionText} IPC socket";
|
||||
wantedBy = ["sockets.target"];
|
||||
socketConfig = {
|
||||
ListenStream = "/run/iota/iota.sock";
|
||||
SocketMode = "0660";
|
||||
SocketUser = "iota";
|
||||
SocketGroup = "iota";
|
||||
Backlog = 5;
|
||||
RemoveOnStop = "true";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.iota-daemon = {
|
||||
description = descriptionText;
|
||||
wantedBy = ["multi-user.target"];
|
||||
after = ["network.target"];
|
||||
requires = ["iota-daemon.socket"];
|
||||
|
||||
serviceConfig =
|
||||
{
|
||||
Type = "simple";
|
||||
Type = "notify";
|
||||
User = "iota";
|
||||
Group = "iota";
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
|
|
@ -186,11 +227,19 @@
|
|||
'')
|
||||
];
|
||||
|
||||
Restart = "always";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5s";
|
||||
RuntimeDirectory = "iota";
|
||||
RuntimeDirectoryMode = "0750";
|
||||
|
||||
# Exit code 75 = restart requested
|
||||
RestartPreventExitStatus = "0";
|
||||
RestartForceExitStatus = "75";
|
||||
|
||||
TimeoutStopSec = "10";
|
||||
KillMode = "mixed";
|
||||
KillSignal = "SIGTERM";
|
||||
|
||||
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
|
||||
CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"];
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ iota-logger = { path = "../iota-logger", optional = true }
|
|||
iota-state = { path = "../iota-state" }
|
||||
iota-storage = { path = "../iota-storage", optional = true }
|
||||
iota-terms = { path = "../iota-terms" }
|
||||
iota-util = { path = "../iota-util", optional = true }
|
||||
iota-util = { path = "../iota-util", optional = true }
|
||||
iota-ipc = { path = "../iota-ipc" }
|
||||
omikron-connector = { path = "../omikron-connector", optional = true }
|
||||
|
||||
|
|
@ -64,6 +64,7 @@ strum = "0.27.2"
|
|||
strum_macros = "0.27.2"
|
||||
sysinfo = "0.38.3"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
tokio-tungstenite = { version = "*", features = ["native-tls"] }
|
||||
tungstenite = "*"
|
||||
walkdir = "2.5.0"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,4 @@
|
|||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
#[cfg(feature = "legacy-commands")]
|
||||
use iota_logger::{log, log_cv};
|
||||
#[cfg(feature = "legacy-commands")]
|
||||
use iota_state::{ACTIVE_TASKS, RELOAD, SHUTDOWN};
|
||||
#[cfg(feature = "legacy-commands")]
|
||||
use iota_storage::users::{user_manager, user_profile::UserProfile};
|
||||
#[cfg(feature = "legacy-commands")]
|
||||
use iota_storage::util::config_util::modify_config;
|
||||
#[cfg(feature = "legacy-commands")]
|
||||
use iota_util::file_util;
|
||||
#[cfg(feature = "legacy-commands")]
|
||||
use mtp::codec::{CommunicationType, CommunicationValue};
|
||||
#[cfg(feature = "legacy-commands")]
|
||||
use omikron_connector::omikron_connection::OMIKRON_CONNECTION;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Rect,
|
||||
|
|
@ -47,7 +33,7 @@ pub struct ConsoleCard {
|
|||
|
||||
cursor: Arc<Mutex<bool>>,
|
||||
last_swap: Arc<Mutex<Instant>>,
|
||||
tab_index: usize,
|
||||
pending_restore: Arc<Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
impl ConsoleCard {
|
||||
|
|
@ -62,7 +48,7 @@ impl ConsoleCard {
|
|||
joins: Borders::NONE,
|
||||
cursor: Arc::new(Mutex::new(true)),
|
||||
last_swap: Arc::new(Mutex::new(Instant::now())),
|
||||
tab_index: 0,
|
||||
pending_restore: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,12 +98,12 @@ impl ConsoleCard {
|
|||
spans.push(Span::styled(" ", Style::default().fg(Color::White)));
|
||||
}
|
||||
spans.push(Span::styled(
|
||||
"send command (<help> for info)",
|
||||
"send command (/help for info)",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
} else {
|
||||
spans.push(Span::styled(
|
||||
" send command (<help> for info)",
|
||||
" send command (/help for info)",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
|
|
@ -295,6 +281,12 @@ impl InteractableElement for ConsoleCard {
|
|||
}
|
||||
|
||||
fn interact(&mut self, key: KeyEvent) -> InteractionResult {
|
||||
// Check if a previously failed command should be restored.
|
||||
if let Some(restored) = self.pending_restore.lock().unwrap().take() {
|
||||
self.content = restored;
|
||||
self.cursor_position = self.content.chars().count();
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
if self.content.is_empty() {
|
||||
|
|
@ -303,17 +295,15 @@ impl InteractableElement for ConsoleCard {
|
|||
|
||||
let command = self.content.clone();
|
||||
let ipc = self.ipc.clone();
|
||||
let seq = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
let restore = self.pending_restore.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = ipc.send_command(seq, command).await;
|
||||
if ipc.send_command(0, command.clone()).await.is_err() {
|
||||
*restore.lock().unwrap() = Some(command);
|
||||
}
|
||||
});
|
||||
|
||||
self.content.clear();
|
||||
self.cursor_position = 0;
|
||||
self.tab_index = 0;
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
|
|
@ -350,14 +340,7 @@ impl InteractableElement for ConsoleCard {
|
|||
self.cursor_position = self.content.chars().count();
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
if let Some(prefix) = self.current_prefix() {
|
||||
if prefix == "/" {
|
||||
self.tab_index = self.tab_index.saturating_add(1);
|
||||
}
|
||||
}
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Tab | KeyCode::BackTab => InteractionResult::Unhandled,
|
||||
_ => {
|
||||
if let Some(c) = key.code.as_char() {
|
||||
self.insert_at_cursor(c);
|
||||
|
|
@ -381,144 +364,3 @@ impl InteractableElement for ConsoleCard {
|
|||
self.focused = f;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "legacy-commands")]
|
||||
pub async fn run_command(command: &str) {
|
||||
let parts = command.split(" ").collect::<Vec<&str>>();
|
||||
|
||||
match parts.as_slice() {
|
||||
["tasks"] => {
|
||||
let active_tasks: Vec<String> =
|
||||
ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect();
|
||||
let info = if *SHUTDOWN.read().await && *RELOAD.read().await {
|
||||
"Rebooting, "
|
||||
} else if *SHUTDOWN.read().await {
|
||||
"Shutting , "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
log!("{}Active tasks: {:?}", info, active_tasks);
|
||||
}
|
||||
["fps"] => {
|
||||
let (fps, skips) = *FPS.read().await;
|
||||
log!("{:.1} FPS with {:.1}% of attempts skipped", fps, skips);
|
||||
}
|
||||
|
||||
["help"] => {
|
||||
log!("Available commands: tasks, fps, ping, user, reconnect, regenerate");
|
||||
}
|
||||
|
||||
["help", "tasks"] => {
|
||||
log!("Tasks command usage: tasks");
|
||||
}
|
||||
["help", "fps"] => {
|
||||
log!("FPS command usage: fps");
|
||||
}
|
||||
["help", "ping"] => {
|
||||
log!("Ping command usage: ping [time]");
|
||||
}
|
||||
["help", "user"] => {
|
||||
log!("User command usage: user add <username> | user remove <username> | user list");
|
||||
}
|
||||
["help", "reconnect"] => {
|
||||
log!("Reconnect command usage: reconnect. Retry connecting to the Omikron server");
|
||||
}
|
||||
["help", "regenerate"] => {
|
||||
log!(
|
||||
"Regenerate command usage: regenerate keys. Generate a new Iota key pair and reconnect"
|
||||
);
|
||||
}
|
||||
|
||||
["ping"] => {
|
||||
ping(20).await;
|
||||
}
|
||||
["ping", time] => {
|
||||
let time = time.parse::<u64>().unwrap_or(20);
|
||||
ping(time).await;
|
||||
}
|
||||
["user", "add", username] => {
|
||||
if let (Some(user), Some(_)) = omikron_connector::user_ops::create_user(username).await
|
||||
{
|
||||
log!("Created user {}", user.user_id);
|
||||
} else {
|
||||
log!("User creation: Failed to create user. See errors above.");
|
||||
}
|
||||
}
|
||||
["user", "remove", username] => {
|
||||
if let Some(user) = user_manager::get_user_by_username(username) {
|
||||
let msg = CommunicationValue::new(CommunicationType::DeleteUser)
|
||||
.with_sender(user.user_id as u64);
|
||||
let _ = OMIKRON_CONNECTION.send_message(&msg).await;
|
||||
user_manager::remove_user(user.user_id);
|
||||
log!("Removed user {}", user.user_id);
|
||||
} else {
|
||||
log!("User removal: Username doesn't exist");
|
||||
}
|
||||
}
|
||||
["user", "list"] => {
|
||||
let users: Vec<UserProfile> = user_manager::get_users();
|
||||
for user in users {
|
||||
let storage = file_util::get_designed_storage(user.user_id);
|
||||
log!(
|
||||
"> Username: {}, ID: {}, created at: {}, storage: {}",
|
||||
user.username,
|
||||
user.user_id,
|
||||
user.created_at,
|
||||
storage
|
||||
);
|
||||
}
|
||||
}
|
||||
["user", "info", username] => {
|
||||
if let Some(user) = user_manager::get_user_by_username(username) {
|
||||
user_manager::remove_user(user.user_id);
|
||||
log!("Removed user {}", user.user_id);
|
||||
} else {
|
||||
log!("User info: Username doesn't exist");
|
||||
}
|
||||
}
|
||||
["reconnect"] => {
|
||||
log!("Reconnecting to Omikron server...");
|
||||
OMIKRON_CONNECTION.reconnect().await;
|
||||
log!("Reconnected to Omikron server");
|
||||
}
|
||||
["regenerate", "keys"] => {
|
||||
log!("Regenerating Iota key pair...");
|
||||
modify_config(|cfg| {
|
||||
cfg.public_key = None;
|
||||
cfg.private_key = None;
|
||||
cfg.iota_id = None;
|
||||
});
|
||||
log!("Key pair regenerated. Reconnecting to Omikron server...");
|
||||
OMIKRON_CONNECTION.reconnect().await;
|
||||
log!("Reconnected with new key pair");
|
||||
}
|
||||
["reload"] | ["restart"] => {
|
||||
log!("Restarting");
|
||||
*RELOAD.write().await = true;
|
||||
*SHUTDOWN.write().await = true;
|
||||
}
|
||||
["shutdown"] | ["stop"] => {
|
||||
log!("Shutting down");
|
||||
*SHUTDOWN.write().await = true;
|
||||
}
|
||||
_ => {
|
||||
log!("Unknown command");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "legacy-commands")]
|
||||
pub async fn ping(time: u64) {
|
||||
let conn = OMIKRON_CONNECTION.clone();
|
||||
|
||||
let response_cv = conn
|
||||
.await_response(
|
||||
&CommunicationValue::new(CommunicationType::Ping),
|
||||
Some(Duration::from_secs(time)),
|
||||
)
|
||||
.await;
|
||||
match response_cv {
|
||||
Ok(response) => log_cv!(response),
|
||||
Err(err) => log!("Ping error: {:?}", err),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,472 @@
|
|||
use iota_ipc::{ClientMessage, DaemonMessage, read_msg, write_msg};
|
||||
use iota_ipc::{
|
||||
ClientMessage, DaemonMessage, LocalRequest, MIN_PROTOCOL_VERSION, PROTOCOL_VERSION,
|
||||
RequestEnvelope, ResponseResult, read_msg, write_msg,
|
||||
};
|
||||
use iota_state::{ClientState, UiLogEntry};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Result;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::net::UnixStream;
|
||||
use tokio::net::unix::OwnedWriteHalf;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::sync::{Mutex, oneshot, watch};
|
||||
|
||||
const INITIAL_BACKOFF: Duration = Duration::from_millis(200);
|
||||
const MAX_BACKOFF: Duration = Duration::from_secs(10);
|
||||
const MAX_RECONNECT_ATTEMPTS: u32 = 50;
|
||||
|
||||
/// Connection state exposed to the UI.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IpcConnectionState {
|
||||
Connecting,
|
||||
Connected,
|
||||
Reconnecting { attempt: u32 },
|
||||
Incompatible { message: String },
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
/// Pending request awaiting a response.
|
||||
struct PendingRequest {
|
||||
response_tx: oneshot::Sender<ResponseResult>,
|
||||
}
|
||||
|
||||
/* The TUI owns this cache. IPC updates replace daemon snapshots and append
|
||||
* logs, so rendering never reaches into daemon-owned storage or connections. */
|
||||
pub struct IpcClient {
|
||||
state: ClientState,
|
||||
writer: Mutex<OwnedWriteHalf>,
|
||||
next_request_id: AtomicU64,
|
||||
pending: Mutex<HashMap<u64, PendingRequest>>,
|
||||
connection_state: watch::Sender<IpcConnectionState>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl IpcClient {
|
||||
pub async fn connect(path: impl AsRef<Path>) -> Result<Arc<Self>> {
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let stream = Self::try_connect(&path).await?;
|
||||
let (mut reader, writer) = stream.into_split();
|
||||
|
||||
let (conn_state_tx, _) = watch::channel(IpcConnectionState::Connecting);
|
||||
let client = Arc::new(Self {
|
||||
state: ClientState::new(),
|
||||
writer: Mutex::new(writer),
|
||||
next_request_id: AtomicU64::new(1),
|
||||
pending: Mutex::new(HashMap::new()),
|
||||
connection_state: conn_state_tx,
|
||||
path: path.clone(),
|
||||
});
|
||||
|
||||
// --- Handshake: send Hello, read HelloAck ---
|
||||
{
|
||||
let mut w = client.writer.lock().await;
|
||||
write_msg(
|
||||
&mut *w,
|
||||
&ClientMessage::Hello {
|
||||
supported_versions: vec![MIN_PROTOCOL_VERSION, PROTOCOL_VERSION],
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
match read_msg::<_, DaemonMessage>(&mut reader).await {
|
||||
Ok(DaemonMessage::HelloAck(ack)) => {
|
||||
if ack.protocol_version < MIN_PROTOCOL_VERSION {
|
||||
let _ = client
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Incompatible {
|
||||
message: format!(
|
||||
"Daemon protocol {} < required {}",
|
||||
ack.protocol_version, MIN_PROTOCOL_VERSION
|
||||
),
|
||||
});
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
format!(
|
||||
"Protocol version mismatch: daemon={}, minimum={}",
|
||||
ack.protocol_version, MIN_PROTOCOL_VERSION
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Expected HelloAck from daemon",
|
||||
));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
let _ = client.connection_state.send(IpcConnectionState::Connected);
|
||||
|
||||
// Start reader task (continues reading after handshake)
|
||||
let reader_client = client.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Ok(message) = read_msg::<_, DaemonMessage>(&mut reader).await {
|
||||
reader_client.apply(message).await;
|
||||
}
|
||||
reader_client.read_loop(reader).await;
|
||||
});
|
||||
client.send(ClientMessage::Subscribe).await?;
|
||||
|
||||
// Subscribe to events
|
||||
client
|
||||
.send(ClientMessage::Subscribe {
|
||||
log_classes: vec![],
|
||||
metric_interval_ms: Some(500),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Try to connect with retries for socket activation.
|
||||
pub async fn connect_or_activate(path: impl AsRef<Path>) -> Result<Arc<Self>> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let max_attempts = 30;
|
||||
for attempt in 0..max_attempts {
|
||||
match Self::connect(&path).await {
|
||||
Ok(client) => return Ok(client),
|
||||
Err(error) => {
|
||||
if attempt < max_attempts - 1 {
|
||||
let delay = Duration::from_millis(100 + attempt as u64 * 100);
|
||||
tokio::time::sleep(delay).await;
|
||||
continue;
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn try_connect(path: &Path) -> Result<UnixStream> {
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
match UnixStream::connect(path).await {
|
||||
Ok(stream) => return Ok(stream),
|
||||
Err(error) => {
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
return Err(error);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the reconnection actor.
|
||||
pub fn spawn_reconnector(self: &Arc<Self>) {
|
||||
let client = self.clone();
|
||||
tokio::spawn(async move {
|
||||
client.reconnection_loop().await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn reconnection_loop(self: Arc<Self>) {
|
||||
let mut rx = self.connection_status();
|
||||
|
||||
loop {
|
||||
// Wait until the connection enters the Disconnected state.
|
||||
loop {
|
||||
let disconnected = matches!(*rx.borrow(), IpcConnectionState::Disconnected);
|
||||
if disconnected {
|
||||
break;
|
||||
}
|
||||
if rx.changed().await.is_err() {
|
||||
return; // sender dropped
|
||||
}
|
||||
}
|
||||
|
||||
let mut backoff = INITIAL_BACKOFF;
|
||||
let mut attempt: u32 = 0;
|
||||
|
||||
// Attempt reconnection until success or max attempts.
|
||||
loop {
|
||||
tokio::time::sleep(backoff).await;
|
||||
attempt += 1;
|
||||
|
||||
if attempt > MAX_RECONNECT_ATTEMPTS {
|
||||
let _ = self
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Incompatible {
|
||||
message: "Max reconnection attempts exceeded".into(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Reconnecting { attempt });
|
||||
|
||||
match Self::try_connect(&self.path).await {
|
||||
Ok(stream) => {
|
||||
let (mut reader, writer) = stream.into_split();
|
||||
*self.writer.lock().await = writer;
|
||||
|
||||
// Re-handshake
|
||||
{
|
||||
let mut w = self.writer.lock().await;
|
||||
if write_msg(
|
||||
&mut *w,
|
||||
&ClientMessage::Hello {
|
||||
supported_versions: vec![
|
||||
MIN_PROTOCOL_VERSION,
|
||||
PROTOCOL_VERSION,
|
||||
],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
let _ = self
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Disconnected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Read HelloAck
|
||||
match read_msg::<_, DaemonMessage>(&mut reader).await {
|
||||
Ok(DaemonMessage::HelloAck(ack)) => {
|
||||
if ack.protocol_version < MIN_PROTOCOL_VERSION {
|
||||
let _ = self.connection_state.send(
|
||||
IpcConnectionState::Incompatible {
|
||||
message: format!(
|
||||
"Daemon protocol {} < required {}",
|
||||
ack.protocol_version, MIN_PROTOCOL_VERSION
|
||||
),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let _ = self
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Disconnected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear pending requests with connection-lost errors
|
||||
{
|
||||
let mut pending = self.pending.lock().await;
|
||||
for (_, request) in pending.drain() {
|
||||
let _ = request.response_tx.send(
|
||||
ResponseResult::Error(
|
||||
iota_ipc::IpcErrorCode::Disconnected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.connection_state.send(IpcConnectionState::Connected);
|
||||
|
||||
// Start new reader loop
|
||||
let reader_client = self.clone();
|
||||
tokio::spawn(async move {
|
||||
reader_client.read_loop(reader).await;
|
||||
});
|
||||
|
||||
// Resubscribe
|
||||
let _ = self
|
||||
.send(ClientMessage::Subscribe {
|
||||
log_classes: vec![],
|
||||
metric_interval_ms: Some(500),
|
||||
})
|
||||
.await;
|
||||
|
||||
// Successfully reconnected; go back to waiting for
|
||||
// the next disconnect.
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
backoff = std::cmp::min(backoff * 2, MAX_BACKOFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_loop(self: Arc<Self>, mut reader: OwnedReadHalf) {
|
||||
loop {
|
||||
match read_msg::<_, DaemonMessage>(&mut reader).await {
|
||||
Ok(message) => self.apply(message).await,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
let _ = self.connection_state.send(IpcConnectionState::Disconnected);
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = self.connection_state.send(IpcConnectionState::Disconnected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> ClientState {
|
||||
self.state.clone()
|
||||
}
|
||||
|
||||
pub async fn send_command(&self, seq: u64, line: String) -> Result<()> {
|
||||
self.send(ClientMessage::Command { seq, line }).await
|
||||
pub fn connection_status(&self) -> watch::Receiver<IpcConnectionState> {
|
||||
self.connection_state.subscribe()
|
||||
}
|
||||
|
||||
pub fn connection_status_snapshot(&self) -> IpcConnectionState {
|
||||
self.connection_state.borrow().clone()
|
||||
}
|
||||
|
||||
pub async fn send_request(&self, request: LocalRequest) -> Result<ResponseResult> {
|
||||
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
|
||||
{
|
||||
let mut pending = self.pending.lock().await;
|
||||
pending.insert(request_id, PendingRequest { response_tx });
|
||||
}
|
||||
|
||||
let envelope = RequestEnvelope {
|
||||
request_id,
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
request,
|
||||
};
|
||||
self.send(ClientMessage::Request(envelope)).await?;
|
||||
|
||||
match tokio::time::timeout(Duration::from_secs(30), response_rx).await {
|
||||
Ok(Ok(result)) => Ok(result),
|
||||
Ok(Err(_)) => Ok(ResponseResult::Error(iota_ipc::IpcErrorCode::Disconnected)),
|
||||
Err(_) => {
|
||||
self.pending.lock().await.remove(&request_id);
|
||||
Ok(ResponseResult::Error(iota_ipc::IpcErrorCode::Disconnected))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a legacy console command string into a typed request.
|
||||
pub fn parse_console_command(line: &str) -> Option<LocalRequest> {
|
||||
let parts: Vec<&str> = line.trim_start_matches('/').split_whitespace().collect();
|
||||
match parts.as_slice() {
|
||||
["help"] => None,
|
||||
["tasks"] => Some(LocalRequest::ListTasks),
|
||||
["user", "add", username] => Some(LocalRequest::CreateUser {
|
||||
username: username.to_string(),
|
||||
}),
|
||||
["user", "remove", user_id_str] => {
|
||||
let user_id = user_id_str.parse::<i64>().ok()?;
|
||||
Some(LocalRequest::RemoveUser { user_id })
|
||||
}
|
||||
["user", "list"] => Some(LocalRequest::ListUsers),
|
||||
["reconnect"] => Some(LocalRequest::ReconnectOmikron),
|
||||
["regenerate", "keys"] => Some(LocalRequest::RotateIotaIdentity),
|
||||
["reload"] | ["restart"] => Some(LocalRequest::RestartDaemon),
|
||||
["shutdown"] | ["stop"] => Some(LocalRequest::StopDaemon),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy command interface: parse text command, send as typed request.
|
||||
pub async fn send_command(&self, _seq: u64, line: String) -> Result<()> {
|
||||
let trimmed = line.trim_start_matches('/').trim();
|
||||
|
||||
// Handle ping as a direct Ping message (not a LocalRequest).
|
||||
if trimmed == "ping" || trimmed.starts_with("ping ") {
|
||||
let seq = self.next_request_id.fetch_add(1, Ordering::Relaxed);
|
||||
if let Err(e) = self.send(ClientMessage::Ping { seq }).await {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Console".into(),
|
||||
message: format!("Failed to send ping: {}", e),
|
||||
is_error: true,
|
||||
});
|
||||
return Err(e);
|
||||
}
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Console".into(),
|
||||
message: "Ping sent".into(),
|
||||
is_error: false,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(request) = Self::parse_console_command(&line) {
|
||||
match self.send_request(request).await {
|
||||
Ok(result) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
let message = match &result {
|
||||
ResponseResult::Ok(msg) => msg.clone(),
|
||||
ResponseResult::Error(code) => format!("Error: {:?}", code),
|
||||
};
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Command".into(),
|
||||
message,
|
||||
is_error: matches!(&result, ResponseResult::Error(_)),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Console".into(),
|
||||
message: format!("Failed to send: {}", e),
|
||||
is_error: true,
|
||||
});
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Console".into(),
|
||||
message: if line.trim() == "help" {
|
||||
"Available commands: tasks, ping, user, reconnect, regenerate, restart, stop"
|
||||
.into()
|
||||
} else {
|
||||
format!("Unknown command: {}", line)
|
||||
},
|
||||
is_error: false,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&self, message: ClientMessage) -> Result<()> {
|
||||
|
|
@ -46,19 +475,26 @@ impl IpcClient {
|
|||
}
|
||||
|
||||
async fn apply(&self, message: DaemonMessage) {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
match message {
|
||||
DaemonMessage::LogEntry(entry) => state.push_log(UiLogEntry {
|
||||
timestamp_ms: entry.timestamp_ms,
|
||||
sender: entry.sender,
|
||||
message: entry.message,
|
||||
is_error: entry.is_error,
|
||||
}),
|
||||
DaemonMessage::LogEntry(entry) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: entry.timestamp_ms,
|
||||
sender: entry.sender,
|
||||
message: entry.message,
|
||||
is_error: entry.is_error,
|
||||
});
|
||||
}
|
||||
DaemonMessage::StateUpdate(snapshot) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.cpu = snapshot.cpu;
|
||||
state.ram = snapshot.ram;
|
||||
state.ping = snapshot.ping;
|
||||
|
|
@ -66,18 +502,106 @@ impl IpcClient {
|
|||
state.net_down = snapshot.net_down;
|
||||
state.sys_info = snapshot.sys_info;
|
||||
}
|
||||
DaemonMessage::CommandResult {
|
||||
success, message, ..
|
||||
} => state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Command".into(),
|
||||
message,
|
||||
is_error: !success,
|
||||
}),
|
||||
DaemonMessage::MetricSample(sample) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
if let Some(cpu) = sample.cpu {
|
||||
let idx = state.cpu.len() as f64;
|
||||
state.cpu.push((idx, cpu));
|
||||
if state.cpu.len() > iota_state::MAX_POINTS {
|
||||
state.cpu.remove(0);
|
||||
}
|
||||
}
|
||||
if let Some(ram) = sample.ram {
|
||||
let idx = state.ram.len() as f64;
|
||||
state.ram.push((idx, ram));
|
||||
if state.ram.len() > iota_state::MAX_POINTS {
|
||||
state.ram.remove(0);
|
||||
}
|
||||
}
|
||||
if let Some(ping) = sample.ping {
|
||||
state.push_ping_val(ping);
|
||||
}
|
||||
if let Some(net_up) = sample.net_up {
|
||||
let idx = state.net_up.len() as f64;
|
||||
state.net_up.push((idx, net_up));
|
||||
if state.net_up.len() > iota_state::MAX_POINTS {
|
||||
state.net_up.remove(0);
|
||||
}
|
||||
}
|
||||
if let Some(net_down) = sample.net_down {
|
||||
let idx = state.net_down.len() as f64;
|
||||
state.net_down.push((idx, net_down));
|
||||
if state.net_down.len() > iota_state::MAX_POINTS {
|
||||
state.net_down.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
DaemonMessage::Response(response) => {
|
||||
let mut pending = self.pending.lock().await;
|
||||
if let Some(request) = pending.remove(&response.request_id) {
|
||||
let _ = request.response_tx.send(response.result);
|
||||
} else {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
let message = match &response.result {
|
||||
ResponseResult::Ok(msg) => msg.clone(),
|
||||
ResponseResult::Error(code) => format!("Error: {:?}", code),
|
||||
};
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Command".into(),
|
||||
message,
|
||||
is_error: matches!(&response.result, ResponseResult::Error(_)),
|
||||
});
|
||||
}
|
||||
}
|
||||
DaemonMessage::HelloAck(_) => {}
|
||||
DaemonMessage::Pong { .. } => {}
|
||||
DaemonMessage::LifecycleEvent(event) => match event {
|
||||
iota_ipc::LifecycleEvent::Shutdown { reason } => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Daemon".into(),
|
||||
message: format!("Daemon shutting down: {}", reason),
|
||||
is_error: true,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
DaemonMessage::Gap { skipped } => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "System".into(),
|
||||
message: format!("Skipped {} messages, resynchronizing", skipped),
|
||||
is_error: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
log_card::LogCard,
|
||||
},
|
||||
interaction_result::InteractionResult,
|
||||
ipc_client::IpcConnectionState,
|
||||
screens::screens::{NavDirection, Screen},
|
||||
ui::UI,
|
||||
};
|
||||
|
|
@ -16,6 +17,7 @@ use ratatui::{
|
|||
layout::{Constraint, Layout, Margin, Rect},
|
||||
widgets::{Block, Borders},
|
||||
};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
|
|
@ -24,6 +26,7 @@ pub struct MainScreen {
|
|||
nav_grid: Vec<Vec<Option<usize>>>,
|
||||
selected_coords: (usize, usize),
|
||||
graphs_open: bool,
|
||||
connection_status_rx: watch::Receiver<IpcConnectionState>,
|
||||
}
|
||||
|
||||
impl MainScreen {
|
||||
|
|
@ -58,11 +61,14 @@ impl MainScreen {
|
|||
|
||||
let graphs_open = true;
|
||||
|
||||
let connection_status_rx = ui.ipc().connection_status();
|
||||
|
||||
let mut screen = MainScreen {
|
||||
elements,
|
||||
nav_grid,
|
||||
selected_coords: (1, 0),
|
||||
graphs_open,
|
||||
connection_status_rx,
|
||||
};
|
||||
screen.focus_current();
|
||||
screen
|
||||
|
|
@ -147,6 +153,40 @@ impl MainScreen {
|
|||
|
||||
self.focus_current();
|
||||
}
|
||||
|
||||
/// Cycle focus between unique elements in the navigation grid.
|
||||
fn navigate_focus(&mut self, forward: bool) {
|
||||
// Collect unique elements in grid order.
|
||||
let mut positions: Vec<(usize, usize)> = Vec::new(); // (row, col)
|
||||
let mut seen: Vec<Option<usize>> = Vec::new();
|
||||
for (y, row) in self.nav_grid.iter().enumerate() {
|
||||
for (x, elem_opt) in row.iter().enumerate() {
|
||||
if elem_opt.is_some() && !seen.contains(elem_opt) {
|
||||
seen.push(*elem_opt);
|
||||
positions.push((y, x));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current = self.selected_coords;
|
||||
let current_pos = positions
|
||||
.iter()
|
||||
.position(|&(r, c)| r == current.0 && c == current.1);
|
||||
|
||||
let next_pos = if let Some(idx) = current_pos {
|
||||
if forward {
|
||||
(idx + 1) % positions.len()
|
||||
} else {
|
||||
(idx + positions.len() - 1) % positions.len()
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
self.unfocus_current(self.selected_coords.0, self.selected_coords.1);
|
||||
self.selected_coords = positions[next_pos];
|
||||
self.focus_current();
|
||||
}
|
||||
}
|
||||
|
||||
impl Screen for MainScreen {
|
||||
|
|
@ -159,7 +199,21 @@ impl Screen for MainScreen {
|
|||
}
|
||||
|
||||
fn render(&self, f: &mut Frame, rect: Rect) {
|
||||
let main_block = Block::default().title("Main").borders(Borders::ALL);
|
||||
let status = self.connection_status_rx.borrow();
|
||||
let status_text = match &*status {
|
||||
IpcConnectionState::Connected => "Connected".to_string(),
|
||||
IpcConnectionState::Connecting => "Connecting...".to_string(),
|
||||
IpcConnectionState::Reconnecting { attempt } => {
|
||||
format!("Reconnecting (attempt {})...", attempt)
|
||||
}
|
||||
IpcConnectionState::Incompatible { message } => {
|
||||
format!("Incompatible: {}", message)
|
||||
}
|
||||
IpcConnectionState::Disconnected => "Disconnected".to_string(),
|
||||
};
|
||||
let main_block = Block::default()
|
||||
.title(format!("Main [{}]", status_text))
|
||||
.borders(Borders::ALL);
|
||||
f.render_widget(main_block, rect);
|
||||
|
||||
let inner = rect.inner(Margin {
|
||||
|
|
@ -215,10 +269,14 @@ impl Screen for MainScreen {
|
|||
|
||||
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
|
||||
match event.code {
|
||||
KeyCode::Up => self.navigate(NavDirection::Up),
|
||||
KeyCode::Down => self.navigate(NavDirection::Down),
|
||||
KeyCode::Left => self.navigate(NavDirection::Left),
|
||||
KeyCode::Right => self.navigate(NavDirection::Right),
|
||||
KeyCode::Tab => {
|
||||
self.navigate_focus(true);
|
||||
return InteractionResult::Handled;
|
||||
}
|
||||
KeyCode::BackTab => {
|
||||
self.navigate_focus(false);
|
||||
return InteractionResult::Handled;
|
||||
}
|
||||
KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => {
|
||||
self.graphs_open = !self.graphs_open;
|
||||
for element in self.elements.iter_mut() {
|
||||
|
|
@ -232,7 +290,17 @@ impl Screen for MainScreen {
|
|||
let (y, x) = self.selected_coords;
|
||||
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) {
|
||||
if let Some(el) = self.elements.get_mut(*index) {
|
||||
return el.interact(event);
|
||||
let result = el.interact(event);
|
||||
if matches!(result, InteractionResult::Unhandled) {
|
||||
match event.code {
|
||||
KeyCode::Up => self.navigate(NavDirection::Up),
|
||||
KeyCode::Down => self.navigate(NavDirection::Down),
|
||||
KeyCode::Left => self.navigate(NavDirection::Left),
|
||||
KeyCode::Right => self.navigate(NavDirection::Right),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,3 +24,4 @@ pnet = "0.35.0"
|
|||
ratatui = "0.30.0"
|
||||
reqwest = "0.13.2"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ async fn main() {
|
|||
if !web_server::start(port).await {
|
||||
log!("Failed to start the MTP web server on port {}", port);
|
||||
}
|
||||
let _ = omikron::omikron_connection::get_omikron_connection().await;
|
||||
let _ = omikron::omikron_connection::get_omikron_connection(tokio_util::sync::CancellationToken::new()).await;
|
||||
|
||||
log_t!("setup_completed");
|
||||
loop {
|
||||
|
|
|
|||
|
|
@ -11,5 +11,9 @@ iota-storage = { path = "../iota-storage" }
|
|||
iota-util = { path = "../iota-util" }
|
||||
omikron-connector = { path = "../omikron-connector" }
|
||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
||||
dashmap = "6.1.0"
|
||||
libc = "0.2"
|
||||
sysinfo = "0.38.3"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
uuid = { version = "*", features = ["v4"] }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use crate::DaemonRuntime;
|
||||
use iota_ipc::DaemonMessage;
|
||||
use iota_ipc::{
|
||||
IpcErrorCode, LocalRequest, ResponseEnvelope, ResponseResult,
|
||||
};
|
||||
use iota_logger::{log, log_command};
|
||||
use iota_storage::users::user_manager;
|
||||
use iota_storage::util::config_util::modify_config;
|
||||
|
|
@ -8,6 +10,8 @@ use omikron_connector::omikron_connection::OMIKRON_CONNECTION;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::daemon_state::ShutdownReason;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CommandRouter {
|
||||
runtime: Arc<DaemonRuntime>,
|
||||
|
|
@ -18,85 +22,124 @@ impl CommandRouter {
|
|||
Self { runtime }
|
||||
}
|
||||
|
||||
pub async fn route(&self, seq: u64, line: String) -> DaemonMessage {
|
||||
log_command!("{}", line);
|
||||
let result = self.execute(&line).await;
|
||||
DaemonMessage::CommandResult {
|
||||
seq,
|
||||
success: result.is_ok(),
|
||||
message: result.unwrap_or_else(|error| error),
|
||||
pub async fn route(&self, request_id: u64, request: LocalRequest) -> ResponseEnvelope {
|
||||
log_command!("{:?}", request);
|
||||
let result = self.execute(request).await;
|
||||
ResponseEnvelope {
|
||||
request_id,
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute(&self, line: &str) -> Result<String, String> {
|
||||
let parts = line
|
||||
/// Parse a legacy console command string into a typed request.
|
||||
pub fn parse_console_command(line: &str) -> Option<LocalRequest> {
|
||||
let parts: Vec<&str> = line
|
||||
.trim_start_matches('/')
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
match parts.as_slice() {
|
||||
["tasks"] => Ok(self
|
||||
.runtime
|
||||
.state
|
||||
.active_tasks
|
||||
.iter()
|
||||
.map(|task| task.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")),
|
||||
["help"] => Ok(
|
||||
"Available commands: tasks, ping, user, reconnect, regenerate, reload, shutdown"
|
||||
.into(),
|
||||
),
|
||||
["ping"] => self.ping(20).await,
|
||||
["ping", seconds] => self.ping(seconds.parse::<u64>().unwrap_or(20)).await,
|
||||
["user", "add", username] => {
|
||||
let (user, _) = omikron_connector::user_ops::create_user(username).await;
|
||||
user.map(|user| format!("Created user {}", user.user_id))
|
||||
.ok_or_else(|| "User creation failed".into())
|
||||
}
|
||||
["help"] => None,
|
||||
["tasks"] => Some(LocalRequest::ListTasks),
|
||||
["ping", _] | ["ping"] => None,
|
||||
["user", "add", username] => Some(LocalRequest::CreateUser {
|
||||
username: username.to_string(),
|
||||
}),
|
||||
["user", "remove", username] => {
|
||||
let user = user_manager::get_user_by_username(username)
|
||||
.ok_or_else(|| "Username does not exist".to_string())?;
|
||||
let user = user_manager::get_user_by_username(username)?;
|
||||
Some(LocalRequest::RemoveUser {
|
||||
user_id: user.user_id,
|
||||
})
|
||||
}
|
||||
["user", "list"] => Some(LocalRequest::ListUsers),
|
||||
["reconnect"] => Some(LocalRequest::ReconnectOmikron),
|
||||
["regenerate", "keys"] => Some(LocalRequest::RotateIotaIdentity),
|
||||
["reload"] | ["restart"] => Some(LocalRequest::RestartDaemon),
|
||||
["shutdown"] | ["stop"] => Some(LocalRequest::StopDaemon),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute(&self, request: LocalRequest) -> ResponseResult {
|
||||
match request {
|
||||
LocalRequest::GetStatus => {
|
||||
let phase = self.runtime.current_startup_phase();
|
||||
let degraded = self.runtime.degraded_reason.borrow().clone();
|
||||
let tasks: Vec<String> = self
|
||||
.runtime
|
||||
.state
|
||||
.active_tasks
|
||||
.iter()
|
||||
.map(|task| task.to_string())
|
||||
.collect();
|
||||
let mut info = format!("Phase: {:?}, Tasks: {}", phase, tasks.join(", "));
|
||||
if let Some(reason) = degraded {
|
||||
info.push_str(&format!(", Degraded: {}", reason));
|
||||
}
|
||||
ResponseResult::Ok(info)
|
||||
}
|
||||
LocalRequest::ListTasks => {
|
||||
let tasks: Vec<String> = self
|
||||
.runtime
|
||||
.state
|
||||
.active_tasks
|
||||
.iter()
|
||||
.map(|task| task.to_string())
|
||||
.collect();
|
||||
ResponseResult::Ok(tasks.join(", "))
|
||||
}
|
||||
LocalRequest::ListUsers => {
|
||||
let users: Vec<String> = user_manager::get_users()
|
||||
.into_iter()
|
||||
.map(|user| format!("{} ({})", user.username, user.user_id))
|
||||
.collect();
|
||||
ResponseResult::Ok(users.join("\n"))
|
||||
}
|
||||
LocalRequest::CreateUser { username } => {
|
||||
match omikron_connector::user_ops::create_user(&username).await {
|
||||
(Some(user), _) => {
|
||||
ResponseResult::Ok(format!("Created user {}", user.user_id))
|
||||
}
|
||||
_ => ResponseResult::Error(IpcErrorCode::StorageFailure),
|
||||
}
|
||||
}
|
||||
LocalRequest::RemoveUser { user_id } => {
|
||||
let user = match user_manager::get_user(user_id) {
|
||||
Some(user) => user,
|
||||
None => return ResponseResult::Error(IpcErrorCode::NotFound),
|
||||
};
|
||||
let message = CommunicationValue::new(CommunicationType::DeleteUser)
|
||||
.with_sender(user.user_id as u64);
|
||||
OMIKRON_CONNECTION
|
||||
.send_message(&message)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
if let Err(_e) = OMIKRON_CONNECTION.send_message(&message).await {
|
||||
return ResponseResult::Error(IpcErrorCode::OmikronUnavailable);
|
||||
}
|
||||
user_manager::remove_user(user.user_id);
|
||||
Ok(format!("Removed user {}", user.user_id))
|
||||
ResponseResult::Ok(format!("Removed user {}", user.user_id))
|
||||
}
|
||||
["user", "list"] => Ok(user_manager::get_users()
|
||||
.into_iter()
|
||||
.map(|user| format!("{} ({})", user.username, user.user_id))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")),
|
||||
["reconnect"] => {
|
||||
LocalRequest::ReconnectOmikron => {
|
||||
OMIKRON_CONNECTION.reconnect().await;
|
||||
Ok("Reconnected to Omikron server".into())
|
||||
ResponseResult::Ok("Reconnected to Omikron server".into())
|
||||
}
|
||||
["regenerate", "keys"] => {
|
||||
LocalRequest::RotateIotaIdentity => {
|
||||
modify_config(|config| {
|
||||
config.public_key = None;
|
||||
config.private_key = None;
|
||||
config.iota_id = None;
|
||||
});
|
||||
OMIKRON_CONNECTION.reconnect().await;
|
||||
Ok("Key pair regenerated and Omikron reconnection requested".into())
|
||||
ResponseResult::Ok("Key pair regenerated and Omikron reconnection requested".into())
|
||||
}
|
||||
["reload"] | ["restart"] => {
|
||||
*self.runtime.state.reload.write().await = true;
|
||||
*self.runtime.state.shutdown.write().await = true;
|
||||
Ok("Daemon restart requested".into())
|
||||
LocalRequest::RestartDaemon => {
|
||||
self.runtime.shutdown(ShutdownReason::Restart);
|
||||
ResponseResult::Ok("Daemon restart requested".into())
|
||||
}
|
||||
["shutdown"] | ["stop"] => {
|
||||
*self.runtime.state.shutdown.write().await = true;
|
||||
Ok("Daemon shutdown requested".into())
|
||||
LocalRequest::StopDaemon => {
|
||||
self.runtime.shutdown(ShutdownReason::Stop);
|
||||
ResponseResult::Ok("Daemon shutdown requested".into())
|
||||
}
|
||||
_ => Err("Unknown command".into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn ping(&self, seconds: u64) -> Result<String, String> {
|
||||
pub async fn ping(&self, seconds: u64) -> Result<String, String> {
|
||||
let response = OMIKRON_CONNECTION
|
||||
.await_response(
|
||||
&CommunicationValue::new(CommunicationType::Ping),
|
||||
|
|
|
|||
|
|
@ -3,21 +3,123 @@ use iota_state::DaemonState;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use sysinfo::{RefreshKind, System};
|
||||
use tokio::sync::watch;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// Reason the daemon is shutting down.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ShutdownReason {
|
||||
Stop,
|
||||
Restart,
|
||||
Fatal(String),
|
||||
}
|
||||
|
||||
impl ShutdownReason {
|
||||
pub fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
ShutdownReason::Stop => 0,
|
||||
ShutdownReason::Restart => 75,
|
||||
ShutdownReason::Fatal(_) => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the lifecycle phase of the daemon for IPC visibility.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum StartupPhase {
|
||||
Starting,
|
||||
MigratingStorage,
|
||||
LoadingUsers,
|
||||
StartingServices,
|
||||
Ready,
|
||||
Degraded,
|
||||
Stopping,
|
||||
}
|
||||
|
||||
impl From<StartupPhase> for iota_ipc::StartupPhase {
|
||||
fn from(phase: StartupPhase) -> Self {
|
||||
match phase {
|
||||
StartupPhase::Starting => iota_ipc::StartupPhase::Starting,
|
||||
StartupPhase::MigratingStorage => iota_ipc::StartupPhase::MigratingStorage,
|
||||
StartupPhase::LoadingUsers => iota_ipc::StartupPhase::LoadingUsers,
|
||||
StartupPhase::StartingServices => iota_ipc::StartupPhase::StartingServices,
|
||||
StartupPhase::Ready => iota_ipc::StartupPhase::Ready,
|
||||
StartupPhase::Degraded => iota_ipc::StartupPhase::Degraded,
|
||||
StartupPhase::Stopping => iota_ipc::StartupPhase::Stopping,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* This wrapper exposes daemon state as IPC-safe snapshots while preserving a
|
||||
* single owned state instance for all daemon subsystems. */
|
||||
#[derive(Clone, Default)]
|
||||
* single owned state instance for all daemon subsystems. The cancellation token
|
||||
* is the single lifecycle signal — all subsystems check it instead of a
|
||||
* separate boolean. */
|
||||
pub struct DaemonRuntime {
|
||||
pub state: Arc<DaemonState>,
|
||||
pub cancellation: CancellationToken,
|
||||
pub shutdown_tx: watch::Sender<Option<ShutdownReason>>,
|
||||
pub startup_phase: watch::Sender<StartupPhase>,
|
||||
pub degraded_reason: watch::Sender<Option<String>>,
|
||||
}
|
||||
|
||||
impl Clone for DaemonRuntime {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
state: self.state.clone(),
|
||||
cancellation: self.cancellation.clone(),
|
||||
shutdown_tx: self.shutdown_tx.clone(),
|
||||
startup_phase: self.startup_phase.clone(),
|
||||
degraded_reason: self.degraded_reason.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DaemonRuntime {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DaemonRuntime {
|
||||
pub fn new() -> Self {
|
||||
let (shutdown_tx, _) = watch::channel(None);
|
||||
let (startup_phase, _) = watch::channel(StartupPhase::Starting);
|
||||
let (degraded_reason, _) = watch::channel(None);
|
||||
Self {
|
||||
state: Arc::new(DaemonState::new()),
|
||||
cancellation: CancellationToken::new(),
|
||||
shutdown_tx,
|
||||
startup_phase,
|
||||
degraded_reason,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shutdown(&self, reason: ShutdownReason) {
|
||||
self.cancellation.cancel();
|
||||
let _ = self.shutdown_tx.send(Some(reason));
|
||||
}
|
||||
|
||||
pub fn shutdown_reason(&self) -> Option<ShutdownReason> {
|
||||
self.shutdown_tx.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn is_shutting_down(&self) -> bool {
|
||||
self.cancellation.is_cancelled()
|
||||
}
|
||||
|
||||
pub fn set_startup_phase(&self, phase: StartupPhase) {
|
||||
let _ = self.startup_phase.send(phase);
|
||||
}
|
||||
|
||||
pub fn current_startup_phase(&self) -> StartupPhase {
|
||||
*self.startup_phase.borrow()
|
||||
}
|
||||
|
||||
pub fn mark_degraded(&self, reason: String) {
|
||||
let _ = self.degraded_reason.send(Some(reason.clone()));
|
||||
let _ = self.startup_phase.send(StartupPhase::Degraded);
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> StateSnapshot {
|
||||
let state = self
|
||||
.state
|
||||
|
|
@ -41,7 +143,7 @@ impl DaemonRuntime {
|
|||
let mut system = System::new_with_specifics(RefreshKind::everything());
|
||||
let mut counter = 0.0;
|
||||
loop {
|
||||
if *runtime.state.shutdown.read().await {
|
||||
if runtime.is_shutting_down() {
|
||||
break;
|
||||
}
|
||||
system.refresh_cpu_all();
|
||||
|
|
|
|||
|
|
@ -1,28 +1,42 @@
|
|||
use crate::{CommandRouter, DaemonRuntime};
|
||||
use iota_ipc::{ClientMessage, DaemonMessage, read_msg, write_msg};
|
||||
use iota_ipc::{
|
||||
ClientMessage, DaemonMessage, HelloAck, MIN_PROTOCOL_VERSION, PROTOCOL_VERSION, read_msg,
|
||||
write_msg,
|
||||
};
|
||||
use iota_logger::log;
|
||||
use std::io::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::{env, os::fd::FromRawFd};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::{broadcast, mpsc, watch};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Per-client outbound queue capacity.
|
||||
const CLIENT_CHANNEL_SIZE: usize = 256;
|
||||
|
||||
/// Maximum handshake retries before giving up.
|
||||
const MAX_HANDSHAKE_RETRIES: u32 = 10;
|
||||
|
||||
pub struct IpcServer {
|
||||
path: PathBuf,
|
||||
runtime: Arc<DaemonRuntime>,
|
||||
messages: broadcast::Sender<DaemonMessage>,
|
||||
log_tx: broadcast::Sender<DaemonMessage>,
|
||||
state_rx: watch::Sender<iota_ipc::StateSnapshot>,
|
||||
}
|
||||
|
||||
impl IpcServer {
|
||||
pub fn new(
|
||||
path: impl Into<PathBuf>,
|
||||
runtime: Arc<DaemonRuntime>,
|
||||
messages: broadcast::Sender<DaemonMessage>,
|
||||
log_tx: broadcast::Sender<DaemonMessage>,
|
||||
state_rx: watch::Sender<iota_ipc::StateSnapshot>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
runtime,
|
||||
messages,
|
||||
log_tx,
|
||||
state_rx,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,11 +52,14 @@ impl IpcServer {
|
|||
}
|
||||
};
|
||||
loop {
|
||||
let (stream, _) = listener.accept().await?;
|
||||
let (stream, _addr) = listener.accept().await?;
|
||||
let runtime = self.runtime.clone();
|
||||
let messages = self.messages.clone();
|
||||
let log_tx = self.log_tx.clone();
|
||||
let state_rx = self.state_rx.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = handle_client(stream, runtime, messages).await;
|
||||
if let Err(error) = handle_client(stream, runtime, log_tx, state_rx).await {
|
||||
eprintln!("IPC client error: {error}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -72,34 +89,159 @@ async fn remove_stale_socket(path: &Path) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PeerIdentity {
|
||||
pid: i32,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
}
|
||||
|
||||
fn peer_credentials(stream: &UnixStream) -> PeerIdentity {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::unix::io::AsRawFd;
|
||||
unsafe {
|
||||
let mut cred: libc::ucred = std::mem::zeroed();
|
||||
let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
|
||||
let fd = stream.as_raw_fd();
|
||||
libc::getsockopt(
|
||||
fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_PEERCRED,
|
||||
&mut cred as *mut _ as *mut libc::c_void,
|
||||
&mut len,
|
||||
);
|
||||
PeerIdentity {
|
||||
pid: cred.pid,
|
||||
uid: cred.uid,
|
||||
gid: cred.gid,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
PeerIdentity {
|
||||
pid: 0,
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_client(
|
||||
stream: UnixStream,
|
||||
runtime: Arc<DaemonRuntime>,
|
||||
messages: broadcast::Sender<DaemonMessage>,
|
||||
log_tx: broadcast::Sender<DaemonMessage>,
|
||||
_state_rx: watch::Sender<iota_ipc::StateSnapshot>,
|
||||
) -> Result<()> {
|
||||
let peer = peer_credentials(&stream);
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
let mut outgoing = messages.subscribe();
|
||||
let initial = DaemonMessage::StateUpdate(runtime.snapshot());
|
||||
write_msg(&mut writer, &initial).await?;
|
||||
let writer_task = tokio::spawn(async move {
|
||||
while let Ok(message) = outgoing.recv().await {
|
||||
if write_msg(&mut writer, &message).await.is_err() {
|
||||
let (directed_tx, directed_rx) = mpsc::channel::<DaemonMessage>(CLIENT_CHANNEL_SIZE);
|
||||
|
||||
// --- Handshake ---
|
||||
let mut negotiated_version: Option<u16> = None;
|
||||
for _ in 0..MAX_HANDSHAKE_RETRIES {
|
||||
match read_msg::<_, ClientMessage>(&mut reader).await {
|
||||
Ok(ClientMessage::Hello { supported_versions }) => {
|
||||
let version = supported_versions
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|v| *v >= MIN_PROTOCOL_VERSION && *v <= PROTOCOL_VERSION)
|
||||
.unwrap_or(PROTOCOL_VERSION);
|
||||
negotiated_version = Some(version);
|
||||
let instance_id = Uuid::new_v4().to_string();
|
||||
let ack = DaemonMessage::HelloAck(HelloAck {
|
||||
protocol_version: version,
|
||||
daemon_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
instance_id,
|
||||
startup_phase: runtime.current_startup_phase().into(),
|
||||
capabilities: vec!["commands".into(), "metrics".into(), "logs".into()],
|
||||
});
|
||||
write_msg(&mut writer, &ack).await?;
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
// Unexpected first message — send error and close.
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Expected Hello as first message",
|
||||
));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
});
|
||||
}
|
||||
let _version = negotiated_version.ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, "Handshake failed after retries")
|
||||
})?;
|
||||
|
||||
log!("IPC client connected (pid={}, uid={})", peer.pid, peer.uid);
|
||||
|
||||
// --- Send initial state snapshot ---
|
||||
let initial = DaemonMessage::StateUpdate(runtime.snapshot());
|
||||
let _ = directed_tx.send(initial).await;
|
||||
|
||||
// --- Writer task: merge directed responses + shared log events ---
|
||||
let mut log_rx = log_tx.subscribe();
|
||||
let directed_for_writer = directed_tx.clone();
|
||||
let writer_task = {
|
||||
let runtime = runtime.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut directed_rx = directed_rx;
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Directed messages (responses to this client's requests)
|
||||
msg = directed_rx.recv() => {
|
||||
match msg {
|
||||
Some(message) => {
|
||||
if write_msg(&mut writer, &message).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
// Shared log events
|
||||
result = log_rx.recv() => {
|
||||
match result {
|
||||
Ok(message) => {
|
||||
if write_msg(&mut writer, &message).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
let _ = directed_for_writer.send(DaemonMessage::Gap { skipped }).await;
|
||||
// Then send current snapshot for resync
|
||||
let _ = directed_for_writer.send(
|
||||
DaemonMessage::StateUpdate(runtime.snapshot())
|
||||
).await;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// --- Reader loop ---
|
||||
let router = CommandRouter::new(runtime.clone());
|
||||
loop {
|
||||
match read_msg::<_, ClientMessage>(&mut reader).await {
|
||||
Ok(ClientMessage::Command { seq, line }) => {
|
||||
let result = router.route(seq, line).await;
|
||||
let _ = messages.send(result);
|
||||
Ok(ClientMessage::Request(envelope)) => {
|
||||
let response = router.route(envelope.request_id, envelope.request).await;
|
||||
let _ = directed_tx.send(DaemonMessage::Response(response)).await;
|
||||
}
|
||||
Ok(ClientMessage::Subscribe) => {
|
||||
let _ = messages.send(DaemonMessage::StateUpdate(runtime.snapshot()));
|
||||
Ok(ClientMessage::Subscribe { .. }) => {
|
||||
let snapshot = DaemonMessage::StateUpdate(runtime.snapshot());
|
||||
let _ = directed_tx.send(snapshot).await;
|
||||
}
|
||||
Ok(ClientMessage::Ping { seq }) => {
|
||||
let _ = messages.send(DaemonMessage::Pong { seq });
|
||||
let _ = directed_tx.send(DaemonMessage::Pong { seq }).await;
|
||||
}
|
||||
Ok(ClientMessage::Hello { .. }) => {
|
||||
// Re-handshake on existing connection: treat as resubscribe
|
||||
let snapshot = DaemonMessage::StateUpdate(runtime.snapshot());
|
||||
let _ = directed_tx.send(snapshot).await;
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
||||
Err(error) => {
|
||||
|
|
@ -109,5 +251,10 @@ async fn handle_client(
|
|||
}
|
||||
}
|
||||
writer_task.abort();
|
||||
log!(
|
||||
"IPC client disconnected (pid={}, uid={})",
|
||||
peer.pid,
|
||||
peer.uid
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ pub mod ipc_server;
|
|||
pub mod log_broadcaster;
|
||||
|
||||
pub use command_router::CommandRouter;
|
||||
pub use daemon_state::DaemonRuntime;
|
||||
pub use daemon_state::{DaemonRuntime, ShutdownReason, StartupPhase};
|
||||
pub use ipc_server::IpcServer;
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ iota-storage = { path = "../iota-storage" }
|
|||
omikron-connector = { path = "../omikron-connector" }
|
||||
web-server = { path = "../web-server" }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
use iota_daemon_lib::{DaemonRuntime, IpcServer, log_broadcaster};
|
||||
use iota_daemon_lib::{DaemonRuntime, IpcServer, ShutdownReason, StartupPhase, log_broadcaster};
|
||||
use iota_logger::{self as logger, log, log_t};
|
||||
use iota_storage::users::user_manager;
|
||||
use iota_storage::util::config_util::CONFIG;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use tokio::sync::{broadcast, watch};
|
||||
fn socket_path() -> PathBuf {
|
||||
std::env::var_os("IOTA_SOCKET")
|
||||
.map(PathBuf::from)
|
||||
|
|
@ -17,42 +16,79 @@ fn socket_path() -> PathBuf {
|
|||
async fn main() {
|
||||
logger::startup();
|
||||
iota_storage::util::config_util::load_config();
|
||||
|
||||
let runtime = Arc::new(DaemonRuntime::new());
|
||||
runtime.set_startup_phase(StartupPhase::LoadingUsers);
|
||||
|
||||
if user_manager::load_users().await.is_err() {
|
||||
log_t!("user_load_failed");
|
||||
}
|
||||
let runtime = Arc::new(DaemonRuntime::new());
|
||||
|
||||
// --- IPC infrastructure ---
|
||||
let (log_tx, _) = broadcast::channel(512);
|
||||
log_broadcaster::spawn(log_tx.clone());
|
||||
let (state_tx, _state_rx) = watch::channel(iota_ipc::StateSnapshot::default());
|
||||
|
||||
// --- Start IPC server early (before services) so clients can see startup phases ---
|
||||
runtime.set_startup_phase(StartupPhase::StartingServices);
|
||||
let ipc_server = IpcServer::new(
|
||||
socket_path(),
|
||||
runtime.clone(),
|
||||
log_tx.clone(),
|
||||
state_tx.clone(),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = ipc_server.run().await {
|
||||
eprintln!("iota-daemon IPC server failed: {error}");
|
||||
}
|
||||
});
|
||||
log!("iota-daemon IPC server started");
|
||||
|
||||
// --- System monitor ---
|
||||
runtime.spawn_system_monitor();
|
||||
let (messages, _) = broadcast::channel(512);
|
||||
log_broadcaster::spawn(messages.clone());
|
||||
let state_updates = runtime.clone();
|
||||
let state_messages = messages.clone();
|
||||
|
||||
// --- State update publisher (watch-based, no full broadcast per tick) ---
|
||||
let state_publisher = runtime.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if *state_updates.state.shutdown.read().await {
|
||||
if state_publisher.is_shutting_down() {
|
||||
break;
|
||||
}
|
||||
let _ = state_messages.send(iota_ipc::DaemonMessage::StateUpdate(
|
||||
state_updates.snapshot(),
|
||||
));
|
||||
let snapshot = state_publisher.snapshot();
|
||||
let _ = state_tx.send(snapshot);
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// --- Web server ---
|
||||
let port = CONFIG.load().port;
|
||||
if !web_server::start(port).await {
|
||||
if !web_server::start(port, runtime.cancellation.clone()).await {
|
||||
log!("Failed to start the MTP web server on port {}", port);
|
||||
runtime.mark_degraded("MTP web server failed to start".into());
|
||||
}
|
||||
let _ = omikron_connector::omikron_connection::get_omikron_connection().await;
|
||||
|
||||
let server = IpcServer::new(socket_path(), runtime.clone(), messages);
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = server.run().await {
|
||||
eprintln!("iota-daemon IPC server failed: {error}");
|
||||
}
|
||||
});
|
||||
log!("iota-daemon started");
|
||||
while !*runtime.state.shutdown.read().await {
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
// --- Omikron connection ---
|
||||
let omikron_result =
|
||||
omikron_connector::omikron_connection::get_omikron_connection(runtime.cancellation.clone())
|
||||
.await;
|
||||
if omikron_result.is_none() {
|
||||
runtime.mark_degraded("Omikron connection unavailable".into());
|
||||
}
|
||||
log!("iota-daemon stopping");
|
||||
|
||||
runtime.set_startup_phase(StartupPhase::Ready);
|
||||
log!("iota-daemon started (phase: Ready)");
|
||||
|
||||
// --- Main lifecycle loop ---
|
||||
runtime.cancellation.cancelled().await;
|
||||
|
||||
let reason = runtime.shutdown_reason().unwrap_or(ShutdownReason::Stop);
|
||||
log!("iota-daemon shutting down (reason: {:?})", reason);
|
||||
runtime.set_startup_phase(StartupPhase::Stopping);
|
||||
|
||||
// Wait a moment for in-flight operations to complete
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
let exit_code = reason.exit_code();
|
||||
log!("iota-daemon exited (code: {})", exit_code);
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
pub mod protocol;
|
||||
pub mod transport;
|
||||
|
||||
pub use protocol::{ClientMessage, DaemonMessage, LogEntry, StateSnapshot};
|
||||
pub use protocol::{
|
||||
ClientMessage, DaemonMessage, HelloAck, LogEntry, StateSnapshot, MetricSample,
|
||||
RequestEnvelope, ResponseEnvelope, ResponseResult, LocalRequest, IpcErrorCode,
|
||||
ConnectionStatus, StartupPhase, LifecycleEvent,
|
||||
};
|
||||
pub use transport::{read_msg, write_msg};
|
||||
|
||||
/// Current IPC protocol version.
|
||||
pub const PROTOCOL_VERSION: u16 = 2;
|
||||
/// Minimum protocol version this daemon understands.
|
||||
pub const MIN_PROTOCOL_VERSION: u16 = 2;
|
||||
|
|
|
|||
|
|
@ -1,28 +1,123 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Client → Daemon
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
||||
pub enum ClientMessage {
|
||||
Command { seq: u64, line: String },
|
||||
Subscribe,
|
||||
Hello { supported_versions: Vec<u16> },
|
||||
Subscribe { log_classes: Vec<String>, metric_interval_ms: Option<u64> },
|
||||
Request(RequestEnvelope),
|
||||
Ping { seq: u64 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct RequestEnvelope {
|
||||
pub request_id: u64,
|
||||
pub protocol_version: u16,
|
||||
pub request: LocalRequest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
||||
pub enum LocalRequest {
|
||||
GetStatus,
|
||||
ListTasks,
|
||||
ListUsers,
|
||||
CreateUser { username: String },
|
||||
RemoveUser { user_id: i64 },
|
||||
ReconnectOmikron,
|
||||
RotateIotaIdentity,
|
||||
RestartDaemon,
|
||||
StopDaemon,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Daemon → Client
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
||||
pub enum DaemonMessage {
|
||||
HelloAck(HelloAck),
|
||||
LogEntry(LogEntry),
|
||||
StateUpdate(StateSnapshot),
|
||||
CommandResult {
|
||||
seq: u64,
|
||||
success: bool,
|
||||
message: String,
|
||||
},
|
||||
Pong {
|
||||
seq: u64,
|
||||
},
|
||||
MetricSample(MetricSample),
|
||||
Response(ResponseEnvelope),
|
||||
Pong { seq: u64 },
|
||||
LifecycleEvent(LifecycleEvent),
|
||||
Gap { skipped: u64 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct HelloAck {
|
||||
pub protocol_version: u16,
|
||||
pub daemon_version: String,
|
||||
pub instance_id: String,
|
||||
pub startup_phase: StartupPhase,
|
||||
pub capabilities: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ResponseEnvelope {
|
||||
pub request_id: u64,
|
||||
pub result: ResponseResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
||||
pub enum ResponseResult {
|
||||
Ok(String),
|
||||
Error(IpcErrorCode),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IpcErrorCode {
|
||||
InvalidRequest,
|
||||
NotFound,
|
||||
Conflict,
|
||||
StorageFailure,
|
||||
OmikronUnavailable,
|
||||
UnsupportedVersion,
|
||||
NotReady,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
||||
pub enum LifecycleEvent {
|
||||
StateChanged(ConnectionStatus),
|
||||
Shutdown { reason: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ConnectionStatus {
|
||||
Connected,
|
||||
Reconnecting,
|
||||
Degraded,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StartupPhase {
|
||||
Starting,
|
||||
MigratingStorage,
|
||||
LoadingUsers,
|
||||
StartingServices,
|
||||
Ready,
|
||||
Degraded,
|
||||
Stopping,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp_ms: u128,
|
||||
|
|
@ -40,3 +135,12 @@ pub struct StateSnapshot {
|
|||
pub net_down: Vec<(f64, f64)>,
|
||||
pub sys_info: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct MetricSample {
|
||||
pub cpu: Option<f64>,
|
||||
pub ram: Option<f64>,
|
||||
pub ping: Option<f64>,
|
||||
pub net_up: Option<f64>,
|
||||
pub net_down: Option<f64>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,19 +41,20 @@ where
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{read_msg, write_msg};
|
||||
use crate::protocol::ClientMessage;
|
||||
use crate::protocol::{ClientMessage, LocalRequest, RequestEnvelope};
|
||||
|
||||
#[tokio::test]
|
||||
async fn round_trips_framed_messages() {
|
||||
let (mut writer, mut reader) = tokio::io::duplex(1024);
|
||||
let message = ClientMessage::Command {
|
||||
seq: 4,
|
||||
line: "help".into(),
|
||||
};
|
||||
let message = ClientMessage::Request(RequestEnvelope {
|
||||
request_id: 4,
|
||||
protocol_version: 2,
|
||||
request: LocalRequest::GetStatus,
|
||||
});
|
||||
write_msg(&mut writer, &message)
|
||||
.await
|
||||
.expect("write succeeds");
|
||||
let received: ClientMessage = read_msg(&mut reader).await.expect("read succeeds");
|
||||
assert!(matches!(received, ClientMessage::Command { seq: 4, line } if line == "help"));
|
||||
assert!(matches!(received, ClientMessage::Request(req) if req.request_id == 4));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
iota.mk
BIN
iota.mk
Binary file not shown.
|
|
@ -6,3 +6,4 @@ edition = "2024"
|
|||
[dependencies]
|
||||
iota-cli = { path = "../iota-cli" }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
|
|
|
|||
|
|
@ -10,16 +10,18 @@ fn socket_path() -> PathBuf {
|
|||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
let path = socket_path();
|
||||
let ipc = match IpcClient::connect(&path).await {
|
||||
let ipc = match IpcClient::connect_or_activate(&path).await {
|
||||
Ok(client) => client,
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"Cannot connect to iota-daemon at {}: {error}",
|
||||
path.display()
|
||||
);
|
||||
eprintln!("Ensure iota-daemon.socket is enabled or iota-daemon is running.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
ipc.spawn_reconnector();
|
||||
let ui = start_tui(ipc);
|
||||
ui.set_screen(Box::new(MainScreen::new(ui.clone()).await))
|
||||
.await;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ dashmap = "6.2.1"
|
|||
json = "*"
|
||||
reqwest = "0.13.2"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
uuid = { version = "*", features = ["v4"] }
|
||||
base64 = "0.22.1"
|
||||
hex = "*"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use dashmap::DashMap;
|
||||
use iota_logger::{log, log_cv_in, log_cv_out, log_t};
|
||||
use iota_state::{ACTIVE_TASKS, SHUTDOWN};
|
||||
use iota_state::ACTIVE_TASKS;
|
||||
use iota_storage::util::chat_files::{self, MessageState, change_message_state};
|
||||
use iota_storage::util::config_util::{CONFIG, modify_config};
|
||||
use iota_storage::util::e2ee_storage::{self, PendingChatSecretForward, StoredChatSecret};
|
||||
|
|
@ -16,6 +16,7 @@ use std::time::{Duration, Instant};
|
|||
use tokio::sync::{Mutex, RwLock, Semaphore, oneshot, watch};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::sleep;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::omega_discovery;
|
||||
|
|
@ -161,10 +162,15 @@ pub struct OmikronConnection {
|
|||
pub app_sessions: Arc<DashMap<u64, (i64, String)>>,
|
||||
pub(crate) missed_pongs: Arc<AtomicU32>,
|
||||
handler_semaphore: Arc<Semaphore>,
|
||||
cancellation: CancellationToken,
|
||||
}
|
||||
|
||||
impl OmikronConnection {
|
||||
pub fn new() -> Self {
|
||||
Self::with_cancellation(CancellationToken::new())
|
||||
}
|
||||
|
||||
pub fn with_cancellation(cancellation: CancellationToken) -> Self {
|
||||
let (shutdown_tx, _) = watch::channel(false);
|
||||
let (state_watch_tx, _) = watch::channel(ConnectionState::Disconnected);
|
||||
|
||||
|
|
@ -183,6 +189,7 @@ impl OmikronConnection {
|
|||
app_sessions: Arc::new(DashMap::new()),
|
||||
missed_pongs: Arc::new(AtomicU32::new(0)),
|
||||
handler_semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_HANDLERS)),
|
||||
cancellation,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +244,7 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
if let Some(sender) = self.sender.read().await.as_ref() {
|
||||
sender.close();
|
||||
sender.close().await;
|
||||
}
|
||||
|
||||
self.set_state(ConnectionState::Disconnected).await;
|
||||
|
|
@ -250,7 +257,7 @@ impl OmikronConnection {
|
|||
let mut shutdown_rx = shutdown_rx;
|
||||
|
||||
loop {
|
||||
if *shutdown_rx.borrow() || *SHUTDOWN.read().await {
|
||||
if *shutdown_rx.borrow() || self.cancellation.is_cancelled() {
|
||||
log_t!("omikron_connection_loop_shutdown");
|
||||
break;
|
||||
}
|
||||
|
|
@ -621,7 +628,7 @@ impl OmikronConnection {
|
|||
self.missed_pongs.load(Ordering::Relaxed)
|
||||
);
|
||||
if let Some(sender) = self.sender.read().await.as_ref() {
|
||||
sender.close();
|
||||
sender.close().await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -1750,7 +1757,7 @@ impl OmikronConnection {
|
|||
if !sender.is_open() {
|
||||
drop(sender_guard);
|
||||
if let Some(sender) = self.sender.write().await.take() {
|
||||
sender.close();
|
||||
sender.close().await;
|
||||
}
|
||||
self.fail_all_waiting_tasks(format!(
|
||||
"Send failed: connection closed (connection_id={})",
|
||||
|
|
@ -1933,11 +1940,12 @@ pub static OMIKRON_CONNECTION: LazyLock<Arc<OmikronConnection>> = LazyLock::new(
|
|||
conn
|
||||
});
|
||||
|
||||
pub async fn get_omikron_connection() -> Arc<OmikronConnection> {
|
||||
let conn = OMIKRON_CONNECTION.clone();
|
||||
|
||||
pub async fn get_omikron_connection(
|
||||
cancellation: CancellationToken,
|
||||
) -> Option<Arc<OmikronConnection>> {
|
||||
let conn = Arc::new(OmikronConnection::with_cancellation(cancellation));
|
||||
conn.connect().await;
|
||||
conn
|
||||
Some(conn)
|
||||
}
|
||||
|
||||
impl iota_connection::connection_handler::ConnectionHandler for OmikronConnection {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use iota_logger::{PrintType, log, log_cv, log_t};
|
||||
use iota_state::{RELOAD, SHUTDOWN};
|
||||
use iota_storage::users::user_manager::{add_user, save_users};
|
||||
use iota_storage::users::user_profile::UserProfile;
|
||||
use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64};
|
||||
|
|
@ -81,8 +80,6 @@ pub async fn create_user(username: &str) -> (Option<UserProfile>, Option<String>
|
|||
log_t!("User creation: Response returned none");
|
||||
return (None, None);
|
||||
}
|
||||
*SHUTDOWN.write().await = true;
|
||||
*RELOAD.write().await = true;
|
||||
log!("Created User");
|
||||
save_file(
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -5,12 +5,22 @@ Wants=network-online.target
|
|||
Requires=iota-daemon.socket
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/iota-daemon
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
RuntimeDirectory=iota
|
||||
RuntimeDirectoryMode=0750
|
||||
Environment=IOTA_SOCKET=/run/iota/iota.sock
|
||||
|
||||
# Exit code 75 = restart requested (daemon-specific convention)
|
||||
RestartPreventExitStatus=0
|
||||
RestartForceExitStatus=75
|
||||
|
||||
# Graceful shutdown
|
||||
TimeoutStopSec=10
|
||||
KillMode=mixed
|
||||
KillSignal=SIGTERM
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ ListenStream=/run/iota/iota.sock
|
|||
SocketMode=0660
|
||||
SocketUser=iota
|
||||
SocketGroup=iota
|
||||
Backlog=5
|
||||
RemoveOnStop=true
|
||||
|
||||
[Install]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ edition = "2024"
|
|||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-server"] }
|
||||
bytes = "1"
|
||||
http = "1"
|
||||
iota-state = { path = "../iota-state" }
|
||||
iota-util = { path = "../iota-util" }
|
||||
iota-logger = { path = "../iota-logger" }
|
||||
iota-util = { path = "../iota-util" }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
use bytes::Bytes;
|
||||
use iota_logger::log;
|
||||
use iota_state::{ACTIVE_TASKS, SHUTDOWN};
|
||||
use iota_util::file_util::load_file_vec;
|
||||
use mtp::host::HostConfig;
|
||||
use mtp::webserver::{Http3Request, Http3Response, MTPWebServer, WebServerConfig};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
const CERT_PATH: &str = "certs/cert.pem";
|
||||
const KEY_PATH: &str = "certs/cert.key";
|
||||
|
|
@ -63,7 +62,7 @@ fn content_type(name: &str) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn start(port: u16) -> bool {
|
||||
pub async fn start(port: u16, cancellation: CancellationToken) -> bool {
|
||||
let certificate = match tokio::fs::read(CERT_PATH).await {
|
||||
Ok(certificate) => certificate,
|
||||
Err(error) => {
|
||||
|
|
@ -102,7 +101,6 @@ pub async fn start(port: u16) -> bool {
|
|||
|
||||
log!("MTP web server running on port {}", port);
|
||||
tokio::spawn(async move {
|
||||
ACTIVE_TASKS.insert("WebServer".into());
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = server.accept() => {
|
||||
|
|
@ -112,22 +110,12 @@ pub async fn start(port: u16) -> bool {
|
|||
Err(error) => log!("MTP webserver connection failed: {}", error),
|
||||
}
|
||||
}
|
||||
_ = wait_for_shutdown() => {
|
||||
_ = cancellation.cancelled() => {
|
||||
server.shutdown().await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ACTIVE_TASKS.remove("WebServer");
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
async fn wait_for_shutdown() {
|
||||
loop {
|
||||
if *SHUTDOWN.read().await {
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue