use std::io::{self, Write};
use std::path::{Path, PathBuf};
use anyhow::Result;
pub struct Paths;
impl Paths {
pub fn config_dir() -> PathBuf {
if let Ok(d) = std::env::var("FILAMENT_CONFIG_DIR") {
return PathBuf::from(d);
}
Self::platform_config_dir()
}
fn platform_config_dir() -> PathBuf {
if let Some(proj) = directories::ProjectDirs::from("", "", "filament") {
return proj.config_dir().to_path_buf();
}
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".config").join("filament")
}
pub fn config_path(relative: impl AsRef<Path>) -> PathBuf {
Self::config_dir().join(relative)
}
pub fn migrate_legacy() {
let legacy = PathBuf::from(".config/filament");
if !legacy.is_dir() {
return;
}
let target = Self::config_dir();
if target == legacy || target.exists() {
return;
}
let _ = std::fs::create_dir_all(&target);
if let Ok(entries) = std::fs::read_dir(&legacy) {
for e in entries.flatten() {
let dest = target.join(e.file_name());
let _ = std::fs::copy(e.path(), &dest);
}
}
}
pub fn home_dir() -> PathBuf {
#[cfg(unix)]
{
if let Ok(h) = std::env::var("HOME") {
if !h.is_empty() {
return PathBuf::from(h);
}
}
}
#[cfg(windows)]
{
if let Ok(h) = std::env::var("USERPROFILE") {
if !h.is_empty() {
return PathBuf::from(h);
}
}
}
PathBuf::from(".")
}
pub fn shell_argv(shell_program: Option<&str>, shell_config: Option<&str>, shell_user: Option<&str>) -> (Vec<String>, bool) {
let shell = shell_program
.map(|s| s.to_string())
.or_else(|| std::env::var("FILAMENT_SHELL").ok().filter(|s| !s.is_empty()))
.or_else(|| shell_config.map(|s| s.to_string()))
.unwrap_or_else(|| Self::default_shell());
let parts: Vec<String> = shell.split_whitespace().map(|s| s.to_string()).collect();
#[cfg(unix)]
{
let argv = match shell_user {
Some(user) => vec!["runuser".into(), "-l".into(), user.into()],
None => parts,
};
(argv, true)
}
#[cfg(windows)]
{
(parts, false)
}
#[cfg(not(any(unix, windows)))]
{
(parts, true)
}
}
fn default_shell() -> String {
#[cfg(unix)]
{
std::env::var("SHELL").ok().filter(|s| !s.is_empty()).unwrap_or_else(|| {
if Path::new("/bin/bash").exists() { "/bin/bash".into() } else { "/bin/sh".into() }
})
}
#[cfg(windows)]
{
if Path::new("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe").exists() {
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe".into()
} else if Path::new("C:\\Windows\\System32\\cmd.exe").exists() {
"C:\\Windows\\System32\\cmd.exe".into()
} else {
"cmd.exe".into()
}
}
#[cfg(not(any(unix, windows)))]
{
"/bin/sh".into()
}
}
}
pub struct SecretFile;
impl SecretFile {
pub fn write(path: impl AsRef<Path>, data: &[u8]) -> io::Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
SecretFile::write_raw(path, data)?;
SecretFile::restrict(path)?;
Ok(())
}
pub fn write_str(path: impl AsRef<Path>, s: &str) -> io::Result<()> {
SecretFile::write(path, s.as_bytes())
}
pub fn restrict(path: impl AsRef<Path>) -> io::Result<()> {
let path = path.as_ref();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
}
#[cfg(windows)]
{
restrict_dacl(path)?;
}
Ok(())
}
fn write_raw(path: &Path, data: &[u8]) -> io::Result<()> {
let dir = path.parent().unwrap_or(std::path::Path::new("."));
let temp = dir.join(format!("{}.tmp.{}", path.file_name().unwrap_or_default().to_string_lossy(), std::process::id()));
{
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut f = opts.open(&temp)?;
f.write_all(data)?;
f.sync_all()?;
}
std::fs::rename(&temp, path)?;
if let Some(parent) = path.parent() {
if let Ok(dir) = std::fs::OpenOptions::new().read(true).open(parent) {
let _ = dir.sync_all();
}
}
Ok(())
}
}
#[cfg(windows)]
fn restrict_dacl(path: &Path) -> io::Result<()> {
let user = std::env::var("USERNAME").unwrap_or_default();
if user.is_empty() {
return Ok(());
}
let path_str = path.display().to_string();
let _ = std::process::Command::new("icacls")
.args([&path_str, "/inheritance:r", "/grant:r", &format!("{user}:(F)"), "/Q"])
.output();
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceHost {
Systemd,
Launchd,
WindowsService,
None,
}
pub enum InstallResult {
System,
User,
}
impl ServiceHost {
pub fn detect() -> Self {
#[cfg(target_os = "linux")]
{
if Self::has_systemd() { return ServiceHost::Systemd; }
ServiceHost::None
}
#[cfg(target_os = "macos")]
{ ServiceHost::Launchd }
#[cfg(target_os = "windows")]
{ ServiceHost::WindowsService }
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{ ServiceHost::None }
}
pub fn supports_install(&self) -> bool {
!matches!(self, ServiceHost::None)
}
pub fn install_instructions(&self) -> &'static str {
match self {
ServiceHost::Systemd => "",
ServiceHost::Launchd => "On macOS, create a LaunchAgent plist in ~/Library/LaunchAgents/ and load it with launchctl.",
ServiceHost::WindowsService => "On Windows, create a Scheduled Task (trigger: at logon) or register a Service with sc.exe.",
ServiceHost::None => "No service manager detected. Start filament with `filament up` in a terminal, or configure your init system manually.",
}
}
pub fn install_system(&self, exe: &Path, shell_args: &str) -> Result<InstallResult> {
if self.is_elevated() {
self.do_install_system(exe, shell_args)?;
return Ok(InstallResult::System);
}
let elevated = self.try_elevate(exe, shell_args)?;
if elevated {
return Ok(InstallResult::System);
}
Err(anyhow::anyhow!("elevation declined"))
}
fn is_elevated(&self) -> bool {
#[cfg(unix)]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(windows)]
{
true }
#[cfg(not(any(unix, windows)))]
{ false }
}
fn do_install_system(&self, exe: &Path, shell_args: &str) -> Result<()> {
match self {
#[cfg(target_os = "linux")]
ServiceHost::Systemd => {
let unit = std::path::Path::new("/etc/systemd/system/filament.service");
std::fs::write(unit, format!(
"[Unit]\nDescription=Filament drop target\nAfter=network-online.target\n\n[Service]\nType=notify\nExecStart={} up{}\nRestart=always\nRestartSec=2\nWatchdogSec=45\n\n[Install]\nWantedBy=multi-user.target\n",
exe.display(), shell_args
))?;
let _ = std::process::Command::new("systemctl").args(["daemon-reload"]).status();
let _ = std::process::Command::new("systemctl").args(["enable", "--now", "filament"]).status();
}
#[cfg(target_os = "windows")]
ServiceHost::WindowsService => {
let status = std::process::Command::new("sc")
.args(["create", "filament", "binPath=", &format!("\"{}\" up{}", exe.display(), shell_args),
"start=", "auto", "obj=", "LocalSystem"])
.status()?;
if !status.success() {
anyhow::bail!("sc create failed");
}
let _ = std::process::Command::new("sc").args(["start", "filament"]).status();
}
#[cfg(target_os = "macos")]
ServiceHost::Launchd => {
let plist = std::path::Path::new("/Library/LaunchDaemons/autumated.filament.plist");
std::fs::write(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>autumated.filament</string>
<key>ProgramArguments</key>
<array><string>{}</string><string>up</string>{}</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict>
</plist>"#,
exe.display(), shell_args
))?;
let _ = std::process::Command::new("launchctl").args(["bootstrap", "system"]).arg(plist).status();
}
_ => anyhow::bail!("system install not supported"),
}
Ok(())
}
pub fn install_user(&self, exe: &Path, shell_args: &str) -> Result<()> {
match self {
#[cfg(target_os = "linux")]
ServiceHost::Systemd => {
install_systemd_user(exe, shell_args)
}
#[cfg(target_os = "windows")]
ServiceHost::WindowsService => {
install_scheduled_task(exe, shell_args)
}
#[cfg(target_os = "macos")]
ServiceHost::Launchd => {
install_launch_agent(exe, shell_args)
}
_ => Err(anyhow::anyhow!("no service manager detected")),
}
}
pub fn uninstall(&self) {
match self {
#[cfg(target_os = "linux")]
ServiceHost::Systemd => {
let _ = std::process::Command::new("systemctl")
.args(["--user", "disable", "--now", "filament"])
.status();
let _ = std::process::Command::new("systemctl")
.args(["disable", "--now", "filament"])
.status();
}
#[cfg(target_os = "windows")]
ServiceHost::WindowsService => {
let _ = std::process::Command::new("sc")
.args(["delete", "filament"])
.status();
let _ = std::process::Command::new("schtasks")
.args(["/delete", "/tn", "Filament", "/f"])
.status();
}
#[cfg(target_os = "macos")]
ServiceHost::Launchd => {
let _ = std::process::Command::new("launchctl")
.args(["bootout", "gui/501/autumated.filament"])
.status();
}
_ => {}
}
}
fn try_elevate(&self, exe: &Path, shell_args: &str) -> Result<bool> {
#[cfg(target_os = "linux")]
{
let ok = std::process::Command::new("pkexec")
.arg(exe)
.args(["--install-system", shell_args])
.status()
.map(|s| s.success())
.unwrap_or(false);
return Ok(ok);
}
#[cfg(target_os = "windows")]
{
use std::os::windows::ffi::OsStrExt;
unsafe extern "system" {
fn ShellExecuteW(
hwnd: isize,
lpOperation: *const u16,
lpFile: *const u16,
lpParameters: *const u16,
lpDirectory: *const u16,
nShowCmd: i32,
) -> isize;
fn GetLastError() -> u32;
}
const SW_HIDE: i32 = 0;
let exe_win: Vec<u16> = exe.as_os_str().encode_wide().chain(std::iter::once(0)).collect();
let args = format!("--install-system {shell_args}");
let args_win: Vec<u16> = args.encode_utf16().chain(std::iter::once(0)).collect();
let verb: Vec<u16> = "runas\0".encode_utf16().collect();
let ret = unsafe {
ShellExecuteW(
0, verb.as_ptr(),
exe_win.as_ptr(),
args_win.as_ptr(),
std::ptr::null(),
SW_HIDE,
)
};
if ret as usize > 32 {
return Ok(true);
}
let code = unsafe { GetLastError() };
if code == 1223 {
return Ok(false);
}
anyhow::bail!("ShellExecuteW failed: {}", code);
}
#[cfg(target_os = "macos")]
{
let esc = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\"");
let script = format!(
"do shell script \"'{}' --install-system {}\" with administrator privileges",
esc(&exe.display().to_string()),
esc(shell_args)
);
let ok = std::process::Command::new("osascript")
.args(["-e", &script])
.status()
.map(|s| s.success())
.unwrap_or(false);
return Ok(ok);
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{ Ok(false) }
}
#[cfg(target_os = "linux")]
fn has_systemd() -> bool {
Path::new("/run/systemd/system").is_dir()
}
}
#[cfg(target_os = "linux")]
fn install_systemd_user(exe: &Path, shell_args: &str) -> Result<()> {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
let unit_dir = PathBuf::from(&home).join(".config/systemd/user");
std::fs::create_dir_all(&unit_dir)?;
let unit = unit_dir.join("filament.service");
std::fs::write(&unit, format!(
"[Unit]\nDescription=Filament drop target (trusted devices only)\nAfter=network-online.target\n\n[Service]\nType=notify\nExecStart={} up{}\nRestart=always\nRestartSec=2\nWatchdogSec=45\n\n[Install]\nWantedBy=default.target\n",
exe.display(), shell_args
))?;
let ok = std::process::Command::new("systemctl").args(["--user", "daemon-reload"]).status()
.and_then(|_| std::process::Command::new("systemctl").args(["--user", "enable", "--now", "filament"]).status())
.map(|s| s.success()).unwrap_or(false);
if !ok {
anyhow::bail!("systemctl --user enable --now filament failed; run it manually or check journalctl --user -u filament");
}
Ok(())
}
#[cfg(target_os = "windows")]
fn install_scheduled_task(exe: &Path, shell_args: &str) -> Result<()> {
let task_xml = format!(
r#"<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers><LogonTrigger/></Triggers>
<Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType></Principal></Principals>
<Actions><Exec><Command>{}</Command><Arguments>up{}</Arguments></Exec></Actions>
</Task>"#,
exe.display(), shell_args
);
let tmp = std::env::temp_dir().join("filament-task.xml");
std::fs::write(&tmp, &task_xml)?;
let out = std::process::Command::new("schtasks")
.args(["/create", "/tn", "Filament", "/xml", &tmp.to_string_lossy(), "/f"])
.output()?;
let _ = std::fs::remove_file(&tmp);
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
anyhow::bail!("schtasks failed: {}", stderr.trim());
}
Ok(())
}
#[cfg(target_os = "macos")]
fn install_launch_agent(exe: &Path, shell_args: &str) -> Result<()> {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
let dir = PathBuf::from(&home).join("Library/LaunchAgents");
std::fs::create_dir_all(&dir)?;
let plist = dir.join("autumated.filament.plist");
std::fs::write(&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>autumated.filament</string>
<key>ProgramArguments</key>
<array><string>{}</string><string>up</string>{}</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict>
</plist>"#,
exe.display(), shell_args
))?;
let _ = std::process::Command::new("launchctl").args(["bootstrap", "gui/501", &plist.to_string_lossy()]).status();
Ok(())
}
#[cfg(target_os = "windows")]
pub fn add_firewall_rule(exe: &Path) {
let _ = std::process::Command::new("netsh")
.args(["advfirewall", "firewall", "add", "rule",
"name=Filament QUIC", "dir=in", "action=allow",
"protocol=udp",
"program=", &exe.display().to_string(),
"enable=yes"])
.output();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstallSource {
Homebrew,
Winget,
Scoop,
Cargo,
SelfInstalled,
}
impl InstallSource {
pub fn detect() -> Self {
let path = match std::env::current_exe() {
Ok(p) => match p.canonicalize() {
Ok(c) => c,
Err(_) => p,
},
Err(_) => return InstallSource::SelfInstalled,
};
Self::classify(&path)
}
fn classify(path: &Path) -> Self {
let s = path.to_string_lossy().to_lowercase();
if s.contains("/cellar/") || s.contains("/homebrew/") || s.contains("/linuxbrew/") {
return InstallSource::Homebrew;
}
if s.contains("\\microsoft\\winget\\") || s.contains("/microsoft/winget/") {
return InstallSource::Winget;
}
if s.contains("\\scoop\\apps\\") || s.contains("/scoop/apps/") {
return InstallSource::Scoop;
}
if s.contains("/.cargo/bin") || s.contains("\\.cargo\\bin") {
return InstallSource::Cargo;
}
InstallSource::SelfInstalled
}
pub fn upgrade_hint(&self) -> &'static str {
match self {
InstallSource::Homebrew => "brew upgrade filament",
InstallSource::Winget => "winget upgrade Abdk4Moura.Filament",
InstallSource::Scoop => "scoop update filament",
InstallSource::Cargo => "cargo install filament-cli",
InstallSource::SelfInstalled => "",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ShellKind {
Posix,
PowerShell,
Cmd,
}
pub struct ShellHost {
argv: Vec<String>,
kind: ShellKind,
}
impl ShellHost {
pub fn new(shell_argv: &[String]) -> Self {
let binary = shell_argv.first().map(|s| s.as_str()).unwrap_or("");
let name = Path::new(binary)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_lowercase();
let kind = if name.contains("pwsh") || name.contains("powershell") {
ShellKind::PowerShell
} else if name == "cmd.exe" || name == "cmd" {
ShellKind::Cmd
} else {
ShellKind::Posix
};
ShellHost {
argv: shell_argv.to_vec(),
kind,
}
}
pub fn interactive_args(&self) -> Vec<String> {
let mut args = self.argv.clone();
match self.kind {
ShellKind::Posix => {
if !args.iter().any(|a| a == "-l" || a == "--login") {
args.push("-l".into());
}
args
}
_ => args,
}
}
pub fn exec_cmd_args(&self, cmd: &str) -> Vec<String> {
let mut args = vec![self.argv[0].clone()];
match self.kind {
ShellKind::Posix => {
args.push("-c".into());
args.push(cmd.to_string());
}
ShellKind::PowerShell => {
args.push("-Command".into());
args.push(cmd.to_string());
}
ShellKind::Cmd => {
args.push("/c".into());
args.push(cmd.to_string());
}
}
args
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_returns_a_valid_variant() {
let host = ServiceHost::detect();
assert!(
matches!(
host,
ServiceHost::Systemd
| ServiceHost::Launchd
| ServiceHost::WindowsService
| ServiceHost::None
),
"detect returned a valid variant: {host:?}"
);
}
#[test]
fn all_detected_hosts_support_install() {
assert!(ServiceHost::Systemd.supports_install());
assert!(ServiceHost::Launchd.supports_install());
assert!(ServiceHost::WindowsService.supports_install());
assert!(!ServiceHost::None.supports_install());
}
#[test]
fn install_instructions_non_empty_when_no_backend() {
assert!(!ServiceHost::Launchd.install_instructions().is_empty());
assert!(!ServiceHost::WindowsService.install_instructions().is_empty());
assert!(!ServiceHost::None.install_instructions().is_empty());
assert!(ServiceHost::Systemd.install_instructions().is_empty());
}
#[test]
fn install_source_classify_brew() {
let p = Path::new("/opt/homebrew/Cellar/filament/0.4.1/bin/filament");
assert_eq!(InstallSource::classify(p), InstallSource::Homebrew);
let p2 = Path::new("/home/linuxbrew/.linuxbrew/bin/filament");
assert_eq!(InstallSource::classify(p2), InstallSource::Homebrew);
let p3 = Path::new("/usr/local/Cellar/filament/0.3.1/bin/filament");
assert_eq!(InstallSource::classify(p3), InstallSource::Homebrew);
}
#[test]
fn install_source_classify_winget() {
let p = Path::new("C:\\Users\\kabir\\AppData\\Local\\Microsoft\\WinGet\\Packages\\Abdk4Moura.Filament_filament\\filament.exe");
assert_eq!(InstallSource::classify(p), InstallSource::Winget);
}
#[test]
fn install_source_classify_scoop() {
let p = Path::new("C:\\Users\\kabir\\scoop\\apps\\filament\\0.4.1\\filament.exe");
assert_eq!(InstallSource::classify(p), InstallSource::Scoop);
}
#[test]
fn install_source_classify_cargo() {
let p = Path::new("/home/kabir/.cargo/bin/filament");
assert_eq!(InstallSource::classify(p), InstallSource::Cargo);
}
#[test]
fn install_source_classify_self_installed() {
let p = Path::new("/home/kabir/.local/bin/filament");
assert_eq!(InstallSource::classify(p), InstallSource::SelfInstalled);
}
#[test]
fn install_source_upgrade_hints() {
assert_eq!(InstallSource::Homebrew.upgrade_hint(), "brew upgrade filament");
assert_eq!(InstallSource::Winget.upgrade_hint(), "winget upgrade Abdk4Moura.Filament");
assert_eq!(InstallSource::Cargo.upgrade_hint(), "cargo install filament-cli");
assert_eq!(InstallSource::SelfInstalled.upgrade_hint(), "");
}
#[test]
fn shell_host_interactive_posix_adds_login() {
let sh = ShellHost::new(&["/bin/bash".into()]);
assert!(sh.interactive_args().contains(&"-l".into()));
}
#[test]
fn shell_host_exec_posix_uses_minus_c() {
let sh = ShellHost::new(&["bash".into()]);
let args = sh.exec_cmd_args("echo hi");
assert_eq!(args[0], "bash");
assert_eq!(args[1], "-c");
assert_eq!(args[2], "echo hi");
}
#[test]
fn shell_host_exec_powershell_uses_command_flag() {
let sh = ShellHost::new(&["pwsh.exe".into()]);
let args = sh.exec_cmd_args("Get-Date");
assert_eq!(args[1], "-Command");
assert_eq!(args[2], "Get-Date");
}
#[test]
fn shell_host_exec_cmd_uses_slash_c() {
let sh = ShellHost::new(&["cmd.exe".into()]);
let args = sh.exec_cmd_args("dir");
assert_eq!(args[1], "/c");
assert_eq!(args[2], "dir");
}
#[test]
fn shell_host_preserves_shell_argv_prefix() {
let sh = ShellHost::new(&["bash".into(), "-l".into(), "-i".into()]);
let args = sh.exec_cmd_args("echo x");
assert_eq!(args[0], "bash");
assert_eq!(args[1], "-c");
assert_eq!(args[2], "echo x");
}
}