use std::{
fs::{remove_file, File},
io::{self, Write},
path::{Path, PathBuf},
};
use async_trait::async_trait;
use crate::{
constants::APPLICATION_NAME,
log,
state::LauncherPaths,
util::{
command::capture_command_and_check_status,
errors::{wrap, AnyError, CodeError, MissingHomeDirectory},
},
};
use super::{service::tail_log_file, ServiceManager};
pub struct LaunchdService {
log: log::Logger,
log_file: PathBuf,
}
impl LaunchdService {
pub fn new(log: log::Logger, paths: &LauncherPaths) -> Self {
Self {
log,
log_file: paths.service_log_file(),
}
}
}
#[async_trait]
impl ServiceManager for LaunchdService {
async fn register(
&self,
exe: std::path::PathBuf,
args: &[&str],
) -> Result<(), crate::util::errors::AnyError> {
let service_file = get_service_file_path()?;
write_service_file(&service_file, &self.log_file, exe, args)
.map_err(|e| wrap(e, "error creating service file"))?;
info!(self.log, "Successfully registered service...");
capture_command_and_check_status(
"launchctl",
&["load", service_file.as_os_str().to_string_lossy().as_ref()],
)
.await?;
capture_command_and_check_status("launchctl", &["start", &get_service_label()]).await?;
info!(self.log, "Tunnel service successfully started");
Ok(())
}
async fn show_logs(&self) -> Result<(), AnyError> {
tail_log_file(&self.log_file).await
}
async fn run(
self,
launcher_paths: crate::state::LauncherPaths,
mut handle: impl 'static + super::ServiceContainer,
) -> Result<(), crate::util::errors::AnyError> {
handle.run_service(self.log, launcher_paths).await
}
async fn is_installed(&self) -> Result<bool, AnyError> {
let cmd = capture_command_and_check_status("launchctl", &["list"]).await?;
Ok(String::from_utf8_lossy(&cmd.stdout).contains(&get_service_label()))
}
async fn unregister(&self) -> Result<(), crate::util::errors::AnyError> {
let service_file = get_service_file_path()?;
match capture_command_and_check_status("launchctl", &["stop", &get_service_label()]).await {
Ok(_) => {}
Err(CodeError::CommandFailed { code: 3, .. }) => {}
Err(e) => return Err(wrap(e, "error stopping service").into()),
};
info!(self.log, "Successfully stopped service...");
capture_command_and_check_status(
"launchctl",
&[
"unload",
service_file.as_os_str().to_string_lossy().as_ref(),
],
)
.await?;
info!(self.log, "Tunnel service uninstalled");
if let Ok(f) = get_service_file_path() {
remove_file(f).ok();
}
Ok(())
}
}
fn get_service_label() -> String {
format!("com.visualstudio.{}.tunnel", APPLICATION_NAME)
}
fn get_service_file_path() -> Result<PathBuf, MissingHomeDirectory> {
match dirs::home_dir() {
Some(mut d) => {
d.push(format!("{}.plist", get_service_label()));
Ok(d)
}
None => Err(MissingHomeDirectory()),
}
}
fn write_service_file(
path: &PathBuf,
log_file: &Path,
exe: std::path::PathBuf,
args: &[&str],
) -> io::Result<()> {
let mut f = File::create(path)?;
let log_file = log_file.as_os_str().to_string_lossy();
write!(
&mut f,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n\
<dict>\n\
<key>Label</key>\n\
<string>{}</string>\n\
<key>LimitLoadToSessionType</key>\n\
<string>Aqua</string>\n\
<key>ProgramArguments</key>\n\
<array>\n\
<string>{}</string>\n\
<string>{}</string>\n\
</array>\n\
<key>KeepAlive</key>\n\
<true/>\n\
<key>StandardErrorPath</key>\n\
<string>{}</string>\n\
<key>StandardOutPath</key>\n\
<string>{}</string>\n\
</dict>\n\
</plist>",
get_service_label(),
exe.into_os_string().to_string_lossy(),
args.join("</string><string>"),
log_file,
log_file
)?;
Ok(())
}