Skip to content

Commit

Permalink
feat: download basis binary directly
Browse files Browse the repository at this point in the history
Uses their new recommended aka.ms links.

Fixes #46
Fixes #84
  • Loading branch information
connor4312 committed Mar 15, 2022
1 parent f090950 commit cd9aad1
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 235 deletions.
305 changes: 180 additions & 125 deletions src/cli/src/basis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,156 +3,211 @@
*--------------------------------------------------------------------------------------------*/

use crate::log;
use crate::util::command::{capture_shell_command, pipe_shell_command};
use crate::util::errors::{AnyError, BasisTunnelError};
use crate::state::LauncherPaths;
use crate::util::command::capture_command;
use crate::util::errors::{wrap, AnyError, BasisTunnelError, StatusError};
use crate::util::{http, machine};
use lazy_static::lazy_static;
use regex::Regex;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;

pub async fn install_basis_if_needed(log: &log::Logger) -> Result<(), AnyError> {
let basis_version = capture_shell_command("basis --version").await?;

if !basis_version.status.success() {
log.emit(log::Level::Info, "Installing tunneling service...");
let plog = log.producer(log::Producer::Basis(log::next_counter()));
pipe_shell_command(&plog, "curl -sL https://aka.ms/BasisCliInstall | bash").await?;
} else {
let v = String::from_utf8_lossy(&basis_version.stdout).to_string();

log.emit(
log::Level::Info,
&format!(
"Found existing installation of tunneling service ({}).",
v.trim()
),
);
#[derive(Clone)]
pub struct Basis {
path: PathBuf,
log: log::Logger,
verified_installed: bool,
verified_logged_in: bool,
}

impl Basis {
pub fn new(log: &log::Logger, paths: &LauncherPaths) -> Basis {
Basis {
log: log.clone(),
path: paths.root().join(if cfg!(target_os = "windows") {
"basis.exe"
} else {
"basis"
}),
verified_installed: false,
verified_logged_in: false,
}
}

Ok(())
}
pub async fn ensure_installed(&mut self) -> Result<(), AnyError> {
if self.verified_installed {
return Ok(());
}

pub async fn login_to_basis(log: &log::Logger) -> Result<(), AnyError> {
let output = capture_shell_command("basis user show").await?;
let user = String::from_utf8_lossy(&output.stdout).to_string();
if self.path.exists() {
self.verified_installed = true;
return Ok(());
}

let platform_download_path = if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
"linux-x64"
} else if cfg!(target_os = "macos") {
"osx-x64"
} else {
panic!("Your platform or architecture is not supported by our forwarding tool. You may be able to run with `--no-forward`.");
};

self.log
.emit(log::Level::Verbose, "Downloading port forwarding utility");

let response = reqwest::get(format!(
"https://aka.ms/TunnelsCliDownload/{}",
platform_download_path
))
.await?;

if !response.status().is_success() {
return Err(StatusError::from_res(response).await?.into());
}

if user.contains("Logged in") {
log.emit(log::Level::Info, &user);
return Ok(());
http::download_into_file(&self.path, response).await?;
machine::set_executable_permission(&self.path).await?;
self.verified_installed = true;

Ok(())
}

let mut cmd = Command::new("basis");
cmd.args(["user", "login", "-d"]);
cmd.stdout(Stdio::piped());

let mut child = cmd.spawn().expect("failed to spawn command");
let stdout = child
.stdout
.take()
.expect("child did not have a handle to stdout");

let plog = log.producer(log::Producer::Basis(log::next_counter()));
plog.join(log::Level::Info, stdout);
child
.wait()
.await
.expect("child process encountered an error");
Ok(())
}
pub async fn ensure_logged_in(&mut self) -> Result<(), AnyError> {
if self.verified_logged_in {
return Ok(());
}

pub async fn start_new_tunnel(log: &log::Logger, port: u32) -> Result<String, AnyError> {
let port_str = format!("{}", port);
let mut command = ["host", "-p", &port_str, "-c", "TunnelRelay"];
log.emit(
log::Level::Verbose,
&format!("Starting tunnel with command... basis {:?}", command),
);
self.ensure_installed().await?;

start_tunnel(log, &mut command).await
}
let output = capture_command(&self.path, &["user", "show"]).await?;
let user = String::from_utf8_lossy(&output.stdout).to_string();

pub async fn start_existing_tunnel(
log: &log::Logger,
port: u32,
host_token: &str,
tunnel_id: &str,
) -> Result<String, AnyError> {
let port_str = format!("{}", port);
let mut command = [
"host",
tunnel_id,
"--access-token",
host_token,
"-p",
&port_str,
"-c",
"TunnelRelay",
];
log.emit(
log::Level::Verbose,
&format!("Starting tunnel with command... basis {:?}", command),
);

start_tunnel(log, &mut command).await
}
if user.contains("Logged in") {
self.log.emit(
log::Level::Verbose,
&format!("Logged into port fowarding: {}", user),
);
self.verified_logged_in = true;
return Ok(());
}

lazy_static! {
static ref BASIS_TUNNEL_RE: Regex =
Regex::new(r"Ready to accept connections for tunnel: (?P<tunnel>.+)").unwrap();
}
let mut cmd = Command::new(&self.path);
cmd.args(&["user", "login", "-d"]);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::inherit());

let mut child = cmd.spawn().expect("failed to spawn command");
let stdout = child.stdout.take();

if let Some(stdout) = stdout {
let plog = self.log.producer(log::Producer::Basis(log::next_counter()));
plog.join(log::Level::Info, stdout);
}

child
.wait()
.await
.map_err(|e| wrap(Box::new(e), "failed to log into basis".to_string()))?;

self.verified_logged_in = true;
Ok(())
}

pub async fn start_new_tunnel(&mut self, port: u32) -> Result<String, AnyError> {
self.ensure_logged_in().await?;

let port_str = format!("{}", port);
let mut command = ["host", "-p", &port_str, "-c", "TunnelRelay"];
self.start_tunnel(&mut command).await
}

pub async fn start_existing_tunnel(
&mut self,
port: u32,
host_token: &str,
tunnel_id: &str,
) -> Result<String, AnyError> {
self.ensure_installed().await?; // no login needed here since host_token is given

let port_str = format!("{}", port);
let mut command = [
"host",
tunnel_id,
"--access-token",
host_token,
"-p",
&port_str,
"-c",
"TunnelRelay",
];

self.start_tunnel(&mut command).await
}

async fn start_tunnel(&mut self, args: &mut [&str]) -> Result<String, AnyError> {
self.log.emit(
log::Level::Verbose,
&format!("Starting tunnel with command... basis {:?}", args),
);

async fn start_tunnel(log: &log::Logger, args: &mut [&str]) -> Result<String, AnyError> {
let mut cmd = Command::new("basis");
cmd.args(args);
cmd.stdout(Stdio::piped());
let mut cmd = Command::new(&self.path);
cmd.args(args);
cmd.stdout(Stdio::piped());

let mut child = cmd.spawn().expect("failed to spawn command");
let mut child = cmd.spawn().expect("failed to spawn command");

let stdout = child
.stdout
.take()
.expect("child did not have a handle to stdout");
let stdout = child
.stdout
.take()
.expect("child did not have a handle to stdout");

let (tx, rx) = tokio::sync::oneshot::channel();
let plog = log.producer(log::Producer::Basis(log::next_counter()));
tokio::spawn(async move {
let mut reader = BufReader::new(stdout).lines();
while let Some(line) = reader.next_line().await? {
plog.emit(log::Level::Info, &line);
let (tx, rx) = tokio::sync::oneshot::channel();
let plog = self.log.producer(log::Producer::Basis(log::next_counter()));
tokio::spawn(async move {
let mut reader = BufReader::new(stdout).lines();
while let Some(line) = reader.next_line().await? {
plog.emit(log::Level::Info, &line);

let re_match = BASIS_TUNNEL_RE
.captures(&line)
.and_then(|cap| cap.name("tunnel").map(|tunnel| tunnel.as_str()));
let re_match = BASIS_TUNNEL_RE
.captures(&line)
.and_then(|cap| cap.name("tunnel").map(|tunnel| tunnel.as_str()));

if let Some(t) = re_match {
tx.send(t.to_owned()).expect("Failed to send tunnel ID");
break;
if let Some(t) = re_match {
tx.send(t.to_owned()).expect("Failed to send tunnel ID");
break;
}
}
}

while let Some(line) = reader.next_line().await? {
plog.emit(log::Level::Verbose, &line);
}
while let Some(line) = reader.next_line().await? {
plog.emit(log::Level::Verbose, &line);
}

Ok::<(), std::io::Error>(())
});

let result = (tokio::select! {
msg = rx => Ok(msg.unwrap()),
result = child.wait() => {
match result {
Ok(e) => Err(BasisTunnelError(format!(
"basis exited with code {}, run with --verbose for full logs:",
e.code().unwrap_or(1)
))),
Err(e) => Err(BasisTunnelError(format!(
"error creating tunnel: {}",
e
))),
Ok::<(), std::io::Error>(())
});

let result = (tokio::select! {
msg = rx => Ok(msg.unwrap()),
result = child.wait() => {
match result {
Ok(e) => Err(BasisTunnelError(format!(
"basis exited with code {}, run with --verbose for full logs:",
e.code().unwrap_or(1)
))),
Err(e) => Err(BasisTunnelError(format!(
"error creating tunnel: {}",
e
))),
}
}
}
})?;
})?;

Ok(result)
}
}

Ok(result)
lazy_static! {
static ref BASIS_TUNNEL_RE: Regex =
Regex::new(r"Ready to accept connections for tunnel: (?P<tunnel>.+)").unwrap();
}
21 changes: 8 additions & 13 deletions src/cli/src/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

use crate::basis;
use crate::basis::Basis;
use crate::cli;
use crate::cli::Quality;
use crate::log;
Expand Down Expand Up @@ -30,6 +30,7 @@ pub struct SuccessParams {

pub async fn install_and_start_server(
logger: &log::Logger,
basis: &mut Basis,
launcher_paths: &LauncherPaths,
server_params: &ServerParams,
tunnel_args: &cli::BasisDetails,
Expand Down Expand Up @@ -73,7 +74,7 @@ pub async fn install_and_start_server(

let mut tunnel = None;
if !tunnel_args.no_forward {
tunnel = Some(forward_with_basis(logger, tunnel_args, port).await?);
tunnel = Some(forward_with_basis(basis, tunnel_args, port).await?);
}

let connection_token = launcher_paths
Expand All @@ -87,24 +88,18 @@ pub async fn install_and_start_server(
})
}

pub async fn prepare_basis(log: &log::Logger, details: &cli::BasisDetails) -> Result<(), AnyError> {
basis::install_basis_if_needed(log).await?;
if details.host_token.is_none() {
basis::login_to_basis(log).await?;
}
Ok(())
}

pub async fn forward_with_basis(
log: &log::Logger,
basis: &mut Basis,
details: &cli::BasisDetails,
port: u32,
) -> Result<String, AnyError> {
let tunnel =
if let (Some(host_token), Some(tunnel_id)) = (&details.host_token, &details.tunnel_id) {
basis::start_existing_tunnel(log, port, host_token, tunnel_id).await?
basis
.start_existing_tunnel(port, host_token, tunnel_id)
.await?
} else {
basis::start_new_tunnel(log, port).await?
basis.start_new_tunnel(port).await?
};

Ok(tunnel)
Expand Down
Loading

0 comments on commit cd9aad1

Please sign in to comment.