opencode_ctl/launcher/
posix.rs1use std::path::Path;
4
5use super::windows::{wt_directory_arg, WindowsShell};
6use super::WrapperInput;
7
8#[cfg(target_os = "linux")]
11pub const BOOT_ID_EXPR_POSIX: &str = "cat /proc/sys/kernel/random/boot_id";
12#[cfg(target_os = "linux")]
15pub const STARTTIME_EXPR_POSIX: &str = "s=$(cat /proc/$$/stat); s=${s##*) }; set -- $s; echo ${20}";
16
17#[cfg(target_os = "macos")]
20pub const BOOT_ID_EXPR_POSIX: &str =
21 r#"sysctl -n kern.boottime | sed 's/.*[^u]sec = \([0-9]*\).*/\1/'"#;
22#[cfg(target_os = "macos")]
25pub const STARTTIME_EXPR_POSIX: &str =
26 r#"TZ=UTC ps -o lstart= -p $$ | { read l; TZ=UTC date -j -f '%a %b %e %T %Y' "$l" +%s; }"#;
27
28#[cfg(not(any(target_os = "linux", target_os = "macos")))]
31pub const BOOT_ID_EXPR_POSIX: &str = "cat /proc/sys/kernel/random/boot_id";
32#[cfg(not(any(target_os = "linux", target_os = "macos")))]
33pub const STARTTIME_EXPR_POSIX: &str = "s=$(cat /proc/$$/stat); s=${s##*) }; set -- $s; echo ${20}";
34
35pub fn quote_sh(v: &str) -> String {
37 format!("'{}'", v.replace('\'', r"'\''"))
38}
39
40pub fn wrapper_script_posix(input: &WrapperInput) -> String {
44 let q = |p: &Path| quote_sh(&p.to_string_lossy());
45 let pair_file = |suffix: &str| {
46 input.home.join(format!(
47 "pair.{}.g{}.{suffix}",
48 input.pair_id, input.generation
49 ))
50 };
51 let (boot, start) = if input.boot_id_expr {
52 (
53 format!("$({BOOT_ID_EXPR_POSIX})"),
54 format!("$({STARTTIME_EXPR_POSIX})"),
55 )
56 } else {
57 ("BOOT".to_string(), "START".to_string())
58 };
59 let mut s = String::new();
60 s.push_str("#!/usr/bin/env bash\n");
61 s.push_str(&format!(
62 "# opencode-ctl tab wrapper: pair {} generation {}\n",
63 input.pair_id, input.generation
64 ));
65 s.push_str(&format!(
66 "on_exit() {{ ( umask 077; printf '%s' \"$(date +%s)000\" > {} ); }}\n",
67 q(&pair_file("tui-exited"))
68 ));
69 s.push_str("trap on_exit EXIT\n");
70 s.push_str(&format!("boot={boot}\n"));
71 s.push_str(&format!("start={start}\n"));
72 s.push_str(&format!(
73 "( umask 077; printf '%s' \"$$:$boot:$start\" > {} )\n",
74 q(&pair_file("tui-pid"))
75 ));
76 if let (Some(_), Some(pw)) = (input.server_url, input.server_password) {
77 s.push_str(&format!(
78 "export OPENCODE_SERVER_PASSWORD={}\n",
79 quote_sh(pw)
80 ));
81 s.push_str("unset OPENCODE_PASSWORD\n");
82 }
83 s.push_str(&format!(
84 "{} {} -s {}",
85 q(input.opencode_bin),
86 q(input.directory),
87 quote_sh(input.session_id)
88 ));
89 if let Some(url) = input.server_url {
90 s.push_str(&format!(" --server {}", quote_sh(url)));
91 }
92 s.push('\n');
93 s
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum Terminal {
100 Mate,
101 Gnome,
102 Xfce,
103 Konsole,
104 Wt,
105}
106
107impl Terminal {
108 pub fn program(self) -> &'static str {
109 match self {
110 Terminal::Mate => "mate-terminal",
111 Terminal::Gnome => "gnome-terminal",
112 Terminal::Xfce => "xfce4-terminal",
113 Terminal::Konsole => "konsole",
114 Terminal::Wt => "wt",
115 }
116 }
117}
118
119const PATH_ORDER: [Terminal; 4] = [
121 Terminal::Mate,
122 Terminal::Gnome,
123 Terminal::Xfce,
124 Terminal::Konsole,
125];
126
127fn terminal_for_process(name: &str) -> Option<Terminal> {
128 let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
129 match base {
130 "mate-terminal" => Some(Terminal::Mate),
131 "gnome-terminal" | "gnome-terminal-server" => Some(Terminal::Gnome),
133 "xfce4-terminal" => Some(Terminal::Xfce),
134 "konsole" => Some(Terminal::Konsole),
135 _ => None,
136 }
137}
138
139pub fn select_terminal(
142 parent_chain: &[String],
143 path_has: &dyn Fn(&str) -> bool,
144 windows: bool,
145) -> Option<Terminal> {
146 if windows {
147 return path_has("wt").then_some(Terminal::Wt);
148 }
149 parent_chain
150 .iter()
151 .find_map(|n| terminal_for_process(n))
152 .or_else(|| PATH_ORDER.into_iter().find(|t| path_has(t.program())))
153}
154
155pub fn tab_command(
160 t: Terminal,
161 script: &Path,
162 directory: &Path,
163 shell: WindowsShell,
164) -> Vec<String> {
165 let script = script.to_string_lossy().into_owned();
166 let argv = |parts: &[&str]| parts.iter().map(|p| p.to_string()).collect::<Vec<_>>();
167 match t {
168 Terminal::Mate | Terminal::Xfce => {
169 let mut v = argv(&[t.program(), "--tab", "-e"]);
170 v.push(quote_sh(&script));
171 v
172 }
173 Terminal::Gnome => {
174 let mut v = argv(&["gnome-terminal", "--tab", "--"]);
175 v.push(script);
176 v
177 }
178 Terminal::Konsole => {
179 let mut v = argv(&["konsole", "--new-tab", "-e"]);
180 v.push(script);
181 v
182 }
183 Terminal::Wt => {
184 let mut v = argv(&["wt", "-w", "0", "nt", "-d"]);
185 v.push(wt_directory_arg(&directory.to_string_lossy()));
186 v.extend(argv(&[
187 shell.program(),
188 "-NoProfile",
189 "-ExecutionPolicy",
190 "Bypass",
191 "-File",
192 ]));
193 v.push(script);
194 v
195 }
196 }
197}
198
199pub fn system_parent_chain() -> Vec<String> {
202 use crate::liveness::{ProcessProbe, SystemProbe};
203 let mut chain = Vec::new();
204 let mut pid = SystemProbe.parent_pid(std::process::id());
205 while let Some(p) = pid.filter(|p| *p > 1 && chain.len() < 32) {
206 if let Some(name) = process_name(p) {
207 chain.push(name);
208 }
209 pid = SystemProbe.parent_pid(p);
210 }
211 chain
212}
213
214#[cfg(target_os = "linux")]
215fn process_name(pid: u32) -> Option<String> {
216 let raw = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
217 let first = raw.split(|b| *b == 0).next().filter(|f| !f.is_empty())?;
218 let first = String::from_utf8_lossy(first).into_owned();
219 Some(first.rsplit('/').next().unwrap_or(&first).to_string())
220}
221
222#[cfg(target_os = "macos")]
223fn process_name(pid: u32) -> Option<String> {
224 let out = std::process::Command::new("ps")
225 .args(["-o", "comm=", "-p", &pid.to_string()])
226 .output()
227 .ok()?;
228 let name = String::from_utf8_lossy(&out.stdout).trim().to_string();
229 (!name.is_empty()).then(|| name.rsplit('/').next().unwrap_or(&name).to_string())
230}
231
232#[cfg(not(any(target_os = "linux", target_os = "macos")))]
233fn process_name(_pid: u32) -> Option<String> {
234 None
235}