1use std::io::{BufRead, Write};
19use std::net::{SocketAddr, TcpStream};
20use std::path::PathBuf;
21use std::process::{Child, Command};
22use std::time::{Duration, Instant};
23
24use anyhow::{Context, Result, bail};
25use clap::Args;
26
27use crate::{cargo, workspace_root};
28
29#[derive(Args)]
30pub struct DemoArgs {
31 #[arg(long, default_value_t = 2)]
33 clients: usize,
34
35 #[arg(long, default_value = "127.0.0.1:4100")]
37 listen: String,
38
39 #[arg(long)]
42 db: Option<PathBuf>,
43
44 #[arg(long)]
47 narrate: bool,
48
49 #[arg(long)]
51 release: bool,
52}
53
54pub fn run(args: &DemoArgs) -> Result<()> {
55 if args.clients == 0 {
56 bail!("--clients must be at least 1");
57 }
58 let addr: SocketAddr = args
59 .listen
60 .parse()
61 .with_context(|| format!("--listen {} is not an address", args.listen))?;
62
63 build(args)?;
64
65 let scratch = args.db.is_none();
66 let db = args.db.clone().unwrap_or_else(|| {
67 workspace_root()
68 .join("target")
69 .join("demo-scratch.blockworx.db")
70 });
71 if scratch && db.exists() {
72 std::fs::remove_file(&db).ok();
73 }
74
75 let mut session = Session {
76 server: spawn_server(args, &db, &args.listen)?,
77 clients: Vec::new(),
78 db: scratch.then_some(db.clone()),
79 frozen: false,
80 };
81 wait_for(addr).context("the server never accepted a connection")?;
82
83 let url = format!("ws://{addr}/ws");
84 for _ in 0..args.clients {
85 session.clients.push(spawn_client(args, &url)?);
86 }
87
88 eprintln!();
89 eprintln!(" {} editors on {}", args.clients, db.display());
90 eprintln!(" serving {url}");
91 eprintln!();
92 eprintln!(" Edit in either window; the other follows. To see the case hands");
93 eprintln!(" cannot reach, wait for both titles to say `live`, then freeze,");
94 eprintln!(" edit the same block in both, and thaw: the later commit wins and");
95 eprintln!(" both canvases agree.");
96 eprintln!();
97 console(&mut session)
98}
99
100struct Session {
104 server: Child,
105 clients: Vec<Child>,
106 db: Option<PathBuf>,
107 frozen: bool,
108}
109
110impl Drop for Session {
111 fn drop(&mut self) {
112 if self.frozen {
113 signal(&self.server, "-CONT");
115 }
116 for child in self.clients.iter_mut().chain([&mut self.server]) {
117 child.kill().ok();
118 child.wait().ok();
119 }
120 if let Some(db) = &self.db {
121 std::fs::remove_file(db).ok();
122 }
123 }
124}
125
126fn console(session: &mut Session) -> Result<()> {
127 let stdin = std::io::stdin();
128 loop {
129 eprint!(
130 "{} [f]reeze [t]haw [q]uit > ",
131 if session.frozen { "frozen" } else { "live " }
132 );
133 std::io::stderr().flush().ok();
134
135 let mut line = String::new();
136 if stdin.lock().read_line(&mut line)? == 0 {
137 return Ok(());
138 }
139 match line.trim() {
140 "f" | "freeze" if !session.frozen => {
141 signal(&session.server, "-STOP");
142 session.frozen = true;
143 eprintln!(" server stopped — edits now queue in each editor, unacknowledged");
144 }
145 "t" | "thaw" if session.frozen => {
146 signal(&session.server, "-CONT");
147 session.frozen = false;
148 eprintln!(" server running — the queues flush and the titles catch up");
149 }
150 "f" | "t" | "freeze" | "thaw" => eprintln!(" already there"),
151 "q" | "quit" | "" => return Ok(()),
152 other => eprintln!(" don't know {other:?}"),
153 }
154 }
155}
156
157fn build(args: &DemoArgs) -> Result<()> {
158 let mut argv = vec![
159 "build",
160 "-p",
161 "blockworx",
162 "--bin",
163 "blockworx",
164 "-p",
165 "blockworx-server",
166 "--bin",
167 "blockworx-server",
168 ];
169 if args.release {
170 argv.push("--release");
171 }
172 cargo(&argv).run().context("building the demo binaries")?;
173 Ok(())
174}
175
176fn bin(args: &DemoArgs, name: &str) -> PathBuf {
177 workspace_root()
178 .join("target")
179 .join(if args.release { "release" } else { "debug" })
180 .join(name)
181}
182
183fn spawn_server(args: &DemoArgs, db: &PathBuf, listen: &str) -> Result<Child> {
184 Command::new(bin(args, "blockworx-server"))
185 .arg(db)
186 .args(["--listen", listen])
187 .current_dir(workspace_root())
188 .spawn()
189 .context("starting blockworx-server")
190}
191
192fn spawn_client(args: &DemoArgs, url: &str) -> Result<Child> {
193 let mut cmd = Command::new(bin(args, "blockworx"));
194 cmd.args(["--connect", url]).current_dir(workspace_root());
195 if args.narrate {
196 cmd.env("RUST_LOG", "blockworx=info,edit=debug");
197 }
198 cmd.spawn().context("starting blockworx")
199}
200
201fn signal(child: &Child, sig: &str) {
204 Command::new("kill")
205 .args([sig, &child.id().to_string()])
206 .status()
207 .ok();
208}
209
210fn wait_for(addr: SocketAddr) -> Result<()> {
211 let deadline = Instant::now() + Duration::from_secs(20);
212 while Instant::now() < deadline {
213 if TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_ok() {
214 return Ok(());
215 }
216 std::thread::sleep(Duration::from_millis(100));
217 }
218 bail!("{addr} did not come up within 20s")
219}