use crate::libs::messages::Message;
#[cfg(target_os = "windows")]
use crate::msg_debug;
use crate::msg_error_anyhow;
use crate::msg_info;
use anyhow::Result;
#[cfg(target_os = "windows")]
use std::env;
#[cfg(target_os = "windows")]
mod windows {
use super::*;
use std::os::windows::process::CommandExt;
use std::process::Command;
const TASK_NAME: &str = "KaslAutostart";
const CREATE_NO_WINDOW: u32 = 0x08000000;
pub(crate) fn decode_windows_output(bytes: &[u8]) -> String {
if let Ok(utf8) = String::from_utf8(bytes.to_vec()) {
return utf8;
}
encoding_rs::WINDOWS_1252.decode(bytes).0.into_owned()
}
pub fn enable() -> Result<()> {
let exe_path = env::current_exe()?;
let exe_path_str = exe_path.to_string_lossy();
msg_debug!(format!("Creating scheduled task for: {}", exe_path_str));
let _ = Command::new("schtasks")
.args(["/Delete", "/TN", TASK_NAME, "/F"])
.creation_flags(CREATE_NO_WINDOW)
.output();
let output = Command::new("schtasks")
.args([
"/Create", "/SC",
"ONLOGON", "/TN",
TASK_NAME, "/TR",
&format!("\"{}\" watch", exe_path_str), "/RL",
"LIMITED", "/F", ])
.creation_flags(CREATE_NO_WINDOW)
.output()?;
if output.status.success() {
msg_info!(Message::AutostartEnabled);
Ok(())
} else {
let error = decode_windows_output(&output.stderr);
let stdout = decode_windows_output(&output.stdout);
msg_debug!(format!("schtasks stderr: {}", error));
msg_debug!(format!("schtasks stdout: {}", stdout));
if error.contains("Access is denied") || error.contains("0x80070005") {
Err(msg_error_anyhow!(Message::AutostartRequiresAdmin))
} else {
Err(msg_error_anyhow!(Message::AutostartEnableFailed(error)))
}
}
}
pub fn disable() -> Result<()> {
msg_debug!("Removing scheduled task");
let output = Command::new("schtasks")
.args(["/Delete", "/TN", TASK_NAME, "/F"])
.creation_flags(CREATE_NO_WINDOW)
.output()?;
if output.status.success() {
msg_info!(Message::AutostartDisabled);
Ok(())
} else {
let error = decode_windows_output(&output.stderr);
if error.contains("cannot find") || error.contains("does not exist") {
msg_info!(Message::AutostartAlreadyDisabled);
Ok(())
} else if error.contains("Access is denied") || error.contains("0x80070005") {
Err(msg_error_anyhow!(Message::AutostartRequiresAdmin))
} else {
Err(msg_error_anyhow!(Message::AutostartDisableFailed(error)))
}
}
}
pub fn is_enabled() -> Result<bool> {
let output = Command::new("schtasks")
.args(["/Query", "/TN", TASK_NAME, "/FO", "CSV"])
.creation_flags(CREATE_NO_WINDOW)
.output()?;
Ok(output.status.success())
}
pub fn is_admin() -> bool {
use std::ptr;
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
use winapi::um::securitybaseapi::GetTokenInformation;
use winapi::um::winnt::{TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation};
unsafe {
let mut token = ptr::null_mut();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
return false;
}
let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };
let mut size = 0;
let result = GetTokenInformation(
token,
TokenElevation,
&mut elevation as *mut _ as *mut _,
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
&mut size,
);
CloseHandle(token);
result != 0 && elevation.TokenIsElevated != 0
}
}
}
#[cfg(not(target_os = "windows"))]
mod unix {
use super::*;
use std::fs;
#[cfg(not(target_os = "macos"))]
use std::os::unix::fs::symlink;
use std::path::PathBuf;
use std::process::Command;
#[cfg(target_os = "macos")]
const LAUNCH_AGENT_LABEL: &str = "com.lacodda.kasl";
#[cfg(not(target_os = "macos"))]
const SYSTEMD_UNIT: &str = "kasl.service";
fn exe_path() -> Result<String> {
Ok(std::env::current_exe()?.to_string_lossy().to_string())
}
#[cfg(target_os = "macos")]
fn plist_path() -> Result<PathBuf> {
let home = std::env::var("HOME").map_err(|_| msg_error_anyhow!(Message::AutostartEnableFailed("HOME is not set".into())))?;
Ok(PathBuf::from(home).join("Library/LaunchAgents").join(format!("{}.plist", LAUNCH_AGENT_LABEL)))
}
#[cfg(not(target_os = "macos"))]
fn unit_path() -> Result<PathBuf> {
let base = match std::env::var("XDG_CONFIG_HOME") {
Ok(dir) if !dir.is_empty() => PathBuf::from(dir),
_ => {
let home = std::env::var("HOME").map_err(|_| msg_error_anyhow!(Message::AutostartEnableFailed("HOME is not set".into())))?;
PathBuf::from(home).join(".config")
}
};
Ok(base.join("systemd/user").join(SYSTEMD_UNIT))
}
#[cfg(target_os = "macos")]
pub fn enable() -> Result<()> {
let path = plist_path()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{label}</string>
<key>ProgramArguments</key>
<array>
<string>{exe}</string>
<string>watch</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
"#,
label = LAUNCH_AGENT_LABEL,
exe = exe_path()?
);
fs::write(&path, plist)?;
let _ = Command::new("launchctl").arg("load").arg(&path).output();
msg_info!(Message::AutostartEnabledUser);
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub fn enable() -> Result<()> {
let path = unit_path()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let unit = format!(
"[Unit]
Description=kasl activity monitoring
After=graphical-session.target
[Service]
Type=simple
ExecStart={exe} watch --foreground
Restart=on-failure
[Install]
WantedBy=default.target
",
exe = exe_path()?
);
fs::write(&path, unit)?;
let wants_dir = path
.parent()
.ok_or_else(|| msg_error_anyhow!(Message::AutostartEnableFailed("cannot resolve systemd user directory".into())))?
.join("default.target.wants");
fs::create_dir_all(&wants_dir)?;
let link = wants_dir.join(SYSTEMD_UNIT);
let _ = fs::remove_file(&link);
symlink(&path, &link)?;
let _ = Command::new("systemctl").args(["--user", "daemon-reload"]).output();
msg_info!(Message::AutostartEnabledUser);
Ok(())
}
#[cfg(target_os = "macos")]
pub fn disable() -> Result<()> {
let path = plist_path()?;
if !path.exists() {
msg_info!(Message::AutostartAlreadyDisabled);
return Ok(());
}
let _ = Command::new("launchctl").arg("unload").arg(&path).output();
fs::remove_file(&path)?;
msg_info!(Message::AutostartDisabled);
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub fn disable() -> Result<()> {
let path = unit_path()?;
if !path.exists() {
msg_info!(Message::AutostartAlreadyDisabled);
return Ok(());
}
if let Some(parent) = path.parent() {
let _ = fs::remove_file(parent.join("default.target.wants").join(SYSTEMD_UNIT));
}
fs::remove_file(&path)?;
let _ = Command::new("systemctl").args(["--user", "daemon-reload"]).output();
msg_info!(Message::AutostartDisabled);
Ok(())
}
#[cfg(target_os = "macos")]
pub fn is_enabled() -> Result<bool> {
Ok(plist_path().map(|path| path.exists()).unwrap_or(false))
}
#[cfg(not(target_os = "macos"))]
pub fn is_enabled() -> Result<bool> {
Ok(unit_path().map(|path| path.exists()).unwrap_or(false))
}
#[allow(dead_code)] pub fn is_admin() -> bool {
false
}
}
pub fn enable() -> Result<()> {
#[cfg(target_os = "windows")]
{
if !windows::is_admin() {
msg_info!(Message::AutostartCheckingAlternative);
return enable_user_autostart();
}
windows::enable()
}
#[cfg(not(target_os = "windows"))]
return unix::enable();
}
#[cfg(target_os = "windows")]
fn enable_user_autostart() -> Result<()> {
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let exe_path = env::current_exe()?;
let exe_path_str = exe_path.to_string_lossy();
msg_debug!("Trying user-level autostart via Registry");
let output = Command::new("reg")
.args([
"add",
r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run",
"/v",
"Kasl", "/t",
"REG_SZ", "/d",
&format!("\"{}\" watch", exe_path_str), "/f", ])
.creation_flags(CREATE_NO_WINDOW)
.output()?;
if output.status.success() {
msg_info!(Message::AutostartEnabledUser);
Ok(())
} else {
let error = windows::decode_windows_output(&output.stderr);
Err(msg_error_anyhow!(Message::AutostartEnableFailed(error)))
}
}
pub fn disable() -> Result<()> {
#[cfg(target_os = "windows")]
{
let _ = windows::disable();
let _ = disable_user_autostart();
Ok(())
}
#[cfg(not(target_os = "windows"))]
return unix::disable();
}
#[cfg(target_os = "windows")]
fn disable_user_autostart() -> Result<()> {
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let output = Command::new("reg")
.args([
"delete",
r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run",
"/v",
"Kasl",
"/f", ])
.creation_flags(CREATE_NO_WINDOW)
.output()?;
if output.status.success() {
Ok(())
} else {
let error = windows::decode_windows_output(&output.stderr);
if error.contains("The system cannot find") {
Ok(()) } else {
Err(msg_error_anyhow!(Message::AutostartDisableFailed(error)))
}
}
}
pub fn is_enabled() -> Result<bool> {
#[cfg(target_os = "windows")]
{
if windows::is_enabled().unwrap_or(false) {
return Ok(true);
}
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let output = Command::new("reg")
.args(["query", r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run", "/v", "Kasl"])
.creation_flags(CREATE_NO_WINDOW)
.output()?;
Ok(output.status.success())
}
#[cfg(not(target_os = "windows"))]
return unix::is_enabled();
}
pub fn status() -> Result<String> {
match is_enabled()? {
true => Ok("enabled".to_string()),
false => Ok("disabled".to_string()),
}
}