feat(detmir): add rust linux install planners

This commit is contained in:
igor04091968
2026-06-02 18:23:44 +03:00
parent 632962a90e
commit 63774cb8c4
12 changed files with 511 additions and 2 deletions
+11
View File
@@ -233,6 +233,17 @@ dependencies = [
"tempfile",
]
[[package]]
name = "aw-linux-install"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"serde",
"serde_json",
"tempfile",
]
[[package]]
name = "aw-prune-local-state"
version = "0.1.0"
+1
View File
@@ -7,6 +7,7 @@ members = [
"crates/aw-db-maintenance",
"crates/aw-ensure-reliability",
"crates/aw-health-check",
"crates/aw-linux-install",
"crates/aw-prune-local-state",
"crates/check-aw-data",
"crates/check-aw-full",
+2
View File
@@ -25,6 +25,8 @@ scripts with durable standalone Rust modules.
process-level session events, with backup-before-delete.
- `aw-ensure-reliability` - safe dry-run/apply planner for AW service
reliability repair actions that were previously immediate Bash mutations.
- `aw-linux-install` - safe dry-run/apply planner for Linux ActivityWatch
install wrappers; legacy shell install is explicit.
- `check-aw-full` - read-only local AW/RDP full check replacement for the
legacy shell helper.
- `dlp-aggregator` - AW server DLP warehouse aggregator replacement.
+22
View File
@@ -1501,6 +1501,28 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg
aw-ensure-reliability --all-targets -- -D warnings`, release build OK,
`bash -n aw-server/ensure-reliability.sh`, artifact check OK,
`ansible-playbook deploy_aw_server.yml --syntax-check` OK.
54. `[done]` Перевести Linux install scripts в Rust-first safe planner слой:
- добавлен crate `aw-linux-install`;
- wrappers теперь Rust-first и dry-run по умолчанию:
`scripts/install_aw_linux_client.sh`,
`scripts/install_aw_linux_remote_worker.sh`,
`scripts/install_aw_linux_web_category_logger.sh`,
`scripts/install_aw_console_ssh_logger.sh`,
`scripts/install_aw_pve_webadmin_logger.sh`;
- real install требует explicit `--apply`;
- old shell install требует explicit `--apply-legacy`;
- `aw-linux-install --apply` запускает соответствующий legacy script с
`--apply-legacy`, сохраняя embedded Python collectors и shell install
contract;
- `remote_worker` legacy path исправлен: вложенные client/console/web
installers вызываются с `--apply-legacy`, чтобы real legacy install не
превратился в dry-run после оборачивания;
- PVE webadmin logger не запускался и Proxmox platform не менялся; это
только safe planner wrapper;
- gates: `cargo fmt --all -- --check`, `cargo test -p aw-linux-install`
(`2 passed`), `cargo clippy -p aw-linux-install --all-targets -- -D
warnings`, release build OK, `sh -n` для всех пяти wrappers OK,
dry-run JSON для всех пяти wrappers OK, artifact check OK.
Отложить:
@@ -0,0 +1,16 @@
[package]
name = "aw-linux-install"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
anyhow.workspace = true
clap.workspace = true
serde.workspace = true
serde_json.workspace = true
[dev-dependencies]
tempfile.workspace = true
@@ -0,0 +1,290 @@
use std::path::PathBuf;
use std::process::Command;
use anyhow::{Context, Result, bail};
use clap::{Parser, ValueEnum};
use serde::Serialize;
#[derive(Debug, Clone, Copy, ValueEnum, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
enum InstallKind {
Client,
RemoteWorker,
ConsoleSsh,
WebCategory,
PveWebadmin,
}
#[derive(Debug, Parser)]
#[command(about = "Safe planner/apply wrapper for AW Linux install scripts")]
struct Cli {
#[arg(long, value_enum)]
kind: InstallKind,
#[arg(long)]
legacy_script: PathBuf,
#[arg(long, default_value = "10.10.10.13")]
server_host: String,
#[arg(long, default_value = "5600")]
server_port: String,
#[arg(long, default_value = "5")]
poll_interval: String,
#[arg(long, default_value = "0.13.2")]
version: String,
#[arg(long)]
install_base: Option<PathBuf>,
#[arg(long, default_value_t = false)]
force: bool,
#[arg(long, default_value_t = false)]
apply: bool,
#[arg(long, default_value_t = false)]
json: bool,
}
#[derive(Debug, Serialize)]
struct Plan {
apply: bool,
kind: InstallKind,
legacy_script: PathBuf,
required_files: Vec<Requirement>,
steps: Vec<Step>,
missing_count: usize,
}
#[derive(Debug, Serialize)]
struct Requirement {
name: String,
ok: bool,
detail: String,
}
#[derive(Debug, Serialize)]
struct Step {
order: usize,
name: String,
mutation: bool,
command: Vec<String>,
summary: String,
}
fn main() {
let code = match run() {
Ok(code) => code,
Err(err) => {
eprintln!("{err:#}");
1
}
};
std::process::exit(code);
}
fn run() -> Result<i32> {
let cli = Cli::parse();
let plan = build_plan(&cli);
if !cli.apply {
print_plan(&plan, cli.json)?;
return Ok(if plan.missing_count == 0 { 0 } else { 2 });
}
if plan.missing_count > 0 {
print_plan(&plan, cli.json)?;
bail!("refusing --apply because required files are missing");
}
let Some(step) = plan.steps.first() else {
bail!("empty install plan");
};
let Some(program) = step.command.first() else {
bail!("empty legacy command");
};
let status = Command::new(program)
.args(&step.command[1..])
.status()
.with_context(|| format!("run {}", shell_join(&step.command)))?;
Ok(status.code().unwrap_or(1))
}
fn build_plan(cli: &Cli) -> Plan {
let required_files = vec![Requirement {
name: "legacy_script".to_string(),
ok: cli.legacy_script.is_file(),
detail: cli.legacy_script.display().to_string(),
}];
let command = legacy_command(cli);
let steps = vec![Step {
order: 1,
name: format!("{:?}", cli.kind).to_lowercase(),
mutation: true,
summary: summary(cli.kind).to_string(),
command,
}];
let missing_count = required_files.iter().filter(|item| !item.ok).count();
Plan {
apply: cli.apply,
kind: cli.kind,
legacy_script: cli.legacy_script.clone(),
required_files,
steps,
missing_count,
}
}
fn legacy_command(cli: &Cli) -> Vec<String> {
let mut command = vec![
"sh".to_string(),
cli.legacy_script.display().to_string(),
"--apply-legacy".to_string(),
"--server-host".to_string(),
cli.server_host.clone(),
"--server-port".to_string(),
cli.server_port.clone(),
];
match cli.kind {
InstallKind::Client => {
command.extend(["--version".to_string(), cli.version.clone()]);
if let Some(path) = &cli.install_base {
command.extend(["--install-base".to_string(), path.display().to_string()]);
}
if cli.force {
command.push("--force".to_string());
}
}
InstallKind::RemoteWorker => {
command.extend([
"--poll-interval".to_string(),
cli.poll_interval.clone(),
"--version".to_string(),
cli.version.clone(),
]);
}
InstallKind::ConsoleSsh | InstallKind::WebCategory | InstallKind::PveWebadmin => {
command.extend(["--poll-interval".to_string(), cli.poll_interval.clone()]);
}
}
command
}
fn summary(kind: InstallKind) -> &'static str {
match kind {
InstallKind::Client => {
"Install ActivityWatch Linux GUI watcher bundle and remote server config"
}
InstallKind::RemoteWorker => {
"Install Linux client, console/SSH logger, and web category logger"
}
InstallKind::ConsoleSsh => "Install console command and SSH session logger",
InstallKind::WebCategory => "Install Linux web category logger",
InstallKind::PveWebadmin => "Install Proxmox webadmin logger service",
}
}
fn print_plan(plan: &Plan, json: bool) -> Result<()> {
if json {
println!("{}", serde_json::to_string_pretty(plan)?);
return Ok(());
}
println!(
"aw-linux-install: {}",
if plan.apply { "apply" } else { "dry-run" }
);
println!("kind: {:?}", plan.kind);
println!("legacy_script: {}", plan.legacy_script.display());
println!("missing_inputs: {}", plan.missing_count);
println!("required files:");
for item in &plan.required_files {
println!(
" [{}] {} - {}",
if item.ok { "OK" } else { "MISS" },
item.name,
item.detail
);
}
println!("planned steps:");
for step in &plan.steps {
println!(
" {:02}. MUTATION {} :: {}",
step.order,
step.summary,
shell_join(&step.command)
);
}
if !plan.apply {
println!("No install executed. Use --apply for explicit legacy install execution.");
}
Ok(())
}
fn shell_join(command: &[String]) -> String {
command
.iter()
.map(|part| {
if part
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || "-_./:=,".contains(ch))
{
part.clone()
} else {
format!("'{}'", part.replace('\'', "'\\''"))
}
})
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_plan_preserves_version_and_force() {
let dir = tempfile::tempdir().unwrap();
let script = dir.path().join("install.sh");
std::fs::write(&script, "#!/bin/sh\n").unwrap();
let cli = Cli {
kind: InstallKind::Client,
legacy_script: script,
server_host: "10.10.10.13".to_string(),
server_port: "5600".to_string(),
poll_interval: "5".to_string(),
version: "0.13.2".to_string(),
install_base: Some(PathBuf::from("/tmp/aw")),
force: true,
apply: false,
json: false,
};
let plan = build_plan(&cli);
let cmd = &plan.steps[0].command;
assert!(cmd.contains(&"--apply-legacy".to_string()));
assert!(cmd.contains(&"--install-base".to_string()));
assert!(cmd.contains(&"--force".to_string()));
}
#[test]
fn remote_worker_plan_includes_poll_and_version() {
let dir = tempfile::tempdir().unwrap();
let script = dir.path().join("install.sh");
std::fs::write(&script, "#!/bin/sh\n").unwrap();
let cli = Cli {
kind: InstallKind::RemoteWorker,
legacy_script: script,
server_host: "host".to_string(),
server_port: "5600".to_string(),
poll_interval: "9".to_string(),
version: "0.13.3".to_string(),
install_base: None,
force: false,
apply: false,
json: false,
};
let plan = build_plan(&cli);
let joined = shell_join(&plan.steps[0].command);
assert!(joined.contains("--poll-interval 9"));
assert!(joined.contains("--version 0.13.3"));
}
}
@@ -19,6 +19,7 @@ required_bins=(
aw-db-maintenance
aw-ensure-reliability
aw-health-check
aw-linux-install
aw-prune-local-state
check-aw-data
check-aw-full
+33
View File
@@ -11,6 +11,39 @@ LOG_DIR="${HOME}/.local/state/aw-console-ssh-logger/logs"
SYSTEMD_USER_DIR="${HOME}/.config/systemd/user"
BASHRC="${HOME}/.bashrc"
BASH_PROFILE="${HOME}/.bash_profile"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)"
SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")"
if [ "${1:-}" = "--apply-legacy" ]; then
shift
else
TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}"
for candidate in \
"${AW_LINUX_INSTALL_RUST:-}" \
"${TARGET_ROOT}/release/aw-linux-install" \
"${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \
"/usr/local/bin/aw-linux-install"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
exec "$candidate" --kind console-ssh --legacy-script "$SELF_PATH" "$@"
fi
done
cat >&2 <<'EOF'
install_aw_console_ssh_logger.sh now requires the Rust planner for safe default runs.
Build it first:
cd adk-rust && cargo build --release -p aw-linux-install
Safe dry-run:
scripts/install_aw_console_ssh_logger.sh --json
Explicit install:
scripts/install_aw_console_ssh_logger.sh --apply
Old shell install:
scripts/install_aw_console_ssh_logger.sh --apply-legacy
EOF
exit 2
fi
usage() {
cat <<'EOF'
+33
View File
@@ -9,6 +9,39 @@ BIN_DIR="${HOME}/.local/bin"
AUTOSTART_DIR="${HOME}/.config/autostart"
CONFIG_ROOT="${XDG_CONFIG_HOME:-$HOME/.config}/activitywatch"
FORCE="0"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)"
SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")"
if [ "${1:-}" = "--apply-legacy" ]; then
shift
else
TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}"
for candidate in \
"${AW_LINUX_INSTALL_RUST:-}" \
"${TARGET_ROOT}/release/aw-linux-install" \
"${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \
"/usr/local/bin/aw-linux-install"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
exec "$candidate" --kind client --legacy-script "$SELF_PATH" "$@"
fi
done
cat >&2 <<'EOF'
install_aw_linux_client.sh now requires the Rust planner for safe default runs.
Build it first:
cd adk-rust && cargo build --release -p aw-linux-install
Safe dry-run:
scripts/install_aw_linux_client.sh --json
Explicit install:
scripts/install_aw_linux_client.sh --apply
Old shell install:
scripts/install_aw_linux_client.sh --apply-legacy
EOF
exit 2
fi
usage() {
cat <<'EOF'
+36 -2
View File
@@ -5,6 +5,39 @@ SERVER_HOST="10.10.10.13"
SERVER_PORT="5600"
POLL_INTERVAL="5"
AW_VERSION="0.13.2"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)"
SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")"
if [ "${1:-}" = "--apply-legacy" ]; then
shift
else
TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}"
for candidate in \
"${AW_LINUX_INSTALL_RUST:-}" \
"${TARGET_ROOT}/release/aw-linux-install" \
"${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \
"/usr/local/bin/aw-linux-install"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
exec "$candidate" --kind remote-worker --legacy-script "$SELF_PATH" "$@"
fi
done
cat >&2 <<'EOF'
install_aw_linux_remote_worker.sh now requires the Rust planner for safe default runs.
Build it first:
cd adk-rust && cargo build --release -p aw-linux-install
Safe dry-run:
scripts/install_aw_linux_remote_worker.sh --json
Explicit install:
scripts/install_aw_linux_remote_worker.sh --apply
Old shell install:
scripts/install_aw_linux_remote_worker.sh --apply-legacy
EOF
exit 2
fi
usage() {
cat <<'EOF'
@@ -49,19 +82,20 @@ while [ "$#" -gt 0 ]; do
esac
done
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
sh "${SCRIPT_DIR}/install_aw_linux_client.sh" \
--apply-legacy \
--server-host "${SERVER_HOST}" \
--server-port "${SERVER_PORT}" \
--version "${AW_VERSION}"
sh "${SCRIPT_DIR}/install_aw_console_ssh_logger.sh" \
--apply-legacy \
--server-host "${SERVER_HOST}" \
--server-port "${SERVER_PORT}" \
--poll-interval "${POLL_INTERVAL}"
sh "${SCRIPT_DIR}/install_aw_linux_web_category_logger.sh" \
--apply-legacy \
--server-host "${SERVER_HOST}" \
--server-port "${SERVER_PORT}" \
--poll-interval "${POLL_INTERVAL}"
@@ -10,6 +10,39 @@ STATE_DIR="${HOME}/.local/state/aw-linux-web-category"
LOG_DIR="${HOME}/.local/state/aw-linux-web-category/logs"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/aw-linux-web-category"
SYSTEMD_USER_DIR="${HOME}/.config/systemd/user"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)"
SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")"
if [ "${1:-}" = "--apply-legacy" ]; then
shift
else
TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}"
for candidate in \
"${AW_LINUX_INSTALL_RUST:-}" \
"${TARGET_ROOT}/release/aw-linux-install" \
"${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \
"/usr/local/bin/aw-linux-install"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
exec "$candidate" --kind web-category --legacy-script "$SELF_PATH" "$@"
fi
done
cat >&2 <<'EOF'
install_aw_linux_web_category_logger.sh now requires the Rust planner for safe default runs.
Build it first:
cd adk-rust && cargo build --release -p aw-linux-install
Safe dry-run:
scripts/install_aw_linux_web_category_logger.sh --json
Explicit install:
scripts/install_aw_linux_web_category_logger.sh --apply
Old shell install:
scripts/install_aw_linux_web_category_logger.sh --apply-legacy
EOF
exit 2
fi
usage() {
cat <<'EOF'
+33
View File
@@ -9,6 +9,39 @@ STATE_DIR="/var/lib/aw-pve-webadmin-logger"
LOG_DIR="/var/log/aw-pve-webadmin-logger"
CONFIG_PATH="/etc/aw-pve-webadmin-logger/config.json"
SERVICE_PATH="/etc/systemd/system/aw-pve-webadmin-logger.service"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ROOT_DIR="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)"
SELF_PATH="${SCRIPT_DIR}/$(basename -- "$0")"
if [ "${1:-}" = "--apply-legacy" ]; then
shift
else
TARGET_ROOT="${CARGO_TARGET_DIR:-${ROOT_DIR}/adk-rust/target}"
for candidate in \
"${AW_LINUX_INSTALL_RUST:-}" \
"${TARGET_ROOT}/release/aw-linux-install" \
"${ROOT_DIR}/adk-rust/target/release/aw-linux-install" \
"/usr/local/bin/aw-linux-install"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
exec "$candidate" --kind pve-webadmin --legacy-script "$SELF_PATH" "$@"
fi
done
cat >&2 <<'EOF'
install_aw_pve_webadmin_logger.sh now requires the Rust planner for safe default runs.
Build it first:
cd adk-rust && cargo build --release -p aw-linux-install
Safe dry-run:
scripts/install_aw_pve_webadmin_logger.sh --json
Explicit install:
scripts/install_aw_pve_webadmin_logger.sh --apply
Old shell install:
scripts/install_aw_pve_webadmin_logger.sh --apply-legacy
EOF
exit 2
fi
usage() {
cat <<'EOF'