feat(client): demo mode (--demo or select demo) auto-spawns local agent on 3231
This commit is contained in:
parent
e7eb3e6557
commit
9491dc50a8
@ -241,7 +241,7 @@ Select profile:
|
|||||||
Enter number (or blank to abort): 2
|
Enter number (or blank to abort): 2
|
||||||
```
|
```
|
||||||
|
|
||||||
Choosing a number starts the TUI with that profile. Pressing Enter on blank aborts without connecting.
|
Choosing a number starts the TUI with that profile. A built‑in `demo` option is always appended; selecting it launches a local agent on port 3231 (no TLS) and connects to `ws://127.0.0.1:3231/ws`. Pressing Enter on blank aborts without connecting.
|
||||||
|
|
||||||
### JSON format
|
### JSON format
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ struct ParsedArgs {
|
|||||||
tls_ca: Option<String>,
|
tls_ca: Option<String>,
|
||||||
profile: Option<String>,
|
profile: Option<String>,
|
||||||
save: bool,
|
save: bool,
|
||||||
|
demo: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
||||||
@ -25,13 +26,14 @@ fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, Str
|
|||||||
let mut url: Option<String> = None;
|
let mut url: Option<String> = None;
|
||||||
let mut tls_ca: Option<String> = None;
|
let mut tls_ca: Option<String> = None;
|
||||||
let mut profile: Option<String> = None;
|
let mut profile: Option<String> = None;
|
||||||
let mut save = false; // --save-profile
|
let mut save = false; // --save
|
||||||
|
let mut demo = false; // --demo
|
||||||
|
|
||||||
while let Some(arg) = it.next() {
|
while let Some(arg) = it.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"-h" | "--help" => {
|
"-h" | "--help" => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [ws://HOST:PORT/ws]"
|
"Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
"--tls-ca" | "-t" => {
|
"--tls-ca" | "-t" => {
|
||||||
@ -43,6 +45,9 @@ fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, Str
|
|||||||
"--save" => {
|
"--save" => {
|
||||||
save = true;
|
save = true;
|
||||||
}
|
}
|
||||||
|
"--demo" => {
|
||||||
|
demo = true;
|
||||||
|
}
|
||||||
_ if arg.starts_with("--tls-ca=") => {
|
_ if arg.starts_with("--tls-ca=") => {
|
||||||
if let Some((_, v)) = arg.split_once('=') {
|
if let Some((_, v)) = arg.split_once('=') {
|
||||||
if !v.is_empty() {
|
if !v.is_empty() {
|
||||||
@ -62,7 +67,7 @@ fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, Str
|
|||||||
url = Some(arg);
|
url = Some(arg);
|
||||||
} else {
|
} else {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Unexpected argument. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [ws://HOST:PORT/ws]"
|
"Unexpected argument. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,6 +78,7 @@ fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, Str
|
|||||||
tls_ca,
|
tls_ca,
|
||||||
profile,
|
profile,
|
||||||
save,
|
save,
|
||||||
|
demo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +93,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Demo mode short-circuit (ignore other args except conflicting ones)
|
||||||
|
if parsed.demo || matches!(parsed.profile.as_deref(), Some("demo")) {
|
||||||
|
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||||
|
}
|
||||||
|
|
||||||
let profiles_file = load_profiles();
|
let profiles_file = load_profiles();
|
||||||
let req = ProfileRequest {
|
let req = ProfileRequest {
|
||||||
profile_name: parsed.profile.clone(),
|
profile_name: parsed.profile.clone(),
|
||||||
@ -141,7 +152,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
(u, t)
|
(u, t)
|
||||||
}
|
}
|
||||||
ResolveProfile::Loaded(u, t) => (u, t),
|
ResolveProfile::Loaded(u, t) => (u, t),
|
||||||
ResolveProfile::PromptSelect(names) => {
|
ResolveProfile::PromptSelect(mut names) => {
|
||||||
|
// Always add demo option to list
|
||||||
|
if !names.iter().any(|n| n == "demo") { names.push("demo".into()); }
|
||||||
eprintln!("Select profile:");
|
eprintln!("Select profile:");
|
||||||
for (i, n) in names.iter().enumerate() {
|
for (i, n) in names.iter().enumerate() {
|
||||||
eprintln!(" {}. {}", i + 1, n);
|
eprintln!(" {}. {}", i + 1, n);
|
||||||
@ -153,6 +166,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if let Ok(idx) = line.trim().parse::<usize>() {
|
if let Ok(idx) = line.trim().parse::<usize>() {
|
||||||
if idx >= 1 && idx <= names.len() {
|
if idx >= 1 && idx <= names.len() {
|
||||||
let name = &names[idx - 1];
|
let name = &names[idx - 1];
|
||||||
|
if name == "demo" { return run_demo_mode(parsed.tls_ca.as_deref()).await; }
|
||||||
if let Some(entry) = profiles_mut.profiles.get(name) {
|
if let Some(entry) = profiles_mut.profiles.get(name) {
|
||||||
(entry.url.clone(), entry.tls_ca.clone())
|
(entry.url.clone(), entry.tls_ca.clone())
|
||||||
} else {
|
} else {
|
||||||
@ -218,3 +232,53 @@ fn prompt_string(prompt: &str) -> io::Result<String> {
|
|||||||
io::stdin().read_line(&mut line)?;
|
io::stdin().read_line(&mut line)?;
|
||||||
Ok(line)
|
Ok(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Demo Mode ---
|
||||||
|
|
||||||
|
async fn run_demo_mode(_tls_ca: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let port = 3231;
|
||||||
|
let url = format!("ws://127.0.0.1:{port}/ws");
|
||||||
|
let child = spawn_demo_agent(port)?;
|
||||||
|
// Use select to handle Ctrl-C and normal quit
|
||||||
|
let mut app = App::new();
|
||||||
|
tokio::select! {
|
||||||
|
res = app.run(&url, None) => { drop(child); res }
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
// Drop child (kills agent) then return
|
||||||
|
drop(child);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DemoGuard(std::sync::Arc<std::sync::Mutex<Option<std::process::Child>>>);
|
||||||
|
impl Drop for DemoGuard { fn drop(&mut self) { if let Some(mut ch) = self.0.lock().unwrap().take() { let _ = ch.kill(); } } }
|
||||||
|
|
||||||
|
fn spawn_demo_agent(port: u16) -> Result<DemoGuard, Box<dyn std::error::Error>> {
|
||||||
|
let candidate = find_agent_executable();
|
||||||
|
let mut cmd = std::process::Command::new(candidate);
|
||||||
|
cmd.arg("--port").arg(port.to_string());
|
||||||
|
cmd.env("SOCKTOP_ENABLE_SSL", "0");
|
||||||
|
cmd.env("SOCKTOP_AGENT_GPU", "0");
|
||||||
|
cmd.env("SOCKTOP_AGENT_TEMP", "0");
|
||||||
|
let child = cmd.spawn()?;
|
||||||
|
// Give the agent a brief moment to start
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||||
|
Ok(DemoGuard(std::sync::Arc::new(std::sync::Mutex::new(Some(child)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_agent_executable() -> std::path::PathBuf {
|
||||||
|
let self_exe = std::env::current_exe().ok();
|
||||||
|
if let Some(exe) = self_exe {
|
||||||
|
if let Some(parent) = exe.parent() {
|
||||||
|
#[cfg(windows)]
|
||||||
|
let name = "socktop_agent.exe";
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let name = "socktop_agent";
|
||||||
|
let candidate = parent.join(name);
|
||||||
|
if candidate.exists() { return candidate; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to relying on PATH
|
||||||
|
std::path::PathBuf::from("socktop_agent")
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user