launcher.rs
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