client: verbose CLI output, TOML config in APPDATA, desktop install
- install.ps1: place exe on Desktop instead of TEMP, start with visible window - main.rs: banner on startup, [CMD]/[OK]/[ERR] prefixed logs with HH:MM:SS timestamps - Config: switch from JSON to TOML (config.toml in %APPDATA%\helios-remote\) - First-run wizard prompts for Relay URL + API Key (relay_code -> api_key) - Add chrono + toml deps to Cargo.toml
This commit is contained in:
parent
6e044e3e05
commit
1d019fa2b4
3 changed files with 104 additions and 67 deletions
|
|
@ -13,6 +13,8 @@ tokio-tungstenite = { version = "0.21", features = ["connect", "native-tls"] }
|
||||||
native-tls = { version = "0.2", features = [] }
|
native-tls = { version = "0.2", features = [] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
toml = "0.8"
|
||||||
|
chrono = "0.4"
|
||||||
helios-common = { path = "../common" }
|
helios-common = { path = "../common" }
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use native_tls::TlsConnector;
|
use native_tls::TlsConnector;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::Message, Connector};
|
use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::Message, Connector};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
use helios_common::{ClientMessage, ServerMessage};
|
use helios_common::{ClientMessage, ServerMessage};
|
||||||
|
|
||||||
|
|
@ -16,10 +17,27 @@ mod screenshot;
|
||||||
mod input;
|
mod input;
|
||||||
mod windows_mgmt;
|
mod windows_mgmt;
|
||||||
|
|
||||||
|
fn ts() -> String {
|
||||||
|
Local::now().format("%H:%M:%S").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! log_info {
|
||||||
|
($($arg:tt)*) => { println!("[{}] {}", ts(), format!($($arg)*)) };
|
||||||
|
}
|
||||||
|
macro_rules! log_cmd {
|
||||||
|
($($arg:tt)*) => { println!("[{}] [CMD] {}", ts(), format!($($arg)*)) };
|
||||||
|
}
|
||||||
|
macro_rules! log_ok {
|
||||||
|
($($arg:tt)*) => { println!("[{}] [OK] {}", ts(), format!($($arg)*)) };
|
||||||
|
}
|
||||||
|
macro_rules! log_err {
|
||||||
|
($($arg:tt)*) => { println!("[{}] [ERR] {}", ts(), format!($($arg)*)) };
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct Config {
|
struct Config {
|
||||||
relay_url: String,
|
relay_url: String,
|
||||||
relay_code: String,
|
api_key: String,
|
||||||
label: Option<String>,
|
label: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,19 +46,19 @@ impl Config {
|
||||||
let base = dirs::config_dir()
|
let base = dirs::config_dir()
|
||||||
.or_else(|| dirs::home_dir())
|
.or_else(|| dirs::home_dir())
|
||||||
.unwrap_or_else(|| PathBuf::from("."));
|
.unwrap_or_else(|| PathBuf::from("."));
|
||||||
base.join("helios-remote").join("config.json")
|
base.join("helios-remote").join("config.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load() -> Option<Self> {
|
fn load() -> Option<Self> {
|
||||||
let path = Self::config_path();
|
let path = Self::config_path();
|
||||||
let data = std::fs::read_to_string(&path).ok()?;
|
let data = std::fs::read_to_string(&path).ok()?;
|
||||||
serde_json::from_str(&data).ok()
|
toml::from_str(&data).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(&self) -> std::io::Result<()> {
|
fn save(&self) -> std::io::Result<()> {
|
||||||
let path = Self::config_path();
|
let path = Self::config_path();
|
||||||
std::fs::create_dir_all(path.parent().unwrap())?;
|
std::fs::create_dir_all(path.parent().unwrap())?;
|
||||||
let data = serde_json::to_string_pretty(self).unwrap();
|
let data = toml::to_string_pretty(self).unwrap();
|
||||||
std::fs::write(&path, data)?;
|
std::fs::write(&path, data)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +66,7 @@ impl Config {
|
||||||
|
|
||||||
fn prompt_config() -> Config {
|
fn prompt_config() -> Config {
|
||||||
let relay_url = {
|
let relay_url = {
|
||||||
println!("Relay server URL [default: wss://remote.agent-helios.me/ws]: ");
|
print!("Relay server URL [default: wss://remote.agent-helios.me/ws]: ");
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
std::io::stdin().read_line(&mut input).unwrap();
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
let trimmed = input.trim();
|
let trimmed = input.trim();
|
||||||
|
|
@ -59,46 +77,41 @@ fn prompt_config() -> Config {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let relay_code = {
|
let api_key = {
|
||||||
println!("Enter relay code: ");
|
print!("API Key: ");
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
std::io::stdin().read_line(&mut input).unwrap();
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
input.trim().to_string()
|
input.trim().to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let label = {
|
let label = {
|
||||||
println!("Label for this machine (optional, press Enter to skip): ");
|
print!("Label for this machine (optional, press Enter to skip): ");
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
std::io::stdin().read_line(&mut input).unwrap();
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
let trimmed = input.trim().to_string();
|
let trimmed = input.trim().to_string();
|
||||||
if trimmed.is_empty() { None } else { Some(trimmed) }
|
if trimmed.is_empty() { None } else { Some(trimmed) }
|
||||||
};
|
};
|
||||||
|
|
||||||
Config { relay_url, relay_code, label }
|
Config { relay_url, api_key, label }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt()
|
println!("=== Helios Remote Client ===");
|
||||||
.with_env_filter(
|
|
||||||
std::env::var("RUST_LOG")
|
|
||||||
.unwrap_or_else(|_| "helios_client=info".to_string()),
|
|
||||||
)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
// Load or prompt for config
|
// Load or prompt for config
|
||||||
let config = match Config::load() {
|
let config = match Config::load() {
|
||||||
Some(c) => {
|
Some(c) => {
|
||||||
info!("Loaded config from {:?}", Config::config_path());
|
log_info!("Config loaded from {:?}", Config::config_path());
|
||||||
c
|
c
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
info!("No config found — prompting for setup");
|
log_info!("No config found — first-time setup");
|
||||||
let c = prompt_config();
|
let c = prompt_config();
|
||||||
if let Err(e) = c.save() {
|
if let Err(e) = c.save() {
|
||||||
error!("Failed to save config: {e}");
|
log_err!("Failed to save config: {e}");
|
||||||
} else {
|
} else {
|
||||||
info!("Config saved to {:?}", Config::config_path());
|
log_info!("Config saved to {:?}", Config::config_path());
|
||||||
}
|
}
|
||||||
c
|
c
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +125,7 @@ async fn main() {
|
||||||
const MAX_BACKOFF: Duration = Duration::from_secs(30);
|
const MAX_BACKOFF: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
info!("Connecting to {}", config.relay_url);
|
log_info!("Connecting to {}...", config.relay_url);
|
||||||
// Build TLS connector - accepts self-signed certs for internal CA (Caddy tls internal)
|
// Build TLS connector - accepts self-signed certs for internal CA (Caddy tls internal)
|
||||||
let tls_connector = TlsConnector::builder()
|
let tls_connector = TlsConnector::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
|
|
@ -121,7 +134,7 @@ async fn main() {
|
||||||
let connector = Connector::NativeTls(tls_connector);
|
let connector = Connector::NativeTls(tls_connector);
|
||||||
match connect_async_tls_with_config(&config.relay_url, None, false, Some(connector)).await {
|
match connect_async_tls_with_config(&config.relay_url, None, false, Some(connector)).await {
|
||||||
Ok((ws_stream, _)) => {
|
Ok((ws_stream, _)) => {
|
||||||
info!("Connected!");
|
log_info!("Connected!");
|
||||||
backoff = Duration::from_secs(1); // reset on success
|
backoff = Duration::from_secs(1); // reset on success
|
||||||
|
|
||||||
let (mut write, mut read) = ws_stream.split();
|
let (mut write, mut read) = ws_stream.split();
|
||||||
|
|
@ -132,7 +145,7 @@ async fn main() {
|
||||||
};
|
};
|
||||||
let hello_json = serde_json::to_string(&hello).unwrap();
|
let hello_json = serde_json::to_string(&hello).unwrap();
|
||||||
if let Err(e) = write.send(Message::Text(hello_json)).await {
|
if let Err(e) = write.send(Message::Text(hello_json)).await {
|
||||||
error!("Failed to send Hello: {e}");
|
log_err!("Failed to send Hello: {e}");
|
||||||
tokio::time::sleep(backoff).await;
|
tokio::time::sleep(backoff).await;
|
||||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -148,7 +161,7 @@ async fn main() {
|
||||||
let server_msg: ServerMessage = match serde_json::from_str(&text) {
|
let server_msg: ServerMessage = match serde_json::from_str(&text) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to parse server message: {e}\nRaw: {text}");
|
log_err!("Failed to parse server message: {e} | raw: {text}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -161,7 +174,7 @@ async fn main() {
|
||||||
let json = serde_json::to_string(&response).unwrap();
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
let mut w = write_clone.lock().await;
|
let mut w = write_clone.lock().await;
|
||||||
if let Err(e) = w.send(Message::Text(json)).await {
|
if let Err(e) = w.send(Message::Text(json)).await {
|
||||||
error!("Failed to send response: {e}");
|
log_err!("Failed to send response: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -170,11 +183,11 @@ async fn main() {
|
||||||
let _ = w.send(Message::Pong(data)).await;
|
let _ = w.send(Message::Pong(data)).await;
|
||||||
}
|
}
|
||||||
Ok(Message::Close(_)) => {
|
Ok(Message::Close(_)) => {
|
||||||
info!("Server closed connection");
|
log_info!("Server closed connection");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("WebSocket error: {e}");
|
log_err!("WebSocket error: {e}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -184,7 +197,7 @@ async fn main() {
|
||||||
warn!("Disconnected. Reconnecting in {:?}...", backoff);
|
warn!("Disconnected. Reconnecting in {:?}...", backoff);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Connection failed: {e}");
|
log_err!("Connection failed: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,15 +212,19 @@ async fn handle_message(
|
||||||
) -> ClientMessage {
|
) -> ClientMessage {
|
||||||
match msg {
|
match msg {
|
||||||
ServerMessage::ScreenshotRequest { request_id } => {
|
ServerMessage::ScreenshotRequest { request_id } => {
|
||||||
|
log_cmd!("screenshot");
|
||||||
match screenshot::take_screenshot() {
|
match screenshot::take_screenshot() {
|
||||||
Ok((image_base64, width, height)) => ClientMessage::ScreenshotResponse {
|
Ok((image_base64, width, height)) => {
|
||||||
request_id,
|
log_ok!("screenshot taken ({}x{})", width, height);
|
||||||
image_base64,
|
ClientMessage::ScreenshotResponse {
|
||||||
width,
|
request_id,
|
||||||
height,
|
image_base64,
|
||||||
},
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Screenshot failed: {e}");
|
log_err!("Screenshot failed: {e}");
|
||||||
ClientMessage::Error {
|
ClientMessage::Error {
|
||||||
request_id,
|
request_id,
|
||||||
message: format!("Screenshot failed: {e}"),
|
message: format!("Screenshot failed: {e}"),
|
||||||
|
|
@ -217,17 +234,20 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::ExecRequest { request_id, command } => {
|
ServerMessage::ExecRequest { request_id, command } => {
|
||||||
info!("Exec: {command}");
|
log_cmd!("exec: {command}");
|
||||||
let mut sh = shell.lock().await;
|
let mut sh = shell.lock().await;
|
||||||
match sh.run(&command).await {
|
match sh.run(&command).await {
|
||||||
Ok((stdout, stderr, exit_code)) => ClientMessage::ExecResponse {
|
Ok((stdout, stderr, exit_code)) => {
|
||||||
request_id,
|
log_ok!("exec completed (exit {})", exit_code);
|
||||||
stdout,
|
ClientMessage::ExecResponse {
|
||||||
stderr,
|
request_id,
|
||||||
exit_code,
|
stdout,
|
||||||
},
|
stderr,
|
||||||
|
exit_code,
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Exec failed for command {:?}: {e}", command);
|
log_err!("exec failed for {:?}: {e}", command);
|
||||||
ClientMessage::Error {
|
ClientMessage::Error {
|
||||||
request_id,
|
request_id,
|
||||||
message: format!(
|
message: format!(
|
||||||
|
|
@ -240,11 +260,14 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::ClickRequest { request_id, x, y, button } => {
|
ServerMessage::ClickRequest { request_id, x, y, button } => {
|
||||||
info!("Click: ({x},{y}) {:?}", button);
|
log_cmd!("click {} {} {:?}", x, y, button);
|
||||||
match input::click(x, y, &button) {
|
match input::click(x, y, &button) {
|
||||||
Ok(()) => ClientMessage::Ack { request_id },
|
Ok(()) => {
|
||||||
|
log_ok!("click done");
|
||||||
|
ClientMessage::Ack { request_id }
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Click failed at ({x},{y}): {e}");
|
log_err!("click failed at ({x},{y}): {e}");
|
||||||
ClientMessage::Error {
|
ClientMessage::Error {
|
||||||
request_id,
|
request_id,
|
||||||
message: format!("Click at ({x},{y}) failed: {e}"),
|
message: format!("Click at ({x},{y}) failed: {e}"),
|
||||||
|
|
@ -254,11 +277,14 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::TypeRequest { request_id, text } => {
|
ServerMessage::TypeRequest { request_id, text } => {
|
||||||
info!("Type: {} chars", text.len());
|
log_cmd!("type: {} chars", text.len());
|
||||||
match input::type_text(&text) {
|
match input::type_text(&text) {
|
||||||
Ok(()) => ClientMessage::Ack { request_id },
|
Ok(()) => {
|
||||||
|
log_ok!("type done");
|
||||||
|
ClientMessage::Ack { request_id }
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Type failed: {e}");
|
log_err!("type failed: {e}");
|
||||||
ClientMessage::Error {
|
ClientMessage::Error {
|
||||||
request_id,
|
request_id,
|
||||||
message: format!("Type failed: {e}"),
|
message: format!("Type failed: {e}"),
|
||||||
|
|
@ -268,59 +294,68 @@ async fn handle_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::ListWindowsRequest { request_id } => {
|
ServerMessage::ListWindowsRequest { request_id } => {
|
||||||
info!("ListWindows");
|
log_cmd!("list-windows");
|
||||||
match windows_mgmt::list_windows() {
|
match windows_mgmt::list_windows() {
|
||||||
Ok(windows) => ClientMessage::ListWindowsResponse { request_id, windows },
|
Ok(windows) => {
|
||||||
|
log_ok!("list-windows: {} windows", windows.len());
|
||||||
|
ClientMessage::ListWindowsResponse { request_id, windows }
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("ListWindows failed: {e}");
|
log_err!("list-windows failed: {e}");
|
||||||
ClientMessage::Error { request_id, message: e }
|
ClientMessage::Error { request_id, message: e }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::MinimizeAllRequest { request_id } => {
|
ServerMessage::MinimizeAllRequest { request_id } => {
|
||||||
info!("MinimizeAll");
|
log_cmd!("minimize-all");
|
||||||
match windows_mgmt::minimize_all() {
|
match windows_mgmt::minimize_all() {
|
||||||
Ok(()) => ClientMessage::Ack { request_id },
|
Ok(()) => {
|
||||||
|
log_ok!("minimize-all done");
|
||||||
|
ClientMessage::Ack { request_id }
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("MinimizeAll failed: {e}");
|
log_err!("minimize-all failed: {e}");
|
||||||
ClientMessage::Error { request_id, message: e }
|
ClientMessage::Error { request_id, message: e }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::FocusWindowRequest { request_id, window_id } => {
|
ServerMessage::FocusWindowRequest { request_id, window_id } => {
|
||||||
info!("FocusWindow: {window_id}");
|
log_cmd!("focus-window: {window_id}");
|
||||||
match windows_mgmt::focus_window(window_id) {
|
match windows_mgmt::focus_window(window_id) {
|
||||||
Ok(()) => ClientMessage::Ack { request_id },
|
Ok(()) => {
|
||||||
|
log_ok!("focus-window done");
|
||||||
|
ClientMessage::Ack { request_id }
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("FocusWindow failed: {e}");
|
log_err!("focus-window failed: {e}");
|
||||||
ClientMessage::Error { request_id, message: e }
|
ClientMessage::Error { request_id, message: e }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::MaximizeAndFocusRequest { request_id, window_id } => {
|
ServerMessage::MaximizeAndFocusRequest { request_id, window_id } => {
|
||||||
info!("MaximizeAndFocus: {window_id}");
|
log_cmd!("maximize-and-focus: {window_id}");
|
||||||
match windows_mgmt::maximize_and_focus(window_id) {
|
match windows_mgmt::maximize_and_focus(window_id) {
|
||||||
Ok(()) => ClientMessage::Ack { request_id },
|
Ok(()) => {
|
||||||
|
log_ok!("maximize-and-focus done");
|
||||||
|
ClientMessage::Ack { request_id }
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("MaximizeAndFocus failed: {e}");
|
log_err!("maximize-and-focus failed: {e}");
|
||||||
ClientMessage::Error { request_id, message: e }
|
ClientMessage::Error { request_id, message: e }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::Ack { request_id } => {
|
ServerMessage::Ack { request_id } => {
|
||||||
info!("Server ack for {request_id}");
|
|
||||||
// Nothing to do - server acked something we sent
|
// Nothing to do - server acked something we sent
|
||||||
ClientMessage::Ack { request_id }
|
ClientMessage::Ack { request_id }
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerMessage::Error { request_id, message } => {
|
ServerMessage::Error { request_id, message } => {
|
||||||
error!("Server error (req={request_id:?}): {message}");
|
log_err!("server error (req={request_id:?}): {message}");
|
||||||
// No meaningful response needed but we need to return something
|
|
||||||
// Use a dummy ack if we have a request_id
|
|
||||||
if let Some(rid) = request_id {
|
if let Some(rid) = request_id {
|
||||||
ClientMessage::Ack { request_id: rid }
|
ClientMessage::Ack { request_id: rid }
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$url = "https://github.com/agent-helios/helios-remote/releases/latest/download/helios-remote-client-windows.exe"
|
$url = "https://github.com/agent-helios/helios-remote/releases/latest/download/helios-remote-client-windows.exe"
|
||||||
$dest = "$env:TEMP\helios-remote.exe"
|
$dest = "$env:USERPROFILE\Desktop\Helios Remote.exe"
|
||||||
|
|
||||||
Write-Host "Downloading helios-remote client..."
|
Write-Host "Downloading helios-remote client..."
|
||||||
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
|
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
|
||||||
|
|
||||||
Write-Host "Starting..."
|
Write-Host "Starting..."
|
||||||
Start-Process -FilePath $dest -NoNewWindow
|
Start-Process -FilePath $dest
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue