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
43 changes: 34 additions & 9 deletions src/browser.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,48 @@
//! Cross-platform "open URL in browser".

use std::process::{Command, Stdio};
use std::process::{Child, Command, Stdio};

/// Opens `url` in the user's default browser. Best-effort and non-fatal: errors
/// (e.g. no browser, headless) are swallowed, since the caller is expected to
/// have already printed the URL for manual opening. The child is detached so the
/// CLI does not block on it.
pub fn open_browser(url: &str) {
for mut cmd in openers(url) {
if cmd
.stdin(Stdio::null())
launch(url);
}

/// [`open_browser`] for `orx up`, reporting an opener that fails to spawn or
/// exits non-zero (e.g. `xdg-open` without a display) to telemetry.
/// Returns the watcher so a command about to exit can await it.
pub fn open_dashboard(
url: &str,
mode: crate::telemetry::UpLaunchMode,
) -> Option<tokio::task::JoinHandle<()>> {
let Some(mut child) = launch(url) else {
crate::telemetry::capture_browser_open_failed(mode);
return None;
};
// Openers that run the browser in the foreground never exit; stop watching then.
Some(tokio::spawn(async move {
for _ in 0..50 {
match child.try_wait() {
Ok(Some(status)) if !status.success() => {
return crate::telemetry::capture_browser_open_failed(mode)
}
Ok(None) => tokio::time::sleep(std::time::Duration::from_millis(100)).await,
_ => return,
}
}
}))
}

fn launch(url: &str) -> Option<Child> {
openers(url).into_iter().find_map(|mut cmd| {
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.is_ok()
{
return;
}
}
.ok()
})
}

/// The open commands to try, in order. On WSL the URL goes to the Windows
Expand Down
8 changes: 6 additions & 2 deletions src/commands/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ pub async fn run(args: UpArgs) -> Result<()> {
let url = format!("http://127.0.0.1:{port}");
eprintln!("orx up: already running — opening {url}");
if !args.no_browser {
browser::open_browser(&url);
if let Some(watch) =
browser::open_dashboard(&url, crate::telemetry::UpLaunchMode::of(&args))
{
let _ = watch.await;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
}
return Ok(());
}
Expand Down Expand Up @@ -228,7 +232,7 @@ pub async fn run(args: UpArgs) -> Result<()> {
eprintln!("orx up: warning: {warning}");
}
if !args.no_browser {
browser::open_browser(&url);
browser::open_dashboard(&url, crate::telemetry::UpLaunchMode::of(&args));
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/commands/up_remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1673,7 +1673,7 @@ pub async fn run(host: &str, args: UpArgs) -> Result<()> {
};
eprintln!("orx up --remote: dashboard on {}", session.gateway_url);
if !args.no_browser {
browser::open_browser(&session.gateway_url);
browser::open_dashboard(&session.gateway_url, crate::telemetry::UpLaunchMode::Remote);
}
eprintln!("orx up --remote: press Ctrl-C to stop.");
let _ = tokio::signal::ctrl_c().await;
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,10 @@ async fn main() {
telemetry::set_flag(cli.no_telemetry);
let session = telemetry::TelemetrySession::start(
should_capture_command(&command).then(|| command_name(&command)),
match &command {
Command::Up(args) => Some(telemetry::UpLaunchMode::of(args)),
_ => None,
},
);

let result = dispatch(command).await;
Expand Down
97 changes: 93 additions & 4 deletions src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,38 @@ pub(crate) enum ProjectCreationMode {
Paper,
}

/// How `orx up` presents the dashboard. The desktop app reports `app_started` instead.
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum UpLaunchMode {
Browser,
NoBrowser,
/// Plain `orx up` inside an SSH session, which prints forwarding steps instead.
Ssh,
Remote,
RemoteHost,
}

impl UpLaunchMode {
pub(crate) fn of(args: &crate::UpArgs) -> Self {
if args.remote_host {
Self::RemoteHost
} else if args.remote.is_some() {
Self::Remote
} else if args.no_browser {
Self::NoBrowser
} else if crate::remote::detect_ssh_session().is_some() {
Self::Ssh
} else {
Self::Browser
}
}
}

pub(crate) fn capture_browser_open_failed(mode: UpLaunchMode) {
capture("browser_open_failed", json!({ "mode": mode }));
}

pub(crate) fn capture_project_created(local: bool, mode: Option<ProjectCreationMode>) {
let mut properties = json!({ "local": local });
if let Some(mode) = mode {
Expand Down Expand Up @@ -1088,11 +1120,15 @@ impl TelemetrySession {
/// The `--no-telemetry` flag is read from the process-global (set in `main`
/// before this is called), matching every other event path. The handle is
/// registered in the pending set and flushed by `finish`.
pub(crate) fn start(command: Option<&str>) -> TelemetrySession {
pub(crate) fn start(command: Option<&str>, mode: Option<UpLaunchMode>) -> TelemetrySession {
retry_outbox();
if let Some(command) = command {
// Bare base name; `build_payload` prefixes it → wire event `cli_command`.
capture("command", json!({ "command": command }));
let mut properties = json!({ "command": command });
if let Some(mode) = mode {
properties["mode"] = json!(mode);
}
capture("command", properties);
}
TelemetrySession
}
Expand Down Expand Up @@ -2100,9 +2136,34 @@ mod tests {
json!({ "kind": "run", "local": true, "computeTarget": "local" }),
),
];
let launch_modes = [
UpLaunchMode::Browser,
UpLaunchMode::NoBrowser,
UpLaunchMode::Ssh,
UpLaunchMode::Remote,
UpLaunchMode::RemoteHost,
];
let launch_payloads = launch_modes.into_iter().flat_map(|mode| {
[
build_payload(
"command",
"cli-release-contract-test",
json!({ "command": "up", "mode": mode }),
),
build_payload(
"browser_open_failed",
"cli-release-contract-test",
json!({ "mode": mode }),
),
]
});
let mut localized = build_payload("app_started", "cli-release-contract-test", json!({}));
localized["context"]["locale"] = json!("zh-CN");
for payload in payloads.into_iter().chain([localized]) {
for payload in payloads
.into_iter()
.chain(launch_payloads)
.chain([localized])
{
assert_eq!(post_payload(&payload).await, DeliveryOutcome::Acknowledged);
}
}
Expand Down Expand Up @@ -2241,6 +2302,33 @@ mod tests {
assert!(serde_json::from_value::<ProjectCreationMode>(json!("/private/path")).is_err());
}

#[test]
fn up_launch_modes_follow_the_dashboard_code_paths() {
let _g = EnvGuard::new(&["SSH_CONNECTION", "SSH_TTY", "SSH_CLIENT"]);
let args = |argv: &[&str]| {
use clap::Parser;
match crate::Cli::parse_from(argv).command {
Some(crate::Command::Up(args)) => UpLaunchMode::of(&args),
_ => unreachable!(),
}
};
let cases = [
(&["orx", "up"][..], "browser"),
(&["orx", "up", "--no-browser"], "no_browser"),
(&["orx", "up", "--remote", "box"], "remote"),
(
&["orx", "up", "--no-browser", "--remote-host"],
"remote_host",
),
];
for (argv, mode) in cases {
assert_eq!(json!(args(argv)), json!(mode), "{argv:?}");
}
std::env::set_var("SSH_CONNECTION", "1.2.3.4 5 6.7.8.9 22");
assert_eq!(args(&["orx", "up"]), UpLaunchMode::Ssh);
assert_eq!(json!(UpLaunchMode::Ssh), json!("ssh"));
}

#[tokio::test]
async fn environment_disabled_consent_never_creates_an_install_id() {
let _g = EnvGuard::new(OPT_VARS);
Expand Down Expand Up @@ -2271,12 +2359,13 @@ mod tests {
}
assert!(environment_disabled_reason().is_some());

let session = TelemetrySession::start(Some("up"));
let session = TelemetrySession::start(Some("up"), Some(UpLaunchMode::Browser));
harness::capture_initial(&json!({"harnesses":[]}));
harness::SetupAttempt::new("opencode", "install", "automatic");
capture_onboarding_completed();
capture_onboarding_research_profile(&ResearchProfile::default());
capture_project_created(true, Some(ProjectCreationMode::Blank));
capture_browser_open_failed(UpLaunchMode::Browser);
capture_demo_welcome_choice("explore_demo");
capture_chat_session_started("codex");
capture_chat_message_sent("codex");
Expand Down
Loading