Skip to content
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ include = [
"Cargo.toml",
"Cargo.lock",
"README.md",
"docs/install.md",
"docs/cli.md",
"docs/troubleshooting.md",
"LICENSE",
"NOTICE",
"config.example.toml",
Expand Down
7 changes: 7 additions & 0 deletions scripts/build-release-bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ mkdir -p \
"$stage_dir/$bundle_name/share/bash-completion/completions" \
"$stage_dir/$bundle_name/share/zsh/site-functions" \
"$stage_dir/$bundle_name/share/fish/vendor_completions.d" \
"$stage_dir/$bundle_name/share/doc/whispers/docs" \
"$stage_dir/$bundle_name/share/doc/whispers" \
"$stage_dir/$bundle_name/share/licenses/whispers"

Expand All @@ -65,6 +66,12 @@ install -Dm755 "${target_dir}/whispers-rewrite-worker" \

install -Dm644 README.md \
"$stage_dir/$bundle_name/share/doc/whispers/README.md"
install -Dm644 docs/install.md \
"$stage_dir/$bundle_name/share/doc/whispers/docs/install.md"
install -Dm644 docs/cli.md \
"$stage_dir/$bundle_name/share/doc/whispers/docs/cli.md"
install -Dm644 docs/troubleshooting.md \
"$stage_dir/$bundle_name/share/doc/whispers/docs/troubleshooting.md"
install -Dm644 config.example.toml \
"$stage_dir/$bundle_name/share/doc/whispers/config.example.toml"
install -Dm644 LICENSE \
Expand Down
2 changes: 2 additions & 0 deletions src/inject/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ mod preflight;
mod tests;

use crate::error::{Result, WhsprError};
#[cfg(test)]
pub(crate) use preflight::InjectionReadinessIssue;
pub use preflight::{InjectionReadinessReport, validate_injection_prerequisites};

const DEVICE_READY_DELAY: std::time::Duration = std::time::Duration::from_millis(120);
Expand Down
14 changes: 10 additions & 4 deletions src/inject/preflight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ impl InjectionReadinessIssue {
"Log out and back in after changing group membership.".into(),
],
Self::UinputUnavailable(_) => {
vec!["Check that `/dev/uinput` exists and is writable by the current user.".into()]
vec![
"Check that `/dev/uinput` exists and is readable and writable by the current user."
.into(),
]
}
}
}
Expand All @@ -141,7 +144,7 @@ impl InjectionReadinessIssue {
"`/dev/uinput` is missing; load the `uinput` kernel module".into()
}
Self::UinputPermissionDenied => {
"`/dev/uinput` is present but not writable by the current user; create a dedicated `uinput` group, add your user to it, install a `udev` rule, then log out and back in".into()
"`/dev/uinput` is present but not readable and writable by the current user; create a dedicated `uinput` group, add your user to it, install a `udev` rule, then log out and back in".into()
}
Self::UinputUnavailable(detail) => {
format!("`/dev/uinput` could not be opened: {detail}")
Expand All @@ -159,12 +162,15 @@ pub fn validate_injection_prerequisites() -> Result<()> {
}

fn probe_uinput() -> Option<InjectionReadinessIssue> {
let path = Path::new(UINPUT_PATH);
probe_uinput_path(Path::new(UINPUT_PATH))
}

pub(super) fn probe_uinput_path(path: &Path) -> Option<InjectionReadinessIssue> {
if !path.exists() {
return Some(InjectionReadinessIssue::MissingUinputDevice);
}

match OpenOptions::new().write(true).open(path) {
match OpenOptions::new().read(true).write(true).open(path) {
Ok(_) => None,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
Some(InjectionReadinessIssue::UinputPermissionDenied)
Expand Down
23 changes: 22 additions & 1 deletion src/inject/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::os::unix::fs::PermissionsExt;

use crate::error::WhsprError;

use super::preflight::InjectionReadinessIssue;
Expand All @@ -19,7 +21,7 @@ fn run_wl_copy_reports_spawn_failure() {
fn run_wl_copy_reports_non_zero_exit() {
let err = clipboard::run_wl_copy(
"/bin/sh",
&[String::from("-c"), String::from("exit 7")],
&[String::from("-c"), String::from("cat >/dev/null; exit 7")],
"hello",
)
.expect_err("non-zero exit should fail");
Expand Down Expand Up @@ -112,3 +114,22 @@ fn readiness_report_only_requires_relogin_when_permission_is_last_blocker() {
]);
assert!(!additional_steps.only_requires_relogin());
}

#[test]
fn probe_uinput_requires_read_and_write_access() {
let dir = crate::test_support::unique_temp_dir("uinput-readwrite-probe");
let path = dir.join("uinput");
std::fs::write(&path, []).expect("create probe file");

let mut perms = std::fs::metadata(&path).expect("metadata").permissions();
perms.set_mode(0o200);
std::fs::set_permissions(&path, perms).expect("set write-only permissions");

let issue = preflight::probe_uinput_path(&path).expect("write-only file should fail");
assert_eq!(issue, InjectionReadinessIssue::UinputPermissionDenied);

let mut cleanup_perms = std::fs::metadata(&path).expect("metadata").permissions();
cleanup_perms.set_mode(0o600);
std::fs::set_permissions(&path, cleanup_perms).expect("restore permissions");
std::fs::remove_dir_all(&dir).expect("cleanup temp dir");
}
6 changes: 6 additions & 0 deletions src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ impl CloudSetup {
}
}

fn validate_setup_invoker(uid: libc::uid_t) -> Result<()> {
side_effects::validate_setup_user(uid)
}

pub async fn run_setup(config_path_override: Option<&Path>) -> Result<()> {
validate_setup_invoker(unsafe { libc::geteuid() })?;

let ui = SetupUi::new();
ui.print_header("whispers setup");
ui.blank();
Expand Down
36 changes: 28 additions & 8 deletions src/setup/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,37 @@ pub(super) fn print_injection_readiness(
println!(" - {line}");
}

if setup.changed_groups {
ui.print_info(
"If you were just added to the `uinput` group, log out and back in before testing.",
);
if let Some(message) = injection_readiness_info_message(readiness, setup) {
ui.print_info(message);
}

for line in readiness.fix_lines() {
for line in injection_readiness_fix_lines(readiness, setup) {
println!(" - {line}");
}
}

pub(super) fn injection_readiness_info_message(
readiness: &InjectionReadinessReport,
setup: &InjectionSetupOutcome,
) -> Option<&'static str> {
if setup.can_finish_with_relogin_only(readiness.only_requires_relogin()) {
Some("Log out and back in before testing.")
} else {
setup.report_group_change_message()
}
}

pub(super) fn injection_readiness_fix_lines(
readiness: &InjectionReadinessReport,
setup: &InjectionSetupOutcome,
) -> Vec<String> {
if setup.can_finish_with_relogin_only(readiness.only_requires_relogin()) {
Vec::new()
} else {
readiness.fix_lines()
}
}

pub(super) fn print_setup_complete(
ui: &SetupUi,
readiness: &InjectionReadinessReport,
Expand All @@ -109,7 +129,7 @@ pub(super) fn print_setup_complete(
setup_complete_message(
readiness.is_ready(),
setup.changed_groups,
readiness.only_requires_relogin(),
setup.can_finish_with_relogin_only(readiness.only_requires_relogin()),
)
);
ui.print_section("Example keybind");
Expand All @@ -120,11 +140,11 @@ pub(super) fn print_setup_complete(
pub(super) fn setup_complete_message(
is_ready: bool,
changed_groups: bool,
only_requires_relogin: bool,
can_finish_with_relogin_only: bool,
) -> &'static str {
if is_ready {
"You can now use whispers."
} else if changed_groups && only_requires_relogin {
} else if can_finish_with_relogin_only {
"Log out and back in, then use whispers."
} else if changed_groups {
"Log out and back in, then finish any remaining paste injection steps above before using whispers."
Expand Down
Loading