MAM append использует один sync вместо четырёх, SM checkpoint — один вместо двух. Checksummed redo journals сохраняют commit-before-ACK и восстанавливают native archive/SM JSON exports после аварии. - Ограничены рост журналов, ротация и tombstone GC. Старый `.rappend` и legacy JSON импортируются. Retain/save/purge не возвращают удалённые записи. - Recovery синхронизирует валидный journal перед публикацией состояния; проверка checksum не заменяет durability barrier. EIO при adoption блокирует startup или offline handoff. Новые fault fixtures проверяют сохранность журнала и retry. - Replay SM проверяет checksum/JSON всех frames, но создаёт и валидирует полный Record только для последнего snapshot, сокращая работу с superseded queues. - `message-processing-slow` содержит времена MAM отправителя/получателя, доставки, carbons, offline/push и handled, а также число ресурсов/копий. Payload и секреты не добавляются в логи. - Добавлена offline команда `storage-checkpoint CONFIG`: durable экспорт и передача archive writer/подготовка отката на legacy Rust. Не запускайте другого writer и не удаляйте journals до успешного завершения команды. - Новые crash, partial-write, corruption, EIO, recovery, handoff и latency регрессии включены в GitHub Actions через verify-local.sh; отчёты сохраняются. Измерения, ограничения и эксплуатация: [docs/DURABLE-MESSAGE-IO.md](docs/DURABLE-MESSAGE-IO.md), [docs/VALIDATION-0.8.3.md](docs/VALIDATION-0.8.3.md).
149 lines
6.6 KiB
Rust
149 lines
6.6 KiB
Rust
use anyhow::{Context, Result, bail};
|
|
use rust_xmpp::config;
|
|
use std::{env, path::Path};
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
if let Err(e) = run().await {
|
|
eprintln!("error: {e:#}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
async fn run() -> Result<()> {
|
|
let args: Vec<_> = env::args().skip(1).collect();
|
|
match args.first().map(String::as_str) {
|
|
Some("storage-checkpoint") if args.len() == 2 => {
|
|
let (c, domain) = rust_xmpp::server::settings_config(Path::new(&args[1]))?;
|
|
let data = Path::new(
|
|
c.option(&domain, "data_path")
|
|
.and_then(|v| v.as_str())
|
|
.context("data_path missing")?,
|
|
);
|
|
let _data_guard = rust_xmpp::data_lock::DataDirectoryGuard::acquire(data)?;
|
|
let archives = rust_xmpp::archive::checkpoint_all(data)?;
|
|
let sm_path = data
|
|
.join(rust_xmpp::storage::encode_name(&domain))
|
|
.join("rust_smacks");
|
|
let sessions = if sm_path.try_exists()? {
|
|
rust_xmpp::session_store::checkpoint_exports(&sm_path)?
|
|
} else {
|
|
0
|
|
};
|
|
println!("Durable native archive handoff: {archives}; SM JSON exports: {sessions}");
|
|
}
|
|
Some("upload-migrate") if args.len() == 5 || args.len() == 6 => {
|
|
anyhow::ensure!(
|
|
args[3] == "--component" && (args.len() == 5 || args[5] == "--execute"),
|
|
"usage: upload-migrate OLD_CONFIG NEW_CONFIG --component HOST [--execute]"
|
|
);
|
|
rust_xmpp::upload_migration::migrate(
|
|
Path::new(&args[1]),
|
|
Path::new(&args[2]),
|
|
&args[4],
|
|
args.len() == 6,
|
|
)
|
|
.await?;
|
|
}
|
|
Some("check") => {
|
|
let path = args
|
|
.get(1)
|
|
.map(String::as_str)
|
|
.unwrap_or("/etc/prosody/prosody.cfg.lua");
|
|
let (c, d) = rust_xmpp::server::settings_config(Path::new(path))?;
|
|
let data = c
|
|
.option(&d, "data_path")
|
|
.and_then(|v| v.as_str())
|
|
.context("data_path missing")?;
|
|
let socket = rust_xmpp::server::admin_path(&c, &d, Path::new(data));
|
|
let reply = rust_xmpp::admin::query(&socket, "server:status()", None).await?;
|
|
if reply.error {
|
|
bail!("server status query failed")
|
|
};
|
|
println!("{}", reply.message);
|
|
}
|
|
Some("shell") if args.len() >= 3 => {
|
|
let (c, d) = rust_xmpp::server::settings_config(Path::new(&args[1]))?;
|
|
let data = c
|
|
.option(&d, "data_path")
|
|
.and_then(|v| v.as_str())
|
|
.context("data_path")?;
|
|
let path = rust_xmpp::server::admin_path(&c, &d, Path::new(data));
|
|
let reply = rust_xmpp::admin::query(&path, &args[2], None).await?;
|
|
for line in reply.output {
|
|
println!("{line}");
|
|
}
|
|
if reply.error {
|
|
bail!("{}", reply.message)
|
|
}
|
|
println!("{}", reply.message);
|
|
}
|
|
Some("--version") => println!(
|
|
"rust-xmpp {} — experimental XMPP runtime",
|
|
env!("CARGO_PKG_VERSION")
|
|
),
|
|
Some("check-config") if args.len() == 2 => {
|
|
let cfg = config::load(Path::new(&args[1]))?;
|
|
println!(
|
|
"Configuration parsed and basic types validated. Runtime support is checked separately by serve."
|
|
);
|
|
println!("Virtual hosts/components: {}", cfg.hosts.len());
|
|
for name in cfg.hosts.keys() {
|
|
println!(
|
|
"{name}: {}",
|
|
cfg.modules(name)?
|
|
.into_iter()
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
);
|
|
}
|
|
for d in cfg.diagnostics() {
|
|
println!("{} [{}]: {}", d.level, d.scope, d.message);
|
|
}
|
|
}
|
|
Some("convert") if args.len() == 4 => {
|
|
let cfg = config::load(Path::new(&args[1]))?;
|
|
let text = match args[2].as_str() {
|
|
"json" => serde_json::to_string_pretty(&cfg)?,
|
|
"yaml" | "yml" => serde_yaml_ng::to_string(&cfg)?,
|
|
"toml" => toml::to_string_pretty(&cfg)?,
|
|
_ => bail!("output format must be json, yaml or toml"),
|
|
};
|
|
// Refuse overwrite; owner-only mode because converted configs contain secrets.
|
|
let mut opts = std::fs::OpenOptions::new();
|
|
opts.write(true).create_new(true);
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
opts.mode(0o600);
|
|
}
|
|
let mut file = opts
|
|
.open(&args[3])
|
|
.context("cannot create output (existing files are never overwritten)")?;
|
|
use std::io::Write;
|
|
file.write_all(text.as_bytes())?;
|
|
file.sync_all()?;
|
|
println!("Written {}", args[3]);
|
|
}
|
|
Some("serve") if args.len() == 3 && args[2] == "--experimental" => {
|
|
rust_xmpp::server::serve(Path::new(&args[1])).await?;
|
|
}
|
|
Some("serve") => {
|
|
bail!("use serve CONFIG --experimental; full Prosody replacement is not implemented")
|
|
}
|
|
Some("create-user") if args.len() == 4 && args[3] == "--password-stdin" => {
|
|
let (data, domain, hash) = rust_xmpp::server::account_config(Path::new(&args[1]))?;
|
|
let _data_guard = rust_xmpp::data_lock::DataDirectoryGuard::acquire(&data)?;
|
|
let mut password = String::new();
|
|
std::io::stdin().read_line(&mut password)?;
|
|
let password = password.trim_end_matches(['\r', '\n']);
|
|
rust_xmpp::auth::create(&data, &domain, &args[2], password, &hash)?;
|
|
println!("Account created: {}@{}", args[2], domain);
|
|
}
|
|
Some("--help") | None => println!(
|
|
"rust-xmpp upload-migrate OLD_CONFIG NEW_CONFIG --component HOST [--execute]\nrust-xmpp check-config PATH\nrust-xmpp convert PATH json|yaml|toml OUTPUT\nrust-xmpp --version\nrust-xmpp storage-checkpoint CONFIG (offline native handoff/downgrade)\n\nExperimental runtime: serve CONFIG --experimental\ncreate-user CONFIG USER --password-stdin\ncheck [CONFIG] (running server healthcheck)\nshell CONFIG COMMAND\n\nSTARTTLS, SCRAM/PLAIN, local messaging, roster, MAM, MUC, PEP/pubsub, stream management, push, federation and HTTP transports are implemented with limitations; see docs/COMPATIBILITY.md."
|
|
),
|
|
_ => bail!("invalid command; use --help"),
|
|
}
|
|
Ok(())
|
|
}
|