cli: use better approach to Windows services (#172679)

Fixes #167741

This eschews the offical Windows service system in favor of registering
into `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run`.
Unlike services, this can be done without administrative permissions,
does not require the current username/password, and is not blocked by
miscellaneous and mysterious system policies.

Since the process is basically unmanaged by the OS, this requires a
little legwork to start and stop the process when registering and
unregistering.
This commit is contained in:
Connor Peet 2023-01-27 15:04:56 -08:00 committed by GitHub
parent 2b48ecd3de
commit b5aa3e0a3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 298 deletions

58
cli/Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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 {

View file

@ -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<Self> {
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),

View file

@ -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<Self> {
let mut reason: Vec<u16> = concatcp!(APPLICATION_NAME, " running tunnel")
.encode_utf16()
.collect();
let mut reason: Vec<u16> = TUNNEL_ACTIVITY_NAME.encode_utf16().collect();
let mut context = REASON_CONTEXT {
Version: POWER_REQUEST_CONTEXT_VERSION,
Flags: POWER_REQUEST_CONTEXT_SIMPLE_STRING,

View file

@ -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, AnyError> {
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::<Vec<OsString>>();
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, &reg_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<dyn ServiceContainer>,
launcher_paths: LauncherPaths,
log: log::Logger,
}
lazy_static! {
static ref SERVICE_IMPL: Mutex<Option<ServiceImpl>> = Mutex::new(None);
}
/// "main" function that the service calls in its own thread.
fn service_main(_arguments: Vec<OsString>) -> 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::<ShutdownSignal>();
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))
}