/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ use std::{ fs::File, io::{self, Write}, path::PathBuf, process::Command, }; use async_trait::async_trait; use zbus::{dbus_proxy, zvariant, Connection}; use crate::{ constants::{APPLICATION_NAME, PRODUCT_NAME_LONG}, log, state::LauncherPaths, util::errors::{wrap, AnyError}, }; use super::ServiceManager; pub struct SystemdService { log: log::Logger, service_file: PathBuf, } impl SystemdService { pub fn new(log: log::Logger, paths: LauncherPaths) -> Self { Self { log, service_file: paths.root().join(SystemdService::service_name_string()), } } } impl SystemdService { async fn connect() -> Result { let connection = Connection::session() .await .map_err(|e| wrap(e, "error creating dbus session"))?; Ok(connection) } async fn proxy(connection: &Connection) -> Result, AnyError> { let proxy = SystemdManagerDbusProxy::new(connection) .await .map_err(|e| { wrap( e, "error connecting to systemd, you may need to re-run with sudo:", ) })?; Ok(proxy) } fn service_path_string(&self) -> String { self.service_file.as_os_str().to_string_lossy().to_string() } fn service_name_string() -> String { format!("{}-tunnel.service", APPLICATION_NAME) } } #[async_trait] impl ServiceManager for SystemdService { async fn register( &self, exe: std::path::PathBuf, args: &[&str], ) -> Result<(), crate::util::errors::AnyError> { let connection = SystemdService::connect().await?; let proxy = SystemdService::proxy(&connection).await?; write_systemd_service_file(&self.service_file, exe, args) .map_err(|e| wrap(e, "error creating service file"))?; proxy .link_unit_files( vec![self.service_path_string()], /* 'runtime only'= */ false, /* replace existing = */ true, ) .await .map_err(|e| wrap(e, "error registering service"))?; info!(self.log, "Successfully registered service..."); // note: enablement is implicit in recent systemd version, but required for older systems // https://github.com/microsoft/vscode/issues/167489#issuecomment-1331222826 proxy .enable_unit_files( vec![SystemdService::service_name_string()], /* 'runtime only'= */ false, /* replace existing = */ true, ) .await .map_err(|e| wrap(e, "error enabling unit files for service"))?; info!(self.log, "Successfully enabled unit files..."); proxy .start_unit(SystemdService::service_name_string(), "replace".to_string()) .await .map_err(|e| wrap(e, "error starting service"))?; info!(self.log, "Tunnel service successfully started"); Ok(()) } 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 show_logs(&self) -> Result<(), AnyError> { // show the systemctl status header... Command::new("systemctl") .args([ "--user", "status", "-n", "0", &SystemdService::service_name_string(), ]) .status() .map(|s| s.code().unwrap_or(1)) .map_err(|e| wrap(e, "error running systemctl"))?; // then follow log files Command::new("journalctl") .args(["--user", "-f", "-u", &SystemdService::service_name_string()]) .status() .map(|s| s.code().unwrap_or(1)) .map_err(|e| wrap(e, "error running journalctl"))?; Ok(()) } async fn unregister(&self) -> Result<(), crate::util::errors::AnyError> { let connection = SystemdService::connect().await?; let proxy = SystemdService::proxy(&connection).await?; proxy .stop_unit(SystemdService::service_name_string(), "replace".to_string()) .await .map_err(|e| wrap(e, "error unregistering service"))?; info!(self.log, "Successfully stopped service..."); proxy .disable_unit_files( vec![SystemdService::service_name_string()], /* 'runtime only'= */ false, ) .await .map_err(|e| wrap(e, "error unregistering service"))?; info!(self.log, "Tunnel service uninstalled"); Ok(()) } } fn write_systemd_service_file( path: &PathBuf, exe: std::path::PathBuf, args: &[&str], ) -> io::Result<()> { let mut f = File::create(path)?; write!( &mut f, "[Unit]\n\ Description={} Tunnel\n\ After=network.target\n\ StartLimitIntervalSec=0\n\ \n\ [Service]\n\ Type=simple\n\ Restart=always\n\ RestartSec=10\n\ ExecStart={} \"{}\"\n\ \n\ [Install]\n\ WantedBy=default.target\n\ ", PRODUCT_NAME_LONG, exe.into_os_string().to_string_lossy(), args.join("\" \"") )?; Ok(()) } /// Minimal implementation of systemd types for the services we need. The full /// definition can be found on any systemd machine with the command: /// /// gdbus introspect --system --dest org.freedesktop.systemd1 --object-path /org/freedesktop/systemd1 /// /// See docs here: https://www.freedesktop.org/software/systemd/man/org.freedesktop.systemd1.html #[dbus_proxy( interface = "org.freedesktop.systemd1.Manager", gen_blocking = false, default_service = "org.freedesktop.systemd1", default_path = "/org/freedesktop/systemd1" )] trait SystemdManagerDbus { #[dbus_proxy(name = "EnableUnitFiles")] fn enable_unit_files( &self, files: Vec, runtime: bool, force: bool, ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; fn link_unit_files( &self, files: Vec, runtime: bool, force: bool, ) -> zbus::Result>; fn disable_unit_files( &self, files: Vec, runtime: bool, ) -> zbus::Result>; #[dbus_proxy(name = "StartUnit")] fn start_unit(&self, name: String, mode: String) -> zbus::Result; #[dbus_proxy(name = "StopUnit")] fn stop_unit(&self, name: String, mode: String) -> zbus::Result; }