/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ use std::{ fs::{remove_file, File}, io::{self, Write}, path::{Path, PathBuf}, }; use async_trait::async_trait; use tokio::sync::mpsc; use crate::{ commands::tunnels::ShutdownSignal, constants::APPLICATION_NAME, log, state::LauncherPaths, util::{ command::capture_command_and_check_status, errors::{wrap, AnyError, 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> { let (tx, rx) = mpsc::unbounded_channel::(); tokio::spawn(async move { tokio::signal::ctrl_c().await.ok(); tx.send(ShutdownSignal::CtrlC).ok(); }); handle.run_service(self.log, launcher_paths, rx).await } 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(_) => {} // status 3 == "no such process" Err(AnyError::CommandFailed(e)) if e.output.status.code() == Some(3) => {} Err(e) => return Err(e), }; 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 { 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(); // todo: we may be able to skip file logging and use the ASL instead // if/when we no longer need to support older macOS versions. write!( &mut f, "\n\ \n\ \n\ \n\ Label\n\ {}\n\ LimitLoadToSessionType\n\ Aqua\n\ ProgramArguments\n\ \n\ {}\n\ {}\n\ \n\ KeepAlive\n\ \n\ StandardErrorPath\n\ {}\n\ StandardOutPath\n\ {}\n\ \n\ ", get_service_label(), exe.into_os_string().to_string_lossy(), args.join(""), log_file, log_file )?; Ok(()) }