Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 38 additions & 7 deletions crates/cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10159,8 +10159,8 @@ impl App {
"tasks" => {
self.open_tasks_popup().await;
}
"remote-control" | "remote" => {
// Subcommand dispatch. `stop`, `debug`, and `cloudflare`
"remote-control" | "remote-connect" | "remote" => {
// Subcommand dispatch. `stop`, `debug`, and provider names
// are reserved keywords; anything else is a literal
// password override (so a user who wants the password
// `stop` has to pick another word).
Expand All @@ -10173,6 +10173,8 @@ impl App {
// dialog's resting state
// /remote-control cloudflare [pw] → skip the dialog,
// start the tunnel
// /remote-control construct <name> → stable first-party
// tunnel
// /remote-control <anything else> → dialog + pw=<that>
use construct_protocol::TunnelProvider;
let (sub, rest) = arg
Expand All @@ -10184,8 +10186,25 @@ impl App {
"stop" => self.stop_remote_control().await,
"" | "debug" => self.open_remote_control_popup(rest_pw).await,
"cloudflare" | "cloudflared" => {
self.start_remote_control_provider(TunnelProvider::Cloudflare, rest_pw)
.await
self.start_remote_control_provider(
TunnelProvider::Cloudflare,
rest_pw,
None,
).await
}
"construct" | "zarvis" => {
let subdomain = (!rest.is_empty()).then(|| rest.to_string());
if subdomain.is_none() {
self.set_status(
"usage: /remote-control construct <subdomain>".to_string(),
);
} else {
self.start_remote_control_provider(
TunnelProvider::Construct,
None,
subdomain,
).await;
}
}
_ => {
// Everything (including any trailing
Expand Down Expand Up @@ -10339,13 +10358,19 @@ impl App {
&mut self,
provider: construct_protocol::TunnelProvider,
password: Option<String>,
subdomain: Option<String>,
) {
if let Some(task) = self.remote_control_task.take() {
task.abort();
}
match self
.client
.remote_start_with_wait(provider, password.clone(), false)
.remote_start_named_with_wait(
provider,
password.clone(),
subdomain.clone(),
false,
)
.await
{
Ok(r) => {
Expand All @@ -10367,7 +10392,9 @@ impl App {

let client = self.client.clone();
self.remote_control_task = Some(tokio::spawn(async move {
let result = client.remote_start_with_wait(provider, password, true).await;
let result = client
.remote_start_named_with_wait(provider, password, subdomain, true)
.await;
(provider, result)
}));
}
Expand Down Expand Up @@ -10437,7 +10464,11 @@ impl App {
);
return true;
}
self.start_remote_control_provider(opt.provider, None).await;
self.start_remote_control_provider(
opt.provider,
None,
std::env::var("CONSTRUCT_TUNNEL_SUBDOMAIN").ok(),
).await;
}
_ => {}
},
Expand Down
3 changes: 3 additions & 0 deletions crates/cli/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15647,6 +15647,9 @@ fn render_remote_choose<'a>(
construct_protocol::TunnelProvider::Cloudflare => {
"Anyone with the URL. Rotates each run.".to_string()
}
construct_protocol::TunnelProvider::Construct => {
"Stable name. Visitors sign in with GitHub or Google.".to_string()
}
construct_protocol::TunnelProvider::None => String::new(),
},
app.theme.dim,
Expand Down
15 changes: 14 additions & 1 deletion crates/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,17 +297,30 @@ impl Client {
provider: construct_protocol::TunnelProvider,
password: Option<String>,
) -> Result<construct_protocol::RemoteStartResult> {
self.remote_start_with_wait(provider, password, true).await
self.remote_start_named_with_wait(provider, password, None, true)
.await
}
pub async fn remote_start_with_wait(
&self,
provider: construct_protocol::TunnelProvider,
password: Option<String>,
wait_for_tunnel: bool,
) -> Result<construct_protocol::RemoteStartResult> {
self.remote_start_named_with_wait(provider, password, None, wait_for_tunnel)
.await
}

pub async fn remote_start_named_with_wait(
&self,
provider: construct_protocol::TunnelProvider,
password: Option<String>,
subdomain: Option<String>,
wait_for_tunnel: bool,
) -> Result<construct_protocol::RemoteStartResult> {
let params = construct_protocol::RemoteStartParams {
provider,
password,
subdomain,
wait_for_tunnel,
};
self.request(ipc_method::REMOTE_START, &params).await
Expand Down
1 change: 1 addition & 0 deletions crates/daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ base64.workspace = true
tokio-tungstenite.workspace = true
qrcode.workspace = true
httparse.workspace = true
reqwest.workspace = true

[dev-dependencies]
tempfile = "3"
5 changes: 4 additions & 1 deletion crates/daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ pub async fn run(socket_override: Option<PathBuf>) -> Result<()> {
let params = construct_protocol::RemoteStartParams {
provider: boot_tunnel_provider(),
password: None,
subdomain: std::env::var("CONSTRUCT_TUNNEL_SUBDOMAIN").ok(),
wait_for_tunnel: true,
};
if let Err(e) = mgr.start_remote(Some(port), params).await {
Expand Down Expand Up @@ -279,6 +280,7 @@ pub async fn run(socket_override: Option<PathBuf>) -> Result<()> {
let params = construct_protocol::RemoteStartParams {
provider,
password: None,
subdomain: std::env::var("CONSTRUCT_TUNNEL_SUBDOMAIN").ok(),
wait_for_tunnel: true,
};
// port_hint=None — the supervisor reads the snapshot
Expand Down Expand Up @@ -386,11 +388,12 @@ fn boot_tunnel_provider() -> construct_protocol::TunnelProvider {
};
match raw.trim().to_ascii_lowercase().as_str() {
"cloudflare" | "cloudflared" => TunnelProvider::Cloudflare,
"construct" | "zarvis" => TunnelProvider::Construct,
"none" | "off" | "lan" => TunnelProvider::None,
other => {
tracing::warn!(
value = %other,
"CONSTRUCT_REMOTE_PROVIDER is not one of cloudflare|none; \
"CONSTRUCT_REMOTE_PROVIDER is not one of cloudflare|construct|none; \
defaulting to cloudflare"
);
TunnelProvider::Cloudflare
Expand Down
17 changes: 16 additions & 1 deletion crates/daemon/src/remote_supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ pub struct StartRequest {
/// this field — `/remote-stop` + `/remote-control <new-pw>`
/// is the recommended way to change the password mid-session.
pub password: Option<String>,
/// Optional stable name requested from a named tunnel provider.
pub subdomain: Option<String>,
pub respond: oneshot::Sender<Result<StartOutcome>>,
}

Expand Down Expand Up @@ -185,6 +187,7 @@ pub async fn run(manager: Arc<SessionManager>, mut rx: mpsc::UnboundedReceiver<S
req.port_hint,
req.provider,
req.password,
req.subdomain,
)
.await;
let _ = req.respond.send(outcome);
Expand All @@ -209,6 +212,7 @@ async fn handle_start(
port_hint: Option<u16>,
provider: TunnelProvider,
password: Option<String>,
subdomain: Option<String>,
) -> Result<StartOutcome> {
// Fast path: listener already installed (previous request, or
// boot-time env-var startup). Reuse it and only kick the
Expand All @@ -225,6 +229,17 @@ async fn handle_start(
bind_and_install(manager, ws_task, port_hint, password).await?
};

// The listener is shared, but providers are mutually exclusive. If
// the user chooses a different provider, stop the old child before
// starting the new one so its URL can never be mislabeled as the
// newly requested provider.
if provider != TunnelProvider::None
&& tunnel_task.is_some()
&& state.tunnel_provider() != provider
{
handle_stop_tunnel(manager, tunnel_task).await;
}

// Spawn the provider's tunnel at most once per current RemoteState
// lifetime. The tunnel itself is restart-on-death inside
// `tunnel::run`, so we don't track health here — only whether we
Expand All @@ -241,7 +256,7 @@ async fn handle_start(
let adopt_pid = state.tunnel_pid();
let st = state.clone();
let handle = tokio::spawn(async move {
crate::tunnel::run(provider, st, port, adopt_pid).await;
crate::tunnel::run(provider, st, port, adopt_pid, subdomain).await;
});
*tunnel_task = Some(handle);
} else {
Expand Down
1 change: 1 addition & 0 deletions crates/daemon/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,7 @@ impl SessionManager {
port_hint,
provider: params.provider,
password: params.password.clone(),
subdomain: params.subdomain.clone(),
respond: tx,
},
))
Expand Down
Loading
Loading