[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')"
|
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"
|
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: |
|
run: |
|
||||||
nix-shell -p docker --run "docker build -f dockerfile -t tensamin/iota:latest . && docker push tensamin/iota:latest"
|
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: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
nix build .#iota --print-build-logs
|
nix build .#iota-daemon --print-build-logs
|
||||||
install -Dm755 result/bin/iota dist/iota
|
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
|
- name: Read release metadata
|
||||||
id: version
|
id: version
|
||||||
|
|
@ -55,7 +58,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
VERSION="$(nix eval --raw .#iota.version)"
|
VERSION="$(nix eval --raw .#iota-daemon.version)"
|
||||||
SHORT_SHA="$(git rev-parse --short=7 HEAD)"
|
SHORT_SHA="$(git rev-parse --short=7 HEAD)"
|
||||||
|
|
||||||
case "$RELEASE_TYPE" in
|
case "$RELEASE_TYPE" in
|
||||||
|
|
@ -73,18 +76,15 @@ jobs:
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
ASSET_PATH="dist/iota"
|
|
||||||
ASSET_NAME="iota"
|
|
||||||
test -x "$ASSET_PATH"
|
|
||||||
|
|
||||||
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
|
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
|
||||||
echo "tag=$TAG" >> "$FORGEJO_OUTPUT"
|
echo "tag=$TAG" >> "$FORGEJO_OUTPUT"
|
||||||
echo "title=$TAG" >> "$FORGEJO_OUTPUT"
|
echo "title=$TAG" >> "$FORGEJO_OUTPUT"
|
||||||
echo "prerelease=$PRERELEASE" >> "$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:
|
env:
|
||||||
TOKEN: ${{ forgejo.token }}
|
TOKEN: ${{ forgejo.token }}
|
||||||
API: ${{ forgejo.api_url }}
|
API: ${{ forgejo.api_url }}
|
||||||
|
|
@ -93,8 +93,6 @@ jobs:
|
||||||
TAG: ${{ steps.version.outputs.tag }}
|
TAG: ${{ steps.version.outputs.tag }}
|
||||||
TITLE: ${{ steps.version.outputs.title }}
|
TITLE: ${{ steps.version.outputs.title }}
|
||||||
PRERELEASE: ${{ steps.version.outputs.prerelease }}
|
PRERELEASE: ${{ steps.version.outputs.prerelease }}
|
||||||
ASSET_PATH: ${{ steps.version.outputs.asset_path }}
|
|
||||||
ASSET_NAME: ${{ steps.version.outputs.asset_name }}
|
|
||||||
DESCRIPTION: ${{ inputs.description }}
|
DESCRIPTION: ${{ inputs.description }}
|
||||||
run: |
|
run: |
|
||||||
nix-shell -p curl jq --run '
|
nix-shell -p curl jq --run '
|
||||||
|
|
@ -122,7 +120,11 @@ jobs:
|
||||||
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
|
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
|
||||||
fi
|
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" \
|
-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
|
logs
|
||||||
agreements
|
agreements
|
||||||
languages/
|
languages/
|
||||||
|
.envrc
|
||||||
|
.direnv
|
||||||
|
config.json
|
||||||
|
*.mk
|
||||||
|
*.sqlite*
|
||||||
|
|
|
||||||
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -2091,6 +2091,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iota-cli",
|
"iota-cli",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2186,6 +2187,7 @@ dependencies = [
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
|
"tokio-util",
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"warp",
|
"warp",
|
||||||
|
|
@ -2213,6 +2215,7 @@ dependencies = [
|
||||||
"iota-storage",
|
"iota-storage",
|
||||||
"omikron-connector",
|
"omikron-connector",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"web-server",
|
"web-server",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2220,15 +2223,19 @@ dependencies = [
|
||||||
name = "iota-daemon-lib"
|
name = "iota-daemon-lib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dashmap",
|
||||||
"iota-ipc",
|
"iota-ipc",
|
||||||
"iota-logger",
|
"iota-logger",
|
||||||
"iota-state",
|
"iota-state",
|
||||||
"iota-storage",
|
"iota-storage",
|
||||||
"iota-util",
|
"iota-util",
|
||||||
|
"libc",
|
||||||
"mtp",
|
"mtp",
|
||||||
"omikron-connector",
|
"omikron-connector",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3082,6 +3089,7 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"uuid",
|
"uuid",
|
||||||
"x448",
|
"x448",
|
||||||
]
|
]
|
||||||
|
|
@ -4898,6 +4906,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
"futures-util",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
@ -5263,10 +5272,10 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http 1.4.2",
|
"http 1.4.2",
|
||||||
"iota-logger",
|
"iota-logger",
|
||||||
"iota-state",
|
|
||||||
"iota-util",
|
"iota-util",
|
||||||
"mtp",
|
"mtp",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@ FROM rust:latest AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN cargo build --release
|
RUN cargo build --release -p iota-daemon
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
FROM debian:sid
|
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/*
|
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
|
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 {
|
rustToolchain = rustPkgs.rust-bin.stable.latest.default.override {
|
||||||
extensions = ["rust-src" "rust-analyzer" "clippy" "rustfmt"];
|
extensions = ["rust-src" "rust-analyzer" "clippy" "rustfmt"];
|
||||||
};
|
};
|
||||||
|
commonBuildInputs = with pkgs; [openssl sqlite];
|
||||||
|
commonNativeBuildInputs = with pkgs; [cmake perl pkg-config];
|
||||||
in {
|
in {
|
||||||
packages = {
|
packages = {
|
||||||
default = self'.packages.iota;
|
default = self'.packages.iota-daemon;
|
||||||
iota = pkgs.rustPlatform.buildRustPackage {
|
|
||||||
|
iota-daemon = pkgs.rustPlatform.buildRustPackage {
|
||||||
pname = "iota-daemon";
|
pname = "iota-daemon";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
@ -50,8 +53,8 @@
|
||||||
lockFile = ./Cargo.lock;
|
lockFile = ./Cargo.lock;
|
||||||
allowBuiltinFetchGit = true;
|
allowBuiltinFetchGit = true;
|
||||||
};
|
};
|
||||||
nativeBuildInputs = with pkgs; [cmake perl pkg-config];
|
nativeBuildInputs = commonNativeBuildInputs;
|
||||||
buildInputs = with pkgs; [openssl sqlite];
|
buildInputs = commonBuildInputs;
|
||||||
dontUseCmakeConfigure = true;
|
dontUseCmakeConfigure = true;
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
for f in $out/bin/*; do
|
for f in $out/bin/*; do
|
||||||
|
|
@ -62,11 +65,36 @@
|
||||||
'';
|
'';
|
||||||
passthru.dataDir = "/var/lib/iota";
|
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 {
|
devShells.default = pkgs.mkShell {
|
||||||
nativeBuildInputs = with pkgs; [rustToolchain git cmake perl pkg-config];
|
nativeBuildInputs = with pkgs; [rustToolchain git cmake perl pkg-config];
|
||||||
buildInputs = with pkgs; [openssl sqlite];
|
buildInputs = commonBuildInputs;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -78,7 +106,7 @@
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
cfg = config.services.iota;
|
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 =
|
configFile =
|
||||||
if cfg.settingsFile != null
|
if cfg.settingsFile != null
|
||||||
|
|
@ -158,14 +186,27 @@
|
||||||
|
|
||||||
users.groups.iota = {};
|
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;
|
description = descriptionText;
|
||||||
wantedBy = ["multi-user.target"];
|
|
||||||
after = ["network.target"];
|
after = ["network.target"];
|
||||||
|
requires = ["iota-daemon.socket"];
|
||||||
|
|
||||||
serviceConfig =
|
serviceConfig =
|
||||||
{
|
{
|
||||||
Type = "simple";
|
Type = "notify";
|
||||||
User = "iota";
|
User = "iota";
|
||||||
Group = "iota";
|
Group = "iota";
|
||||||
WorkingDirectory = cfg.dataDir;
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
|
@ -186,11 +227,19 @@
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
|
|
||||||
Restart = "always";
|
Restart = "on-failure";
|
||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
RuntimeDirectory = "iota";
|
RuntimeDirectory = "iota";
|
||||||
RuntimeDirectoryMode = "0750";
|
RuntimeDirectoryMode = "0750";
|
||||||
|
|
||||||
|
# Exit code 75 = restart requested
|
||||||
|
RestartPreventExitStatus = "0";
|
||||||
|
RestartForceExitStatus = "75";
|
||||||
|
|
||||||
|
TimeoutStopSec = "10";
|
||||||
|
KillMode = "mixed";
|
||||||
|
KillSignal = "SIGTERM";
|
||||||
|
|
||||||
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
|
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
|
||||||
CapabilityBoundingSet = ["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-state = { path = "../iota-state" }
|
||||||
iota-storage = { path = "../iota-storage", optional = true }
|
iota-storage = { path = "../iota-storage", optional = true }
|
||||||
iota-terms = { path = "../iota-terms" }
|
iota-terms = { path = "../iota-terms" }
|
||||||
iota-util = { path = "../iota-util", optional = true }
|
iota-util = { path = "../iota-util", optional = true }
|
||||||
iota-ipc = { path = "../iota-ipc" }
|
iota-ipc = { path = "../iota-ipc" }
|
||||||
omikron-connector = { path = "../omikron-connector", optional = true }
|
omikron-connector = { path = "../omikron-connector", optional = true }
|
||||||
|
|
||||||
|
|
@ -64,6 +64,7 @@ strum = "0.27.2"
|
||||||
strum_macros = "0.27.2"
|
strum_macros = "0.27.2"
|
||||||
sysinfo = "0.38.3"
|
sysinfo = "0.38.3"
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["rt"] }
|
||||||
tokio-tungstenite = { version = "*", features = ["native-tls"] }
|
tokio-tungstenite = { version = "*", features = ["native-tls"] }
|
||||||
tungstenite = "*"
|
tungstenite = "*"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,4 @@
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
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::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
|
@ -47,7 +33,7 @@ pub struct ConsoleCard {
|
||||||
|
|
||||||
cursor: Arc<Mutex<bool>>,
|
cursor: Arc<Mutex<bool>>,
|
||||||
last_swap: Arc<Mutex<Instant>>,
|
last_swap: Arc<Mutex<Instant>>,
|
||||||
tab_index: usize,
|
pending_restore: Arc<Mutex<Option<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConsoleCard {
|
impl ConsoleCard {
|
||||||
|
|
@ -62,7 +48,7 @@ impl ConsoleCard {
|
||||||
joins: Borders::NONE,
|
joins: Borders::NONE,
|
||||||
cursor: Arc::new(Mutex::new(true)),
|
cursor: Arc::new(Mutex::new(true)),
|
||||||
last_swap: Arc::new(Mutex::new(Instant::now())),
|
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(" ", Style::default().fg(Color::White)));
|
||||||
}
|
}
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
"send command (<help> for info)",
|
"send command (/help for info)",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(Color::DarkGray),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
" send command (<help> for info)",
|
" send command (/help for info)",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(Color::DarkGray),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -295,6 +281,12 @@ impl InteractableElement for ConsoleCard {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interact(&mut self, key: KeyEvent) -> InteractionResult {
|
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 {
|
match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if self.content.is_empty() {
|
if self.content.is_empty() {
|
||||||
|
|
@ -303,17 +295,15 @@ impl InteractableElement for ConsoleCard {
|
||||||
|
|
||||||
let command = self.content.clone();
|
let command = self.content.clone();
|
||||||
let ipc = self.ipc.clone();
|
let ipc = self.ipc.clone();
|
||||||
let seq = std::time::SystemTime::now()
|
let restore = self.pending_restore.clone();
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_millis() as u64;
|
|
||||||
tokio::spawn(async move {
|
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.content.clear();
|
||||||
self.cursor_position = 0;
|
self.cursor_position = 0;
|
||||||
self.tab_index = 0;
|
|
||||||
InteractionResult::Handled
|
InteractionResult::Handled
|
||||||
}
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
|
|
@ -350,14 +340,7 @@ impl InteractableElement for ConsoleCard {
|
||||||
self.cursor_position = self.content.chars().count();
|
self.cursor_position = self.content.chars().count();
|
||||||
InteractionResult::Handled
|
InteractionResult::Handled
|
||||||
}
|
}
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab | KeyCode::BackTab => InteractionResult::Unhandled,
|
||||||
if let Some(prefix) = self.current_prefix() {
|
|
||||||
if prefix == "/" {
|
|
||||||
self.tab_index = self.tab_index.saturating_add(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InteractionResult::Handled
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(c) = key.code.as_char() {
|
if let Some(c) = key.code.as_char() {
|
||||||
self.insert_at_cursor(c);
|
self.insert_at_cursor(c);
|
||||||
|
|
@ -381,144 +364,3 @@ impl InteractableElement for ConsoleCard {
|
||||||
self.focused = f;
|
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 iota_state::{ClientState, UiLogEntry};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::Result;
|
use std::io::Result;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
use tokio::net::unix::OwnedWriteHalf;
|
use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf};
|
||||||
use tokio::sync::Mutex;
|
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
|
/* The TUI owns this cache. IPC updates replace daemon snapshots and append
|
||||||
* logs, so rendering never reaches into daemon-owned storage or connections. */
|
* logs, so rendering never reaches into daemon-owned storage or connections. */
|
||||||
pub struct IpcClient {
|
pub struct IpcClient {
|
||||||
state: ClientState,
|
state: ClientState,
|
||||||
writer: Mutex<OwnedWriteHalf>,
|
writer: Mutex<OwnedWriteHalf>,
|
||||||
|
next_request_id: AtomicU64,
|
||||||
|
pending: Mutex<HashMap<u64, PendingRequest>>,
|
||||||
|
connection_state: watch::Sender<IpcConnectionState>,
|
||||||
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IpcClient {
|
impl IpcClient {
|
||||||
pub async fn connect(path: impl AsRef<Path>) -> Result<Arc<Self>> {
|
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 (mut reader, writer) = stream.into_split();
|
||||||
|
|
||||||
|
let (conn_state_tx, _) = watch::channel(IpcConnectionState::Connecting);
|
||||||
let client = Arc::new(Self {
|
let client = Arc::new(Self {
|
||||||
state: ClientState::new(),
|
state: ClientState::new(),
|
||||||
writer: Mutex::new(writer),
|
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();
|
let reader_client = client.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Ok(message) = read_msg::<_, DaemonMessage>(&mut reader).await {
|
reader_client.read_loop(reader).await;
|
||||||
reader_client.apply(message).await;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
client.send(ClientMessage::Subscribe).await?;
|
|
||||||
|
// Subscribe to events
|
||||||
|
client
|
||||||
|
.send(ClientMessage::Subscribe {
|
||||||
|
log_classes: vec![],
|
||||||
|
metric_interval_ms: Some(500),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(client)
|
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 {
|
pub fn state(&self) -> ClientState {
|
||||||
self.state.clone()
|
self.state.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_command(&self, seq: u64, line: String) -> Result<()> {
|
pub fn connection_status(&self) -> watch::Receiver<IpcConnectionState> {
|
||||||
self.send(ClientMessage::Command { seq, line }).await
|
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<()> {
|
async fn send(&self, message: ClientMessage) -> Result<()> {
|
||||||
|
|
@ -46,19 +475,26 @@ impl IpcClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn apply(&self, message: DaemonMessage) {
|
async fn apply(&self, message: DaemonMessage) {
|
||||||
let mut state = self
|
|
||||||
.state
|
|
||||||
.app
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(|error| error.into_inner());
|
|
||||||
match message {
|
match message {
|
||||||
DaemonMessage::LogEntry(entry) => state.push_log(UiLogEntry {
|
DaemonMessage::LogEntry(entry) => {
|
||||||
timestamp_ms: entry.timestamp_ms,
|
let mut state = self
|
||||||
sender: entry.sender,
|
.state
|
||||||
message: entry.message,
|
.app
|
||||||
is_error: entry.is_error,
|
.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) => {
|
DaemonMessage::StateUpdate(snapshot) => {
|
||||||
|
let mut state = self
|
||||||
|
.state
|
||||||
|
.app
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
state.cpu = snapshot.cpu;
|
state.cpu = snapshot.cpu;
|
||||||
state.ram = snapshot.ram;
|
state.ram = snapshot.ram;
|
||||||
state.ping = snapshot.ping;
|
state.ping = snapshot.ping;
|
||||||
|
|
@ -66,18 +502,106 @@ impl IpcClient {
|
||||||
state.net_down = snapshot.net_down;
|
state.net_down = snapshot.net_down;
|
||||||
state.sys_info = snapshot.sys_info;
|
state.sys_info = snapshot.sys_info;
|
||||||
}
|
}
|
||||||
DaemonMessage::CommandResult {
|
DaemonMessage::MetricSample(sample) => {
|
||||||
success, message, ..
|
let mut state = self
|
||||||
} => state.push_log(UiLogEntry {
|
.state
|
||||||
timestamp_ms: std::time::SystemTime::now()
|
.app
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.lock()
|
||||||
.unwrap_or_default()
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
.as_millis(),
|
if let Some(cpu) = sample.cpu {
|
||||||
sender: "Command".into(),
|
let idx = state.cpu.len() as f64;
|
||||||
message,
|
state.cpu.push((idx, cpu));
|
||||||
is_error: !success,
|
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::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,
|
log_card::LogCard,
|
||||||
},
|
},
|
||||||
interaction_result::InteractionResult,
|
interaction_result::InteractionResult,
|
||||||
|
ipc_client::IpcConnectionState,
|
||||||
screens::screens::{NavDirection, Screen},
|
screens::screens::{NavDirection, Screen},
|
||||||
ui::UI,
|
ui::UI,
|
||||||
};
|
};
|
||||||
|
|
@ -16,6 +17,7 @@ use ratatui::{
|
||||||
layout::{Constraint, Layout, Margin, Rect},
|
layout::{Constraint, Layout, Margin, Rect},
|
||||||
widgets::{Block, Borders},
|
widgets::{Block, Borders},
|
||||||
};
|
};
|
||||||
|
use tokio::sync::watch;
|
||||||
|
|
||||||
use std::{any::Any, sync::Arc};
|
use std::{any::Any, sync::Arc};
|
||||||
|
|
||||||
|
|
@ -24,6 +26,7 @@ pub struct MainScreen {
|
||||||
nav_grid: Vec<Vec<Option<usize>>>,
|
nav_grid: Vec<Vec<Option<usize>>>,
|
||||||
selected_coords: (usize, usize),
|
selected_coords: (usize, usize),
|
||||||
graphs_open: bool,
|
graphs_open: bool,
|
||||||
|
connection_status_rx: watch::Receiver<IpcConnectionState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainScreen {
|
impl MainScreen {
|
||||||
|
|
@ -58,11 +61,14 @@ impl MainScreen {
|
||||||
|
|
||||||
let graphs_open = true;
|
let graphs_open = true;
|
||||||
|
|
||||||
|
let connection_status_rx = ui.ipc().connection_status();
|
||||||
|
|
||||||
let mut screen = MainScreen {
|
let mut screen = MainScreen {
|
||||||
elements,
|
elements,
|
||||||
nav_grid,
|
nav_grid,
|
||||||
selected_coords: (1, 0),
|
selected_coords: (1, 0),
|
||||||
graphs_open,
|
graphs_open,
|
||||||
|
connection_status_rx,
|
||||||
};
|
};
|
||||||
screen.focus_current();
|
screen.focus_current();
|
||||||
screen
|
screen
|
||||||
|
|
@ -147,6 +153,40 @@ impl MainScreen {
|
||||||
|
|
||||||
self.focus_current();
|
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 {
|
impl Screen for MainScreen {
|
||||||
|
|
@ -159,7 +199,21 @@ impl Screen for MainScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&self, f: &mut Frame, rect: Rect) {
|
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);
|
f.render_widget(main_block, rect);
|
||||||
|
|
||||||
let inner = rect.inner(Margin {
|
let inner = rect.inner(Margin {
|
||||||
|
|
@ -215,10 +269,14 @@ impl Screen for MainScreen {
|
||||||
|
|
||||||
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
|
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Up => self.navigate(NavDirection::Up),
|
KeyCode::Tab => {
|
||||||
KeyCode::Down => self.navigate(NavDirection::Down),
|
self.navigate_focus(true);
|
||||||
KeyCode::Left => self.navigate(NavDirection::Left),
|
return InteractionResult::Handled;
|
||||||
KeyCode::Right => self.navigate(NavDirection::Right),
|
}
|
||||||
|
KeyCode::BackTab => {
|
||||||
|
self.navigate_focus(false);
|
||||||
|
return InteractionResult::Handled;
|
||||||
|
}
|
||||||
KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => {
|
KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => {
|
||||||
self.graphs_open = !self.graphs_open;
|
self.graphs_open = !self.graphs_open;
|
||||||
for element in self.elements.iter_mut() {
|
for element in self.elements.iter_mut() {
|
||||||
|
|
@ -232,7 +290,17 @@ impl Screen for MainScreen {
|
||||||
let (y, x) = self.selected_coords;
|
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(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) {
|
||||||
if let Some(el) = self.elements.get_mut(*index) {
|
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"
|
ratatui = "0.30.0"
|
||||||
reqwest = "0.13.2"
|
reqwest = "0.13.2"
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
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 {
|
if !web_server::start(port).await {
|
||||||
log!("Failed to start the MTP web server on port {}", port);
|
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");
|
log_t!("setup_completed");
|
||||||
loop {
|
loop {
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,9 @@ iota-storage = { path = "../iota-storage" }
|
||||||
iota-util = { path = "../iota-util" }
|
iota-util = { path = "../iota-util" }
|
||||||
omikron-connector = { path = "../omikron-connector" }
|
omikron-connector = { path = "../omikron-connector" }
|
||||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
||||||
|
dashmap = "6.1.0"
|
||||||
|
libc = "0.2"
|
||||||
sysinfo = "0.38.3"
|
sysinfo = "0.38.3"
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
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 crate::DaemonRuntime;
|
||||||
use iota_ipc::DaemonMessage;
|
use iota_ipc::{
|
||||||
|
IpcErrorCode, LocalRequest, ResponseEnvelope, ResponseResult,
|
||||||
|
};
|
||||||
use iota_logger::{log, log_command};
|
use iota_logger::{log, log_command};
|
||||||
use iota_storage::users::user_manager;
|
use iota_storage::users::user_manager;
|
||||||
use iota_storage::util::config_util::modify_config;
|
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::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::daemon_state::ShutdownReason;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CommandRouter {
|
pub struct CommandRouter {
|
||||||
runtime: Arc<DaemonRuntime>,
|
runtime: Arc<DaemonRuntime>,
|
||||||
|
|
@ -18,85 +22,124 @@ impl CommandRouter {
|
||||||
Self { runtime }
|
Self { runtime }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn route(&self, seq: u64, line: String) -> DaemonMessage {
|
pub async fn route(&self, request_id: u64, request: LocalRequest) -> ResponseEnvelope {
|
||||||
log_command!("{}", line);
|
log_command!("{:?}", request);
|
||||||
let result = self.execute(&line).await;
|
let result = self.execute(request).await;
|
||||||
DaemonMessage::CommandResult {
|
ResponseEnvelope {
|
||||||
seq,
|
request_id,
|
||||||
success: result.is_ok(),
|
result,
|
||||||
message: result.unwrap_or_else(|error| error),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, line: &str) -> Result<String, String> {
|
/// Parse a legacy console command string into a typed request.
|
||||||
let parts = line
|
pub fn parse_console_command(line: &str) -> Option<LocalRequest> {
|
||||||
|
let parts: Vec<&str> = line
|
||||||
.trim_start_matches('/')
|
.trim_start_matches('/')
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.collect::<Vec<_>>();
|
.collect();
|
||||||
match parts.as_slice() {
|
match parts.as_slice() {
|
||||||
["tasks"] => Ok(self
|
["help"] => None,
|
||||||
.runtime
|
["tasks"] => Some(LocalRequest::ListTasks),
|
||||||
.state
|
["ping", _] | ["ping"] => None,
|
||||||
.active_tasks
|
["user", "add", username] => Some(LocalRequest::CreateUser {
|
||||||
.iter()
|
username: username.to_string(),
|
||||||
.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())
|
|
||||||
}
|
|
||||||
["user", "remove", username] => {
|
["user", "remove", username] => {
|
||||||
let user = user_manager::get_user_by_username(username)
|
let user = user_manager::get_user_by_username(username)?;
|
||||||
.ok_or_else(|| "Username does not exist".to_string())?;
|
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)
|
let message = CommunicationValue::new(CommunicationType::DeleteUser)
|
||||||
.with_sender(user.user_id as u64);
|
.with_sender(user.user_id as u64);
|
||||||
OMIKRON_CONNECTION
|
if let Err(_e) = OMIKRON_CONNECTION.send_message(&message).await {
|
||||||
.send_message(&message)
|
return ResponseResult::Error(IpcErrorCode::OmikronUnavailable);
|
||||||
.await
|
}
|
||||||
.map_err(|error| error.to_string())?;
|
|
||||||
user_manager::remove_user(user.user_id);
|
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()
|
LocalRequest::ReconnectOmikron => {
|
||||||
.into_iter()
|
|
||||||
.map(|user| format!("{} ({})", user.username, user.user_id))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n")),
|
|
||||||
["reconnect"] => {
|
|
||||||
OMIKRON_CONNECTION.reconnect().await;
|
OMIKRON_CONNECTION.reconnect().await;
|
||||||
Ok("Reconnected to Omikron server".into())
|
ResponseResult::Ok("Reconnected to Omikron server".into())
|
||||||
}
|
}
|
||||||
["regenerate", "keys"] => {
|
LocalRequest::RotateIotaIdentity => {
|
||||||
modify_config(|config| {
|
modify_config(|config| {
|
||||||
config.public_key = None;
|
config.public_key = None;
|
||||||
config.private_key = None;
|
config.private_key = None;
|
||||||
config.iota_id = None;
|
config.iota_id = None;
|
||||||
});
|
});
|
||||||
OMIKRON_CONNECTION.reconnect().await;
|
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"] => {
|
LocalRequest::RestartDaemon => {
|
||||||
*self.runtime.state.reload.write().await = true;
|
self.runtime.shutdown(ShutdownReason::Restart);
|
||||||
*self.runtime.state.shutdown.write().await = true;
|
ResponseResult::Ok("Daemon restart requested".into())
|
||||||
Ok("Daemon restart requested".into())
|
|
||||||
}
|
}
|
||||||
["shutdown"] | ["stop"] => {
|
LocalRequest::StopDaemon => {
|
||||||
*self.runtime.state.shutdown.write().await = true;
|
self.runtime.shutdown(ShutdownReason::Stop);
|
||||||
Ok("Daemon shutdown requested".into())
|
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
|
let response = OMIKRON_CONNECTION
|
||||||
.await_response(
|
.await_response(
|
||||||
&CommunicationValue::new(CommunicationType::Ping),
|
&CommunicationValue::new(CommunicationType::Ping),
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,123 @@ use iota_state::DaemonState;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use sysinfo::{RefreshKind, System};
|
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
|
/* This wrapper exposes daemon state as IPC-safe snapshots while preserving a
|
||||||
* single owned state instance for all daemon subsystems. */
|
* single owned state instance for all daemon subsystems. The cancellation token
|
||||||
#[derive(Clone, Default)]
|
* is the single lifecycle signal — all subsystems check it instead of a
|
||||||
|
* separate boolean. */
|
||||||
pub struct DaemonRuntime {
|
pub struct DaemonRuntime {
|
||||||
pub state: Arc<DaemonState>,
|
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 {
|
impl DaemonRuntime {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let (shutdown_tx, _) = watch::channel(None);
|
||||||
|
let (startup_phase, _) = watch::channel(StartupPhase::Starting);
|
||||||
|
let (degraded_reason, _) = watch::channel(None);
|
||||||
Self {
|
Self {
|
||||||
state: Arc::new(DaemonState::new()),
|
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 {
|
pub fn snapshot(&self) -> StateSnapshot {
|
||||||
let state = self
|
let state = self
|
||||||
.state
|
.state
|
||||||
|
|
@ -41,7 +143,7 @@ impl DaemonRuntime {
|
||||||
let mut system = System::new_with_specifics(RefreshKind::everything());
|
let mut system = System::new_with_specifics(RefreshKind::everything());
|
||||||
let mut counter = 0.0;
|
let mut counter = 0.0;
|
||||||
loop {
|
loop {
|
||||||
if *runtime.state.shutdown.read().await {
|
if runtime.is_shutting_down() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
system.refresh_cpu_all();
|
system.refresh_cpu_all();
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,42 @@
|
||||||
use crate::{CommandRouter, DaemonRuntime};
|
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::io::Result;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{env, os::fd::FromRawFd};
|
use std::{env, os::fd::FromRawFd};
|
||||||
use tokio::net::{UnixListener, UnixStream};
|
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 {
|
pub struct IpcServer {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
runtime: Arc<DaemonRuntime>,
|
runtime: Arc<DaemonRuntime>,
|
||||||
messages: broadcast::Sender<DaemonMessage>,
|
log_tx: broadcast::Sender<DaemonMessage>,
|
||||||
|
state_rx: watch::Sender<iota_ipc::StateSnapshot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IpcServer {
|
impl IpcServer {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
path: impl Into<PathBuf>,
|
path: impl Into<PathBuf>,
|
||||||
runtime: Arc<DaemonRuntime>,
|
runtime: Arc<DaemonRuntime>,
|
||||||
messages: broadcast::Sender<DaemonMessage>,
|
log_tx: broadcast::Sender<DaemonMessage>,
|
||||||
|
state_rx: watch::Sender<iota_ipc::StateSnapshot>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: path.into(),
|
path: path.into(),
|
||||||
runtime,
|
runtime,
|
||||||
messages,
|
log_tx,
|
||||||
|
state_rx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,11 +52,14 @@ impl IpcServer {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loop {
|
loop {
|
||||||
let (stream, _) = listener.accept().await?;
|
let (stream, _addr) = listener.accept().await?;
|
||||||
let runtime = self.runtime.clone();
|
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 {
|
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(
|
async fn handle_client(
|
||||||
stream: UnixStream,
|
stream: UnixStream,
|
||||||
runtime: Arc<DaemonRuntime>,
|
runtime: Arc<DaemonRuntime>,
|
||||||
messages: broadcast::Sender<DaemonMessage>,
|
log_tx: broadcast::Sender<DaemonMessage>,
|
||||||
|
_state_rx: watch::Sender<iota_ipc::StateSnapshot>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let peer = peer_credentials(&stream);
|
||||||
let (mut reader, mut writer) = stream.into_split();
|
let (mut reader, mut writer) = stream.into_split();
|
||||||
let mut outgoing = messages.subscribe();
|
let (directed_tx, directed_rx) = mpsc::channel::<DaemonMessage>(CLIENT_CHANNEL_SIZE);
|
||||||
let initial = DaemonMessage::StateUpdate(runtime.snapshot());
|
|
||||||
write_msg(&mut writer, &initial).await?;
|
// --- Handshake ---
|
||||||
let writer_task = tokio::spawn(async move {
|
let mut negotiated_version: Option<u16> = None;
|
||||||
while let Ok(message) = outgoing.recv().await {
|
for _ in 0..MAX_HANDSHAKE_RETRIES {
|
||||||
if write_msg(&mut writer, &message).await.is_err() {
|
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;
|
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());
|
let router = CommandRouter::new(runtime.clone());
|
||||||
loop {
|
loop {
|
||||||
match read_msg::<_, ClientMessage>(&mut reader).await {
|
match read_msg::<_, ClientMessage>(&mut reader).await {
|
||||||
Ok(ClientMessage::Command { seq, line }) => {
|
Ok(ClientMessage::Request(envelope)) => {
|
||||||
let result = router.route(seq, line).await;
|
let response = router.route(envelope.request_id, envelope.request).await;
|
||||||
let _ = messages.send(result);
|
let _ = directed_tx.send(DaemonMessage::Response(response)).await;
|
||||||
}
|
}
|
||||||
Ok(ClientMessage::Subscribe) => {
|
Ok(ClientMessage::Subscribe { .. }) => {
|
||||||
let _ = messages.send(DaemonMessage::StateUpdate(runtime.snapshot()));
|
let snapshot = DaemonMessage::StateUpdate(runtime.snapshot());
|
||||||
|
let _ = directed_tx.send(snapshot).await;
|
||||||
}
|
}
|
||||||
Ok(ClientMessage::Ping { seq }) => {
|
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) if error.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
@ -109,5 +251,10 @@ async fn handle_client(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writer_task.abort();
|
writer_task.abort();
|
||||||
|
log!(
|
||||||
|
"IPC client disconnected (pid={}, uid={})",
|
||||||
|
peer.pid,
|
||||||
|
peer.uid
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,5 @@ pub mod ipc_server;
|
||||||
pub mod log_broadcaster;
|
pub mod log_broadcaster;
|
||||||
|
|
||||||
pub use command_router::CommandRouter;
|
pub use command_router::CommandRouter;
|
||||||
pub use daemon_state::DaemonRuntime;
|
pub use daemon_state::{DaemonRuntime, ShutdownReason, StartupPhase};
|
||||||
pub use ipc_server::IpcServer;
|
pub use ipc_server::IpcServer;
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,4 @@ iota-storage = { path = "../iota-storage" }
|
||||||
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"] }
|
||||||
|
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_logger::{self as logger, log, log_t};
|
||||||
use iota_storage::users::user_manager;
|
use iota_storage::users::user_manager;
|
||||||
use iota_storage::util::config_util::CONFIG;
|
use iota_storage::util::config_util::CONFIG;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::{broadcast, watch};
|
||||||
|
|
||||||
fn socket_path() -> PathBuf {
|
fn socket_path() -> PathBuf {
|
||||||
std::env::var_os("IOTA_SOCKET")
|
std::env::var_os("IOTA_SOCKET")
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
|
|
@ -17,42 +16,79 @@ fn socket_path() -> PathBuf {
|
||||||
async fn main() {
|
async fn main() {
|
||||||
logger::startup();
|
logger::startup();
|
||||||
iota_storage::util::config_util::load_config();
|
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() {
|
if user_manager::load_users().await.is_err() {
|
||||||
log_t!("user_load_failed");
|
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();
|
runtime.spawn_system_monitor();
|
||||||
let (messages, _) = broadcast::channel(512);
|
|
||||||
log_broadcaster::spawn(messages.clone());
|
// --- State update publisher (watch-based, no full broadcast per tick) ---
|
||||||
let state_updates = runtime.clone();
|
let state_publisher = runtime.clone();
|
||||||
let state_messages = messages.clone();
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
if *state_updates.state.shutdown.read().await {
|
if state_publisher.is_shutting_down() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let _ = state_messages.send(iota_ipc::DaemonMessage::StateUpdate(
|
let snapshot = state_publisher.snapshot();
|
||||||
state_updates.snapshot(),
|
let _ = state_tx.send(snapshot);
|
||||||
));
|
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Web server ---
|
||||||
let port = CONFIG.load().port;
|
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);
|
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);
|
// --- Omikron connection ---
|
||||||
tokio::spawn(async move {
|
let omikron_result =
|
||||||
if let Err(error) = server.run().await {
|
omikron_connector::omikron_connection::get_omikron_connection(runtime.cancellation.clone())
|
||||||
eprintln!("iota-daemon IPC server failed: {error}");
|
.await;
|
||||||
}
|
if omikron_result.is_none() {
|
||||||
});
|
runtime.mark_degraded("Omikron connection unavailable".into());
|
||||||
log!("iota-daemon started");
|
|
||||||
while !*runtime.state.shutdown.read().await {
|
|
||||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
|
||||||
}
|
}
|
||||||
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 protocol;
|
||||||
pub mod transport;
|
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};
|
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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Client → Daemon
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
||||||
pub enum ClientMessage {
|
pub enum ClientMessage {
|
||||||
Command { seq: u64, line: String },
|
Hello { supported_versions: Vec<u16> },
|
||||||
Subscribe,
|
Subscribe { log_classes: Vec<String>, metric_interval_ms: Option<u64> },
|
||||||
|
Request(RequestEnvelope),
|
||||||
Ping { seq: u64 },
|
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)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
||||||
pub enum DaemonMessage {
|
pub enum DaemonMessage {
|
||||||
|
HelloAck(HelloAck),
|
||||||
LogEntry(LogEntry),
|
LogEntry(LogEntry),
|
||||||
StateUpdate(StateSnapshot),
|
StateUpdate(StateSnapshot),
|
||||||
CommandResult {
|
MetricSample(MetricSample),
|
||||||
seq: u64,
|
Response(ResponseEnvelope),
|
||||||
success: bool,
|
Pong { seq: u64 },
|
||||||
message: String,
|
LifecycleEvent(LifecycleEvent),
|
||||||
},
|
Gap { skipped: u64 },
|
||||||
Pong {
|
|
||||||
seq: 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)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct LogEntry {
|
pub struct LogEntry {
|
||||||
pub timestamp_ms: u128,
|
pub timestamp_ms: u128,
|
||||||
|
|
@ -40,3 +135,12 @@ pub struct StateSnapshot {
|
||||||
pub net_down: Vec<(f64, f64)>,
|
pub net_down: Vec<(f64, f64)>,
|
||||||
pub sys_info: String,
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{read_msg, write_msg};
|
use super::{read_msg, write_msg};
|
||||||
use crate::protocol::ClientMessage;
|
use crate::protocol::{ClientMessage, LocalRequest, RequestEnvelope};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn round_trips_framed_messages() {
|
async fn round_trips_framed_messages() {
|
||||||
let (mut writer, mut reader) = tokio::io::duplex(1024);
|
let (mut writer, mut reader) = tokio::io::duplex(1024);
|
||||||
let message = ClientMessage::Command {
|
let message = ClientMessage::Request(RequestEnvelope {
|
||||||
seq: 4,
|
request_id: 4,
|
||||||
line: "help".into(),
|
protocol_version: 2,
|
||||||
};
|
request: LocalRequest::GetStatus,
|
||||||
|
});
|
||||||
write_msg(&mut writer, &message)
|
write_msg(&mut writer, &message)
|
||||||
.await
|
.await
|
||||||
.expect("write succeeds");
|
.expect("write succeeds");
|
||||||
let received: ClientMessage = read_msg(&mut reader).await.expect("read 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]
|
[dependencies]
|
||||||
iota-cli = { path = "../iota-cli" }
|
iota-cli = { path = "../iota-cli" }
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
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")]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let path = socket_path();
|
let path = socket_path();
|
||||||
let ipc = match IpcClient::connect(&path).await {
|
let ipc = match IpcClient::connect_or_activate(&path).await {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Cannot connect to iota-daemon at {}: {error}",
|
"Cannot connect to iota-daemon at {}: {error}",
|
||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
|
eprintln!("Ensure iota-daemon.socket is enabled or iota-daemon is running.");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
ipc.spawn_reconnector();
|
||||||
let ui = start_tui(ipc);
|
let ui = start_tui(ipc);
|
||||||
ui.set_screen(Box::new(MainScreen::new(ui.clone()).await))
|
ui.set_screen(Box::new(MainScreen::new(ui.clone()).await))
|
||||||
.await;
|
.await;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ dashmap = "6.2.1"
|
||||||
json = "*"
|
json = "*"
|
||||||
reqwest = "0.13.2"
|
reqwest = "0.13.2"
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["rt"] }
|
||||||
uuid = { version = "*", features = ["v4"] }
|
uuid = { version = "*", features = ["v4"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
hex = "*"
|
hex = "*"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use iota_logger::{log, log_cv_in, log_cv_out, log_t};
|
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::chat_files::{self, MessageState, change_message_state};
|
||||||
use iota_storage::util::config_util::{CONFIG, modify_config};
|
use iota_storage::util::config_util::{CONFIG, modify_config};
|
||||||
use iota_storage::util::e2ee_storage::{self, PendingChatSecretForward, StoredChatSecret};
|
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::sync::{Mutex, RwLock, Semaphore, oneshot, watch};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::omega_discovery;
|
use crate::omega_discovery;
|
||||||
|
|
@ -161,10 +162,15 @@ pub struct OmikronConnection {
|
||||||
pub app_sessions: Arc<DashMap<u64, (i64, String)>>,
|
pub app_sessions: Arc<DashMap<u64, (i64, String)>>,
|
||||||
pub(crate) missed_pongs: Arc<AtomicU32>,
|
pub(crate) missed_pongs: Arc<AtomicU32>,
|
||||||
handler_semaphore: Arc<Semaphore>,
|
handler_semaphore: Arc<Semaphore>,
|
||||||
|
cancellation: CancellationToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OmikronConnection {
|
impl OmikronConnection {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
Self::with_cancellation(CancellationToken::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_cancellation(cancellation: CancellationToken) -> Self {
|
||||||
let (shutdown_tx, _) = watch::channel(false);
|
let (shutdown_tx, _) = watch::channel(false);
|
||||||
let (state_watch_tx, _) = watch::channel(ConnectionState::Disconnected);
|
let (state_watch_tx, _) = watch::channel(ConnectionState::Disconnected);
|
||||||
|
|
||||||
|
|
@ -183,6 +189,7 @@ impl OmikronConnection {
|
||||||
app_sessions: Arc::new(DashMap::new()),
|
app_sessions: Arc::new(DashMap::new()),
|
||||||
missed_pongs: Arc::new(AtomicU32::new(0)),
|
missed_pongs: Arc::new(AtomicU32::new(0)),
|
||||||
handler_semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_HANDLERS)),
|
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() {
|
if let Some(sender) = self.sender.read().await.as_ref() {
|
||||||
sender.close();
|
sender.close().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_state(ConnectionState::Disconnected).await;
|
self.set_state(ConnectionState::Disconnected).await;
|
||||||
|
|
@ -250,7 +257,7 @@ impl OmikronConnection {
|
||||||
let mut shutdown_rx = shutdown_rx;
|
let mut shutdown_rx = shutdown_rx;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if *shutdown_rx.borrow() || *SHUTDOWN.read().await {
|
if *shutdown_rx.borrow() || self.cancellation.is_cancelled() {
|
||||||
log_t!("omikron_connection_loop_shutdown");
|
log_t!("omikron_connection_loop_shutdown");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -621,7 +628,7 @@ impl OmikronConnection {
|
||||||
self.missed_pongs.load(Ordering::Relaxed)
|
self.missed_pongs.load(Ordering::Relaxed)
|
||||||
);
|
);
|
||||||
if let Some(sender) = self.sender.read().await.as_ref() {
|
if let Some(sender) = self.sender.read().await.as_ref() {
|
||||||
sender.close();
|
sender.close().await;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -1750,7 +1757,7 @@ impl OmikronConnection {
|
||||||
if !sender.is_open() {
|
if !sender.is_open() {
|
||||||
drop(sender_guard);
|
drop(sender_guard);
|
||||||
if let Some(sender) = self.sender.write().await.take() {
|
if let Some(sender) = self.sender.write().await.take() {
|
||||||
sender.close();
|
sender.close().await;
|
||||||
}
|
}
|
||||||
self.fail_all_waiting_tasks(format!(
|
self.fail_all_waiting_tasks(format!(
|
||||||
"Send failed: connection closed (connection_id={})",
|
"Send failed: connection closed (connection_id={})",
|
||||||
|
|
@ -1933,11 +1940,12 @@ pub static OMIKRON_CONNECTION: LazyLock<Arc<OmikronConnection>> = LazyLock::new(
|
||||||
conn
|
conn
|
||||||
});
|
});
|
||||||
|
|
||||||
pub async fn get_omikron_connection() -> Arc<OmikronConnection> {
|
pub async fn get_omikron_connection(
|
||||||
let conn = OMIKRON_CONNECTION.clone();
|
cancellation: CancellationToken,
|
||||||
|
) -> Option<Arc<OmikronConnection>> {
|
||||||
|
let conn = Arc::new(OmikronConnection::with_cancellation(cancellation));
|
||||||
conn.connect().await;
|
conn.connect().await;
|
||||||
conn
|
Some(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl iota_connection::connection_handler::ConnectionHandler for OmikronConnection {
|
impl iota_connection::connection_handler::ConnectionHandler for OmikronConnection {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||||
use iota_logger::{PrintType, log, log_cv, log_t};
|
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_manager::{add_user, save_users};
|
||||||
use iota_storage::users::user_profile::UserProfile;
|
use iota_storage::users::user_profile::UserProfile;
|
||||||
use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64};
|
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");
|
log_t!("User creation: Response returned none");
|
||||||
return (None, None);
|
return (None, None);
|
||||||
}
|
}
|
||||||
*SHUTDOWN.write().await = true;
|
|
||||||
*RELOAD.write().await = true;
|
|
||||||
log!("Created User");
|
log!("Created User");
|
||||||
save_file(
|
save_file(
|
||||||
"",
|
"",
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,22 @@ Wants=network-online.target
|
||||||
Requires=iota-daemon.socket
|
Requires=iota-daemon.socket
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=notify
|
||||||
ExecStart=/usr/bin/iota-daemon
|
ExecStart=/usr/bin/iota-daemon
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
RuntimeDirectory=iota
|
RuntimeDirectory=iota
|
||||||
RuntimeDirectoryMode=0750
|
RuntimeDirectoryMode=0750
|
||||||
Environment=IOTA_SOCKET=/run/iota/iota.sock
|
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]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ ListenStream=/run/iota/iota.sock
|
||||||
SocketMode=0660
|
SocketMode=0660
|
||||||
SocketUser=iota
|
SocketUser=iota
|
||||||
SocketGroup=iota
|
SocketGroup=iota
|
||||||
|
Backlog=5
|
||||||
RemoveOnStop=true
|
RemoveOnStop=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ edition = "2024"
|
||||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-server"] }
|
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-server"] }
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
http = "1"
|
http = "1"
|
||||||
iota-state = { path = "../iota-state" }
|
|
||||||
iota-util = { path = "../iota-util" }
|
|
||||||
iota-logger = { path = "../iota-logger" }
|
iota-logger = { path = "../iota-logger" }
|
||||||
|
iota-util = { path = "../iota-util" }
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["rt"] }
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use iota_logger::log;
|
use iota_logger::log;
|
||||||
use iota_state::{ACTIVE_TASKS, SHUTDOWN};
|
|
||||||
use iota_util::file_util::load_file_vec;
|
use iota_util::file_util::load_file_vec;
|
||||||
use mtp::host::HostConfig;
|
use mtp::host::HostConfig;
|
||||||
use mtp::webserver::{Http3Request, Http3Response, MTPWebServer, WebServerConfig};
|
use mtp::webserver::{Http3Request, Http3Response, MTPWebServer, WebServerConfig};
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
use tokio::time::{Duration, sleep};
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
const CERT_PATH: &str = "certs/cert.pem";
|
const CERT_PATH: &str = "certs/cert.pem";
|
||||||
const KEY_PATH: &str = "certs/cert.key";
|
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 {
|
let certificate = match tokio::fs::read(CERT_PATH).await {
|
||||||
Ok(certificate) => certificate,
|
Ok(certificate) => certificate,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
@ -102,7 +101,6 @@ pub async fn start(port: u16) -> bool {
|
||||||
|
|
||||||
log!("MTP web server running on port {}", port);
|
log!("MTP web server running on port {}", port);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
ACTIVE_TASKS.insert("WebServer".into());
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
result = server.accept() => {
|
result = server.accept() => {
|
||||||
|
|
@ -112,22 +110,12 @@ pub async fn start(port: u16) -> bool {
|
||||||
Err(error) => log!("MTP webserver connection failed: {}", error),
|
Err(error) => log!("MTP webserver connection failed: {}", error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = wait_for_shutdown() => {
|
_ = cancellation.cancelled() => {
|
||||||
server.shutdown().await;
|
server.shutdown().await;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ACTIVE_TASKS.remove("WebServer");
|
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_shutdown() {
|
|
||||||
loop {
|
|
||||||
if *SHUTDOWN.read().await {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue