bin/{wall,write}: init
This commit is contained in:
Generated
+1
@@ -1031,6 +1031,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
|
|||||||
+21
@@ -42,6 +42,7 @@ chrono-tz = "0.10.4"
|
|||||||
netlink-proto = "0.12.0"
|
netlink-proto = "0.12.0"
|
||||||
netlink-sys = { version = "0.8.8", features = ["tokio_socket"] }
|
netlink-sys = { version = "0.8.8", features = ["tokio_socket"] }
|
||||||
netlink-packet-audit = "0.6.1"
|
netlink-packet-audit = "0.6.1"
|
||||||
|
thiserror = "2.0.19"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc = "2.0.7"
|
indoc = "2.0.7"
|
||||||
@@ -90,6 +91,16 @@ name = "rwho"
|
|||||||
bench = false
|
bench = false
|
||||||
path = "src/bin/rwho.rs"
|
path = "src/bin/rwho.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "wall"
|
||||||
|
bench = false
|
||||||
|
path = "src/bin/wall.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "write"
|
||||||
|
bench = false
|
||||||
|
path = "src/bin/write.rs"
|
||||||
|
|
||||||
[profile.releaselto]
|
[profile.releaselto]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
strip = true
|
strip = true
|
||||||
@@ -125,6 +136,16 @@ assets = [
|
|||||||
"usr/bin/rwho",
|
"usr/bin/rwho",
|
||||||
"0755"
|
"0755"
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"target/release/wall",
|
||||||
|
"usr/bin/wall",
|
||||||
|
"0755"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"target/release/write",
|
||||||
|
"usr/bin/write",
|
||||||
|
"0755"
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"assets/debian/config.toml",
|
"assets/debian/config.toml",
|
||||||
"etc/roowho2/config.toml",
|
"etc/roowho2/config.toml",
|
||||||
|
|||||||
+1
-1
@@ -81,7 +81,7 @@ buildFunction ({
|
|||||||
installShellCompletion "--${shell}" --cmd "${command}" "$TMP/${command}.${shell}"
|
installShellCompletion "--${shell}" --cmd "${command}" "$TMP/${command}.${shell}"
|
||||||
'') {
|
'') {
|
||||||
shell = [ "bash" "zsh" "fish" ];
|
shell = [ "bash" "zsh" "fish" ];
|
||||||
command = [ "rwho" "ruptime" "finger" ];
|
command = [ "rwho" "ruptime" "finger" "wall" "write" ];
|
||||||
};
|
};
|
||||||
in lib.concatStringsSep "\n" installShellCompletions;
|
in lib.concatStringsSep "\n" installShellCompletions;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::io::Read as _;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use clap::{CommandFactory, Parser};
|
||||||
|
use clap_complete::{Shell, generate};
|
||||||
|
use roowho2_lib::server::varlink_api::{
|
||||||
|
DEFAULT_CLIENT_SERVER_SOCKET_PATH, VarlinkWalldClientProxy, VarlinkWalldClientResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Write a message to all users
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
|
||||||
|
pub struct Args {
|
||||||
|
/// Only send message to group
|
||||||
|
#[arg(long, short, value_name = "GROUP")]
|
||||||
|
group: Option<String>,
|
||||||
|
|
||||||
|
// TODO: this 'works only for root' is leftover from the original wall implementation, no?
|
||||||
|
// maybe add it to the integration test?
|
||||||
|
/// Do not print banner, works only for root
|
||||||
|
#[arg(long, short)]
|
||||||
|
nobanner: bool,
|
||||||
|
|
||||||
|
/// Write timeout in seconds
|
||||||
|
#[arg(long, short, value_name = "TIMEOUT", default_value_t = 30)]
|
||||||
|
timeout: u32,
|
||||||
|
|
||||||
|
/// Message to send, if not specified, read from stdin
|
||||||
|
#[arg(value_name = "MESSAGE | FILE")]
|
||||||
|
file_or_message: Option<String>,
|
||||||
|
|
||||||
|
/// Generate shell completion scripts for the specified shell
|
||||||
|
/// and print them to stdout.
|
||||||
|
#[arg(long, value_enum, hide = true)]
|
||||||
|
completions: Option<Shell>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if let Some(shell) = args.completions {
|
||||||
|
generate(shell, &mut Args::command(), "wall", &mut std::io::stdout());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = read_message(args.file_or_message.as_deref())?;
|
||||||
|
|
||||||
|
let mut conn = zlink::tokio::unix::connect(DEFAULT_CLIENT_SERVER_SOCKET_PATH)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!("failed to connect to roowho2 at {DEFAULT_CLIENT_SERVER_SOCKET_PATH}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let response = conn
|
||||||
|
.wall(message, args.group, args.nobanner, args.timeout)
|
||||||
|
.await
|
||||||
|
.context("varlink call to walld failed")?
|
||||||
|
.map_err(|err| anyhow::format_err!("{err}"))
|
||||||
|
.and_then(|res| {
|
||||||
|
if let VarlinkWalldClientResponse::Wall(r) = res {
|
||||||
|
Ok(r)
|
||||||
|
} else {
|
||||||
|
Err(anyhow::format_err!(
|
||||||
|
"unexpected response from walld: {:?}",
|
||||||
|
res
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for failure in &response.failures {
|
||||||
|
eprintln!(
|
||||||
|
"wall: could not reach {} on {}: {}",
|
||||||
|
failure.user, failure.tty, failure.reason
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.failures.is_empty() && response.delivered.is_empty() {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_message(file_or_message: Option<&str>) -> anyhow::Result<String> {
|
||||||
|
match file_or_message {
|
||||||
|
Some(arg) if std::path::Path::new(arg).is_file() => {
|
||||||
|
std::fs::read_to_string(arg).with_context(|| format!("cannot read {arg}"))
|
||||||
|
}
|
||||||
|
Some(text) => Ok(text.to_string()),
|
||||||
|
None => {
|
||||||
|
let mut buf = String::new();
|
||||||
|
std::io::stdin()
|
||||||
|
.read_to_string(&mut buf)
|
||||||
|
.context("failed to read message from stdin")?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use std::io::Read as _;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use clap::{CommandFactory, Parser};
|
||||||
|
use clap_complete::{Shell, generate};
|
||||||
|
|
||||||
|
use roowho2_lib::server::varlink_api::{
|
||||||
|
DEFAULT_CLIENT_SERVER_SOCKET_PATH, VarlinkWalldClientProxy, VarlinkWalldClientResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Send a message to another user
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
|
||||||
|
pub struct Args {
|
||||||
|
/// User to send the message to
|
||||||
|
#[arg(value_name = "USER", required_unless_present = "completions")]
|
||||||
|
user: Option<String>,
|
||||||
|
|
||||||
|
/// The tty to send the message to
|
||||||
|
ttyname: Option<String>,
|
||||||
|
|
||||||
|
/// Generate shell completion scripts for the specified shell
|
||||||
|
/// and print them to stdout.
|
||||||
|
#[arg(long, value_enum, hide = true)]
|
||||||
|
completions: Option<Shell>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if let Some(shell) = args.completions {
|
||||||
|
generate(shell, &mut Args::command(), "write", &mut std::io::stdout());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut message = String::new();
|
||||||
|
std::io::stdin()
|
||||||
|
.read_to_string(&mut message)
|
||||||
|
.context("failed to read message from stdin")?;
|
||||||
|
|
||||||
|
let mut conn = zlink::tokio::unix::connect(DEFAULT_CLIENT_SERVER_SOCKET_PATH)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!("failed to connect to roowho2 at {DEFAULT_CLIENT_SERVER_SOCKET_PATH}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let user = args
|
||||||
|
.user
|
||||||
|
.expect("required_unless_present=completions guarantees this is set");
|
||||||
|
|
||||||
|
VarlinkWalldClientProxy::write(&mut conn, user, args.ttyname, message)
|
||||||
|
.await
|
||||||
|
.context("varlink call to walld failed")?
|
||||||
|
.map_err(|err| anyhow::format_err!("{err}"))
|
||||||
|
.and_then(|res| {
|
||||||
|
if let VarlinkWalldClientResponse::Write(r) = res {
|
||||||
|
Ok(r)
|
||||||
|
} else {
|
||||||
|
Err(anyhow::format_err!(
|
||||||
|
"unexpected response from walld: {:?}",
|
||||||
|
res
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -124,6 +124,130 @@ pub enum VarlinkFingerClientError {
|
|||||||
Disabled,
|
Disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Types for 'no.ntnu.pvv.roowho2.walld'
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(tag = "method", content = "parameters")]
|
||||||
|
pub enum VarlinkWalldClientRequest {
|
||||||
|
#[serde(rename = "no.ntnu.pvv.roowho2.walld.Wall")]
|
||||||
|
Wall {
|
||||||
|
message: String,
|
||||||
|
group: Option<String>,
|
||||||
|
nobanner: bool,
|
||||||
|
timeout_secs: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[serde(rename = "no.ntnu.pvv.roowho2.walld.Write")]
|
||||||
|
Write {
|
||||||
|
user: String,
|
||||||
|
tty: Option<String>,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zlink::proxy("no.ntnu.pvv.roowho2.walld")]
|
||||||
|
pub trait VarlinkWalldClientProxy {
|
||||||
|
/// Broadcast `message` to the tty of every logged in user, optionally restricted to `group`.
|
||||||
|
async fn wall(
|
||||||
|
&mut self,
|
||||||
|
message: String,
|
||||||
|
group: Option<String>,
|
||||||
|
nobanner: bool,
|
||||||
|
timeout_secs: u32,
|
||||||
|
) -> zlink::Result<Result<VarlinkWalldClientResponse, VarlinkWalldClientError>>;
|
||||||
|
|
||||||
|
/// Send `message` to a the tty of a single `user`, optionally targeting a specific `tty`.
|
||||||
|
async fn write(
|
||||||
|
&mut self,
|
||||||
|
user: String,
|
||||||
|
tty: Option<String>,
|
||||||
|
message: String,
|
||||||
|
) -> zlink::Result<Result<VarlinkWalldClientResponse, VarlinkWalldClientError>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum VarlinkWalldClientResponse {
|
||||||
|
Wall(VarlinkWallResponse),
|
||||||
|
Write(VarlinkWriteResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub struct VarlinkWallResponse {
|
||||||
|
pub delivered: Vec<VarlinkWallDelivery>,
|
||||||
|
pub failures: Vec<VarlinkWallDeliveryFailure>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct VarlinkWallDelivery {
|
||||||
|
pub user: String,
|
||||||
|
pub tty: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct VarlinkWallDeliveryFailure {
|
||||||
|
pub user: String,
|
||||||
|
pub tty: String,
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub struct VarlinkWriteResponse {
|
||||||
|
/// The tty the message was actually delivered to.
|
||||||
|
/// This is relevant when the caller didn't specify one, and it was picked automatically.
|
||||||
|
pub tty: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, ReplyError)]
|
||||||
|
#[zlink(interface = "no.ntnu.pvv.roowho2.walld")]
|
||||||
|
pub enum VarlinkWalldClientError {
|
||||||
|
/// The walld service is not enabled on the server.
|
||||||
|
Disabled,
|
||||||
|
/// Caller was not authorized by polkit to perform the request.
|
||||||
|
NotAuthorized,
|
||||||
|
/// The target user is not logged in anywhere.
|
||||||
|
UserNotLoggedIn { user: String },
|
||||||
|
/// The target user has disabled incoming messages on the relevant tty/ttys.
|
||||||
|
MessagesDisabled { user: String },
|
||||||
|
/// The requested tty does not belong to the target user, or does not exist.
|
||||||
|
TtyNotFound { user: String, tty: String },
|
||||||
|
/// The request parameters were invalid (e.g. unknown group).
|
||||||
|
InvalidRequest { message: String },
|
||||||
|
/// The request timed out.
|
||||||
|
TimedOut,
|
||||||
|
/// Something went wrong while talking to the tty (open/write failure).
|
||||||
|
Io { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use `thiserror` to derive `Display` the impl instead.
|
||||||
|
|
||||||
|
impl std::fmt::Display for VarlinkWalldClientError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
VarlinkWalldClientError::Disabled => {
|
||||||
|
write!(f, "The walld service is disabled on the server")
|
||||||
|
}
|
||||||
|
VarlinkWalldClientError::NotAuthorized => write!(f, "Caller is not authorized"),
|
||||||
|
VarlinkWalldClientError::UserNotLoggedIn { user } => {
|
||||||
|
write!(f, "User '{}' is not logged in", user)
|
||||||
|
}
|
||||||
|
VarlinkWalldClientError::MessagesDisabled { user } => {
|
||||||
|
write!(f, "User '{}' has disabled incoming messages", user)
|
||||||
|
}
|
||||||
|
VarlinkWalldClientError::TtyNotFound { user, tty } => {
|
||||||
|
write!(f, "Tty '{}' not found for user '{}'", tty, user)
|
||||||
|
}
|
||||||
|
VarlinkWalldClientError::InvalidRequest { message } => {
|
||||||
|
write!(f, "Invalid request: {}", message)
|
||||||
|
}
|
||||||
|
VarlinkWalldClientError::TimedOut => write!(f, "Request timed out"),
|
||||||
|
VarlinkWalldClientError::Io { message } => {
|
||||||
|
write!(f, "I/O error while sending message: {}", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -132,6 +256,7 @@ pub enum VarlinkFingerClientError {
|
|||||||
pub enum VarlinkMethod {
|
pub enum VarlinkMethod {
|
||||||
Rwhod(VarlinkRwhodClientRequest),
|
Rwhod(VarlinkRwhodClientRequest),
|
||||||
Finger(VarlinkFingerClientRequest),
|
Finger(VarlinkFingerClientRequest),
|
||||||
|
Walld(VarlinkWalldClientRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -460,6 +585,24 @@ impl zlink::Service<UnixStream> for VarlinkRoowhoo2ClientServer {
|
|||||||
Default::default(),
|
Default::default(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
VarlinkMethod::Walld(VarlinkWalldClientRequest::Wall { .. }) => {
|
||||||
|
tracing::error!("Received Wall request, but walld service is not implemented");
|
||||||
|
(
|
||||||
|
MethodReply::Error(VarlinkReplyError::Rwhod(
|
||||||
|
VarlinkRwhodClientError::InvalidRequest,
|
||||||
|
)),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
VarlinkMethod::Walld(VarlinkWalldClientRequest::Write { .. }) => {
|
||||||
|
tracing::error!("Received Write request, but walld service is not implemented");
|
||||||
|
(
|
||||||
|
MethodReply::Error(VarlinkReplyError::Rwhod(
|
||||||
|
VarlinkRwhodClientError::InvalidRequest,
|
||||||
|
)),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user