diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 7d525983d07..33be36ee582 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -257,6 +257,7 @@ dependencies = [ "serde_bytes", "serde_json", "sha2", + "shell-escape", "sysinfo", "tar", "tempfile", @@ -266,7 +267,6 @@ dependencies = [ "url", "uuid", "winapi", - "windows-service", "winreg", "zbus 3.4.0", "zip", @@ -587,20 +587,6 @@ dependencies = [ "syn", ] -[[package]] -name = "err-derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "rustversion", - "syn", - "synstructure", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -1885,12 +1871,6 @@ dependencies = [ "yasna", ] -[[package]] -name = "rustversion" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" - [[package]] name = "ryu" version = "1.0.11" @@ -2060,6 +2040,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2123,18 +2109,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - [[package]] name = "sysinfo" version = "0.27.7" @@ -2630,12 +2604,6 @@ dependencies = [ "cc", ] -[[package]] -name = "widestring" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" - [[package]] name = "winapi" version = "0.3.9" @@ -2667,18 +2635,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-service" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917fdb865e7ff03af9dd86609f8767bc88fefba89e8efd569de8e208af8724b3" -dependencies = [ - "bitflags", - "err-derive", - "widestring", - "windows-sys 0.36.1", -] - [[package]] name = "windows-sys" version = "0.36.1" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3f92a7590c7..877895c886c 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -49,13 +49,13 @@ log = "0.4" const_format = "0.2" sha2 = "0.10" base64 = "0.13" +shell-escape = "0.1.5" [build-dependencies] serde = { version = "1.0" } serde_json = { version = "1.0" } [target.'cfg(windows)'.dependencies] -windows-service = "0.5" winreg = "0.10" winapi = "0.3.9" diff --git a/cli/src/constants.rs b/cli/src/constants.rs index ca5b32c8e3d..b3adb020260 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -61,6 +61,9 @@ pub const QUALITYLESS_SERVER_NAME: &str = concatcp!(QUALITYLESS_PRODUCT_NAME, " /// Web URL the editor is hosted at. For VS Code, this is vscode.dev. pub const EDITOR_WEB_URL: Option<&'static str> = option_env!("VSCODE_CLI_EDITOR_WEB_URL"); +/// Name shown in places where we need to tell a user what a process is, e.g. in sleep inhibition. +pub const TUNNEL_ACTIVITY_NAME: &str = concatcp!(PRODUCT_NAME_LONG, " Tunnel"); + const NONINTERACTIVE_VAR: &str = "VSCODE_CLI_NONINTERACTIVE"; pub fn get_default_user_agent() -> String { diff --git a/cli/src/tunnels/nosleep_macos.rs b/cli/src/tunnels/nosleep_macos.rs index 471821af41e..42909d59d61 100644 --- a/cli/src/tunnels/nosleep_macos.rs +++ b/cli/src/tunnels/nosleep_macos.rs @@ -5,12 +5,11 @@ use std::io; -use const_format::concatcp; use core_foundation::base::TCFType; use core_foundation::string::{CFString, CFStringRef}; use libc::c_int; -use crate::constants::APPLICATION_NAME; +use crate::constants::TUNNEL_ACTIVITY_NAME; extern "C" { pub fn IOPMAssertionCreateWithName( @@ -64,8 +63,7 @@ pub struct SleepInhibitor { impl SleepInhibitor { pub async fn new() -> io::Result { let mut assertions = Vec::with_capacity(NUM_ASSERTIONS); - let assertion_name = - CFString::from_static_string(concatcp!(APPLICATION_NAME, " running tunnel")); + let assertion_name = CFString::from_static_string(concatcp!(TUNNEL_ACTIVITY_NAME)); for typ in ASSERTIONS { assertions.push(Assertion::make( &CFString::from_static_string(typ), diff --git a/cli/src/tunnels/nosleep_windows.rs b/cli/src/tunnels/nosleep_windows.rs index ee3bf948086..ed8d3f7a434 100644 --- a/cli/src/tunnels/nosleep_windows.rs +++ b/cli/src/tunnels/nosleep_windows.rs @@ -5,7 +5,6 @@ use std::io; -use const_format::concatcp; use winapi::{ ctypes::c_void, um::{ @@ -19,15 +18,13 @@ use winapi::{ }, }; -use crate::constants::APPLICATION_NAME; +use crate::constants::TUNNEL_ACTIVITY_NAME; struct Request(*mut c_void); impl Request { pub fn new() -> io::Result { - let mut reason: Vec = concatcp!(APPLICATION_NAME, " running tunnel") - .encode_utf16() - .collect(); + let mut reason: Vec = TUNNEL_ACTIVITY_NAME.encode_utf16().collect(); let mut context = REASON_CONTEXT { Version: POWER_REQUEST_CONTEXT_VERSION, Flags: POWER_REQUEST_CONTEXT_SIMPLE_STRING, diff --git a/cli/src/tunnels/service_windows.rs b/cli/src/tunnels/service_windows.rs index c893a3d0b79..7d839ccf330 100644 --- a/cli/src/tunnels/service_windows.rs +++ b/cli/src/tunnels/service_windows.rs @@ -4,42 +4,30 @@ *--------------------------------------------------------------------------------------------*/ use async_trait::async_trait; -use dialoguer::{theme::ColorfulTheme, Input, Password}; -use lazy_static::lazy_static; -use std::{ffi::OsString, path::PathBuf, sync::Mutex, thread, time::Duration}; -use tokio::sync::mpsc; -use windows_service::{ - define_windows_service, - service::{ - ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode, - ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType, - }, - service_control_handler::{self, ServiceControlHandlerResult}, - service_dispatcher, - service_manager::{ServiceManager, ServiceManagerAccess}, +use shell_escape::windows::escape as shell_escape; +use std::{ + io, + path::PathBuf, + process::{Command, Stdio}, }; +use sysinfo::{ProcessExt, System, SystemExt}; +use winreg::{enums::HKEY_CURRENT_USER, RegKey}; use crate::{ - constants::QUALITYLESS_PRODUCT_NAME, - util::errors::{wrap, wrapdbg, AnyError, WindowsNeedsElevation}, tunnels::shutdown_signal::ShutdownSignal, -}; -use crate::{ - log::{self, FileLogSink}, + constants::TUNNEL_ACTIVITY_NAME, + log, state::LauncherPaths, + tunnels::shutdown_signal::ShutdownSignal, + util::errors::{wrap, wrapdbg, AnyError}, }; -use super::service::{ - tail_log_file, ServiceContainer, ServiceManager as CliServiceManager, SERVICE_LOG_FILE_NAME, -}; +use super::service::{tail_log_file, ServiceContainer, ServiceManager as CliServiceManager}; pub struct WindowsService { log: log::Logger, log_file: PathBuf, } -const SERVICE_NAME: &str = "code_tunnel"; -const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; - impl WindowsService { pub fn new(log: log::Logger, paths: &LauncherPaths) -> Self { Self { @@ -47,81 +35,47 @@ impl WindowsService { log_file: paths.service_log_file(), } } + + fn open_key() -> Result { + RegKey::predef(HKEY_CURRENT_USER) + .create_subkey(r"Software\Microsoft\Windows\CurrentVersion\Run") + .map_err(|e| wrap(e, "error opening run registry key").into()) + .map(|(key, _)| key) + } } #[async_trait] impl CliServiceManager for WindowsService { async fn register(&self, exe: std::path::PathBuf, args: &[&str]) -> Result<(), AnyError> { - let service_manager = ServiceManager::local_computer( - None::<&str>, - ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE, - ) - .map_err(|e| WindowsNeedsElevation(format!("error getting service manager: {}", e)))?; + let key = WindowsService::open_key()?; - let mut args = args.iter().map(OsString::from).collect::>(); - args.push(OsString::from("--log-to-file")); - args.push(self.log_file.as_os_str().to_os_string()); + let mut reg_str = String::new(); + let mut cmd = Command::new(&exe); + reg_str.push_str(shell_escape(exe.to_string_lossy()).as_ref()); - let mut service_info = ServiceInfo { - name: OsString::from(SERVICE_NAME), - display_name: OsString::from(format!("{} Tunnel", QUALITYLESS_PRODUCT_NAME)), - service_type: SERVICE_TYPE, - start_type: ServiceStartType::AutoStart, - error_control: ServiceErrorControl::Normal, - executable_path: exe, - launch_arguments: args, - dependencies: vec![], - account_name: None, - account_password: None, + let mut add_arg = |arg: &str| { + reg_str.push(' '); + reg_str.push_str(shell_escape((*arg).into()).as_ref()); + cmd.arg(arg); }; - let existing_service = service_manager.open_service( - SERVICE_NAME, - ServiceAccess::QUERY_STATUS | ServiceAccess::START | ServiceAccess::CHANGE_CONFIG, - ); - let service = if let Ok(service) = existing_service { - service - .change_config(&service_info) - .map_err(|e| wrapdbg(e, "error updating existing service"))?; - service - } else { - loop { - let (username, password) = prompt_credentials()?; - service_info.account_name = Some(format!(".\\{}", username).into()); - service_info.account_password = Some(password.into()); + for arg in args { + add_arg(*arg); + } - match service_manager.create_service( - &service_info, - ServiceAccess::CHANGE_CONFIG | ServiceAccess::START, - ) { - Ok(service) => break service, - Err(windows_service::Error::Winapi(e)) if Some(1057) == e.raw_os_error() => { - error!( - self.log, - "Invalid username or password, please try again..." - ); - } - Err(e) => return Err(wrap(e, "error registering service").into()), - } - } - }; + add_arg("--log-to-file"); + add_arg(self.log_file.to_string_lossy().as_ref()); - service - .set_description("Service that runs `code tunnel` for access on vscode.dev") - .ok(); + key.set_value(TUNNEL_ACTIVITY_NAME, ®_str) + .map_err(|e| AnyError::from(wrapdbg(e, "error setting registry key")))?; info!(self.log, "Successfully registered service..."); - let status = service - .query_status() - .map(|s| s.current_state) - .unwrap_or(ServiceState::Stopped); - - if status == ServiceState::Stopped { - service - .start::<&str>(&[]) - .map_err(|e| wrapdbg(e, "error starting service"))?; - } + cmd.stderr(Stdio::null()); + cmd.stdout(Stdio::null()); + cmd.stdin(Stdio::null()); + cmd.spawn() + .map_err(|e| wrapdbg(e, "error starting service"))?; info!(self.log, "Tunnel service successfully started"); Ok(()) @@ -131,173 +85,41 @@ impl CliServiceManager for WindowsService { tail_log_file(&self.log_file).await } - #[allow(unused_must_use)] // triggers incorrectly on `define_windows_service!` async fn run( self, launcher_paths: LauncherPaths, - handle: impl 'static + ServiceContainer, + mut handle: impl 'static + ServiceContainer, ) -> Result<(), AnyError> { - let log = match FileLogSink::new( - log::Level::Debug, - &launcher_paths.root().join(SERVICE_LOG_FILE_NAME), - ) { - Ok(sink) => self.log.tee(sink), - Err(e) => { - warning!(self.log, "Failed to create service log file: {}", e); - self.log - } - }; - - // We put the handle into the global "impl" type and then take it out in - // my_service_main. This is needed just since we have to have that - // function at the root level, but need to pass in data later here... - SERVICE_IMPL.lock().unwrap().replace(ServiceImpl { - container: Box::new(handle), - launcher_paths, - log, - }); - - define_windows_service!(ffi_service_main, service_main); - - service_dispatcher::start(SERVICE_NAME, ffi_service_main) - .map_err(|e| wrap(e, "error starting service dispatcher").into()) + let rx = ShutdownSignal::create_rx(&[ShutdownSignal::CtrlC]); + handle.run_service(self.log, launcher_paths, rx).await } async fn unregister(&self) -> Result<(), AnyError> { - let service_manager = - ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT) - .map_err(|e| wrap(e, "error getting service manager"))?; - - let service = service_manager.open_service( - SERVICE_NAME, - ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE, - ); - - let service = match service { - Ok(service) => service, - // Service does not exist: - Err(windows_service::Error::Winapi(e)) if Some(1060) == e.raw_os_error() => { - return Ok(()) - } - Err(e) => return Err(wrap(e, "error getting service handle").into()), + let key = WindowsService::open_key()?; + let prev_command_line: String = match key.get_value(TUNNEL_ACTIVITY_NAME) { + Ok(l) => l, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(wrap(e, "error getting registry key").into()), }; - let service_status = service - .query_status() - .map_err(|e| wrapdbg(e, "error getting service status"))?; + key.delete_value(TUNNEL_ACTIVITY_NAME) + .map_err(|e| AnyError::from(wrap(e, "error deleting registry key")))?; + info!(self.log, "Tunnel service uninstalled"); - if service_status.current_state != ServiceState::Stopped { - service - .stop() - .map_err(|e| wrapdbg(e, "error getting stopping service"))?; + let mut sys = System::new(); + sys.refresh_processes(); - while let Ok(ServiceState::Stopped) = service.query_status().map(|s| s.current_state) { - info!(self.log, "Polling for service to stop..."); - thread::sleep(Duration::from_secs(1)); + for process in sys.processes().values() { + let joined = process.cmd().join(" "); // this feels a little sketch, but seems to work fine + if joined == prev_command_line { + process.kill(); + info!(self.log, "Successfully shut down running tunnel"); + return Ok(()); } } - service - .delete() - .map_err(|e| wrapdbg(e, "error deleting service"))?; + warning!(self.log, "The tunnel service has been unregistered, but we couldn't find a running tunnel process. You may need to restart or log out and back in to fully stop the tunnel."); Ok(()) } } - -struct ServiceImpl { - container: Box, - launcher_paths: LauncherPaths, - log: log::Logger, -} - -lazy_static! { - static ref SERVICE_IMPL: Mutex> = Mutex::new(None); -} - -/// "main" function that the service calls in its own thread. -fn service_main(_arguments: Vec) -> Result<(), AnyError> { - let mut service = SERVICE_IMPL.lock().unwrap().take().unwrap(); - - // Create a channel to be able to poll a stop event from the service worker loop. - let (shutdown_tx, shutdown_rx) = mpsc::unbounded_channel::(); - let mut shutdown_tx = Some(shutdown_tx); - - // Define system service event handler that will be receiving service events. - let event_handler = move |control_event| -> ServiceControlHandlerResult { - match control_event { - ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, - ServiceControl::Stop => { - shutdown_tx - .take() - .and_then(|tx| tx.send(ShutdownSignal::ServiceStopped).ok()); - ServiceControlHandlerResult::NoError - } - _ => ServiceControlHandlerResult::NotImplemented, - } - }; - - let status_handle = service_control_handler::register(SERVICE_NAME, event_handler) - .map_err(|e| wrap(e, "error registering service event handler"))?; - - // Tell the system that service is running - status_handle - .set_service_status(ServiceStatus { - service_type: SERVICE_TYPE, - current_state: ServiceState::Running, - controls_accepted: ServiceControlAccept::STOP, - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - }) - .map_err(|e| wrap(e, "error marking service as running"))?; - - info!(service.log, "Starting service loop..."); - - let panic_log = service.log.clone(); - std::panic::set_hook(Box::new(move |p| { - error!(panic_log, "Service panic: {:?}", p); - })); - - let result = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - .block_on( - service - .container - .run_service(service.log, service.launcher_paths, shutdown_rx), - ); - - status_handle - .set_service_status(ServiceStatus { - service_type: SERVICE_TYPE, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - }) - .map_err(|e| wrap(e, "error marking service as stopped"))?; - - result -} - -fn prompt_credentials() -> Result<(String, String), AnyError> { - println!("Running a Windows service under your user requires your username and password."); - println!("These are sent to the Windows Service Manager and are not stored by VS Code."); - - let username: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Windows username:") - .interact_text() - .map_err(|e| wrap(e, "Failed to read username"))?; - - let password = Password::with_theme(&ColorfulTheme::default()) - .with_prompt("Windows password:") - .interact() - .map_err(|e| wrap(e, "Failed to read password"))?; - - Ok((username, password)) -}