launcher.rs
177 lines 5.1 KB
Raw
sha256:0e9549ec7b463911bc08b7d586dc320b1ac9b1f5c943ee7e3865dcc6cb0f6f83 chore(governance): sync handover+roadmap to 84db8c8 (drift:… Human 2 days ago
1 //! Spawn the canonical ``ok app`` process and parse its startup banner.
2
3 use std::io::{BufRead, BufReader};
4 use std::path::{Path, PathBuf};
5 use std::process::{Child, Command, Stdio};
6 use std::sync::mpsc;
7 use std::thread;
8 use std::time::{Duration, Instant};
9
10 const CANONICAL_LAUNCHER: &str = "ok";
11 const CANONICAL_SUBCOMMAND: &str = "app";
12 const DEFAULT_PORT: u16 = 8765;
13 const DEFAULT_BIND: &str = "127.0.0.1";
14 const STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
15
16 #[derive(Debug, Clone)]
17 pub struct StartupBanner {
18 pub url: String,
19 pub session_credential: String,
20 pub csrf_token: String,
21 }
22
23 pub struct OkAppChild {
24 child: Child,
25 }
26
27 impl OkAppChild {
28 pub fn kill(&mut self) {
29 let _ = self.child.kill();
30 let _ = self.child.wait();
31 }
32 }
33
34 pub fn resolve_kit_root() -> PathBuf {
35 if let Ok(root) = std::env::var("OVERSEER_KIT_ROOT") {
36 return PathBuf::from(root);
37 }
38
39 if let Ok(resource_dir) = std::env::var("TAURI_RESOURCE_DIR") {
40 let bundled = PathBuf::from(resource_dir).join("kit");
41 if bundled.join("cli").join(CANONICAL_LAUNCHER).is_file() {
42 return bundled;
43 }
44 }
45
46 if let Ok(cwd) = std::env::current_dir() {
47 for ancestor in cwd.ancestors() {
48 let shim = ancestor.join("cli").join(CANONICAL_LAUNCHER);
49 if shim.is_file() {
50 return ancestor.to_path_buf();
51 }
52 }
53 }
54
55 PathBuf::from(".")
56 }
57
58 pub fn resolve_repo_root(kit_root: &Path) -> PathBuf {
59 if let Ok(root) = std::env::var("OVERSEER_REPO_ROOT") {
60 return PathBuf::from(root);
61 }
62 kit_root.to_path_buf()
63 }
64
65 pub fn build_ok_app_command(kit_root: &Path, repo_root: &Path, port: u16) -> Command {
66 let ok_shim = kit_root.join("cli").join(CANONICAL_LAUNCHER);
67 let mut command = Command::new(ok_shim);
68 command
69 .arg(CANONICAL_SUBCOMMAND)
70 .arg("--repo")
71 .arg(repo_root)
72 .arg("--port")
73 .arg(port.to_string())
74 .arg("--bind")
75 .arg(DEFAULT_BIND)
76 .current_dir(kit_root)
77 .env("PYTHONPATH", kit_root)
78 .stdout(Stdio::null())
79 .stderr(Stdio::piped());
80 command
81 }
82
83 pub fn spawn_ok_app(kit_root: &Path, repo_root: &Path, port: u16) -> Result<(OkAppChild, StartupBanner), String> {
84 let mut child = build_ok_app_command(kit_root, repo_root, port)
85 .spawn()
86 .map_err(|err| format!("failed to spawn ok app: {err}"))?;
87
88 let stderr = child
89 .stderr
90 .take()
91 .ok_or_else(|| "ok app stderr pipe missing".to_string())?;
92
93 let (tx, rx) = mpsc::channel();
94 thread::spawn(move || {
95 let reader = BufReader::new(stderr);
96 for line in reader.lines().map_while(Result::ok) {
97 if tx.send(line).is_err() {
98 break;
99 }
100 }
101 });
102
103 let deadline = Instant::now() + STARTUP_TIMEOUT;
104 let mut lines: Vec<String> = Vec::new();
105 while Instant::now() < deadline {
106 while let Ok(line) = rx.try_recv() {
107 lines.push(line);
108 }
109 if let Some(banner) = parse_startup_stderr(&lines) {
110 return Ok((OkAppChild { child }, banner));
111 }
112 if let Some(status) = child.try_wait().ok().flatten() {
113 let tail = lines.join("\n");
114 return Err(format!("ok app exited early ({status}): {tail}"));
115 }
116 thread::sleep(Duration::from_millis(50));
117 }
118
119 let _ = child.kill();
120 Err("timed out waiting for ok app startup banner".to_string())
121 }
122
123 pub fn parse_startup_stderr(lines: &[String]) -> Option<StartupBanner> {
124 let mut url: Option<String> = None;
125 let mut session: Option<String> = None;
126 let mut csrf: Option<String> = None;
127
128 for line in lines {
129 if let Some(rest) = line.strip_prefix("url: ") {
130 url = Some(rest.trim().to_string());
131 } else if let Some(rest) = line.strip_prefix("session_credential: ") {
132 session = Some(rest.trim().to_string());
133 } else if let Some(rest) = line.strip_prefix("csrf_token: ") {
134 csrf = Some(rest.trim().to_string());
135 }
136 }
137
138 Some(StartupBanner {
139 url: url?,
140 session_credential: session?,
141 csrf_token: csrf?,
142 })
143 }
144
145 pub fn build_auth_bootstrap_script(banner: &StartupBanner) -> String {
146 let session = serde_json::to_string(&banner.session_credential).unwrap_or_else(|_| "\"\"".to_string());
147 let csrf = serde_json::to_string(&banner.csrf_token).unwrap_or_else(|_| "\"\"".to_string());
148 format!(
149 r#"
150 (function() {{
151 const session = {session};
152 const csrf = {csrf};
153 function bootstrap() {{
154 const sessionInput = document.getElementById("session-input");
155 const csrfInput = document.getElementById("csrf-input");
156 const saveButton = document.getElementById("auth-save");
157 if (!sessionInput || !csrfInput || !saveButton) {{
158 setTimeout(bootstrap, 50);
159 return;
160 }}
161 sessionInput.value = session;
162 csrfInput.value = csrf;
163 saveButton.click();
164 }}
165 if (document.readyState === "loading") {{
166 document.addEventListener("DOMContentLoaded", bootstrap);
167 }} else {{
168 bootstrap();
169 }}
170 }})();
171 "#
172 )
173 }
174
175 pub fn default_port() -> u16 {
176 DEFAULT_PORT
177 }
File History 1 commit
sha256:6abcf1fa82a7a621ccbc945f19acdba5bc0db54569599404a1452fb4a096a199 fix(ISR): default require_independent_second_reviewer to require Human minor 2 days ago