mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
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:
parent
2b48ecd3de
commit
b5aa3e0a3d
58
cli/Cargo.lock
generated
58
cli/Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, ®_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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue