opencode_ctl/launcher/
windows.rs1use std::path::{Path, PathBuf};
6use std::process::Command;
7
8use super::WrapperInput;
9
10pub fn strip_verbatim(p: &str) -> String {
13 if let Some(rest) = p.strip_prefix(r"\\?\UNC\") {
14 return format!(r"\\{rest}");
15 }
16 if let Some(rest) = p.strip_prefix(r"\\?\") {
17 return rest.to_string();
18 }
19 p.to_string()
20}
21
22pub fn wt_directory_arg(dir: &str) -> String {
27 dir.to_string()
28}
29
30const DEFAULT_PATHEXT: &str = ".COM;.EXE;.BAT;.CMD";
31
32fn is_cmd_ext(ext: &str) -> bool {
33 ext.eq_ignore_ascii_case(".cmd") || ext.eq_ignore_ascii_case(".bat")
34}
35
36pub fn resolve_executable(name: &str, path: &str, pathext: &str) -> Option<(PathBuf, bool)> {
41 let exts: Vec<&str> = pathext.split(';').filter(|e| !e.is_empty()).collect();
42 let own_ext = name
43 .rfind('.')
44 .map(|i| &name[i..])
45 .filter(|e| exts.iter().any(|x| x.eq_ignore_ascii_case(e)));
46 for dir in path.split(';').filter(|d| !d.is_empty()) {
47 let dir = Path::new(dir);
48 if let Some(ext) = own_ext {
49 let cand = dir.join(name);
50 if cand.is_file() {
51 return Some((cand, is_cmd_ext(ext)));
52 }
53 }
54 for ext in &exts {
55 let cand = dir.join(format!("{name}{ext}"));
56 if cand.is_file() {
57 return Some((cand, is_cmd_ext(ext)));
58 }
59 }
60 }
61 None
62}
63
64pub fn cmd_wrap(exe: &Path, args: &[String]) -> Vec<String> {
66 let mut v = vec![
67 "cmd.exe".to_string(),
68 "/c".to_string(),
69 exe.to_string_lossy().into_owned(),
70 ];
71 v.extend(args.iter().cloned());
72 v
73}
74
75pub fn cmd_line(exe: &Path, args: &[String]) -> String {
82 let quote = |a: &str| -> String {
83 if a.is_empty() || a.contains([' ', '\t']) {
84 let trailing = a.len() - a.trim_end_matches('\\').len();
85 format!("\"{a}{}\"", "\\".repeat(trailing))
86 } else {
87 a.to_string()
88 }
89 };
90 let mut parts = vec![format!("\"{}\"", exe.to_string_lossy())];
91 parts.extend(args.iter().map(|a| quote(a)));
92 format!("\"{}\"", parts.join(" "))
93}
94
95pub fn controller_process(argv: &[String]) -> Command {
99 #[cfg(windows)]
100 if argv.len() >= 3 && argv[0] == "cmd.exe" && argv[1] == "/c" {
101 use std::os::windows::process::CommandExt;
102 let mut c = Command::new("cmd.exe");
103 c.raw_arg("/c")
104 .raw_arg(cmd_line(Path::new(&argv[2]), &argv[3..]));
105 return c;
106 }
107 let mut c = Command::new(&argv[0]);
108 c.args(&argv[1..]);
109 c
110}
111
112pub fn controller_command(
116 name: &str,
117 args: &[String],
118 windows: bool,
119 path: Option<&str>,
120 pathext: Option<&str>,
121) -> Option<Vec<String>> {
122 if !windows {
123 let mut v = vec![name.to_string()];
124 v.extend(args.iter().cloned());
125 return Some(v);
126 }
127 let (exe, is_cmd) = resolve_executable(name, path?, pathext.unwrap_or(DEFAULT_PATHEXT))?;
128 if is_cmd {
129 return Some(cmd_wrap(&exe, args));
130 }
131 let mut v = vec![exe.to_string_lossy().into_owned()];
132 v.extend(args.iter().cloned());
133 Some(v)
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum WindowsShell {
138 Pwsh,
139 PowershellExe,
140}
141
142impl WindowsShell {
143 pub fn program(self) -> &'static str {
144 match self {
145 WindowsShell::Pwsh => "pwsh",
146 WindowsShell::PowershellExe => "powershell.exe",
147 }
148 }
149}
150
151pub fn select_windows_shell(path_has: &dyn Fn(&str) -> bool) -> WindowsShell {
153 if path_has("pwsh") {
154 WindowsShell::Pwsh
155 } else {
156 WindowsShell::PowershellExe
157 }
158}
159
160pub fn quote_ps(v: &str) -> String {
163 let mut out = String::with_capacity(v.len() + 2);
164 out.push('\'');
165 for c in v.chars() {
166 out.push(c);
167 if matches!(c, '\'' | '\u{2018}' | '\u{2019}' | '\u{201a}' | '\u{201b}') {
168 out.push(c);
169 }
170 }
171 out.push('\'');
172 out
173}
174
175pub const BOOT_EXPR_PS1: &str = "[math]::Floor([DateTimeOffset]::new((Get-CimInstance Win32_OperatingSystem).LastBootUpTime).ToUnixTimeSeconds() / 60) * 60";
178
179pub const START_EXPR_PS1: &str = "(Get-Process -Id $PID).StartTime.ToFileTime()";
181
182pub fn wrapper_script_ps1(input: &WrapperInput) -> String {
186 let q = |p: &Path| quote_ps(&p.to_string_lossy());
187 let pair_file = |suffix: &str| {
188 input.home.join(format!(
189 "pair.{}.g{}.{suffix}",
190 input.pair_id, input.generation
191 ))
192 };
193 let (boot, start) = if input.boot_id_expr {
194 (BOOT_EXPR_PS1, START_EXPR_PS1)
195 } else {
196 ("'BOOT'", "'START'")
197 };
198 let mut s = String::new();
199 s.push_str(&format!(
200 "# opencode-ctl tab wrapper: pair {} generation {}\n",
201 input.pair_id, input.generation
202 ));
203 s.push_str(&format!("$tuiPid = {}\n", q(&pair_file("tui-pid"))));
204 s.push_str(&format!("$tuiExited = {}\n", q(&pair_file("tui-exited"))));
205 s.push_str(&format!("$boot = {boot}\n"));
206 s.push_str(&format!("$start = {start}\n"));
207 s.push_str("[System.IO.File]::WriteAllText($tuiPid, ('{0}:{1}:{2}' -f $PID, $boot, $start))\n");
208 if let (Some(_), Some(pw)) = (input.server_url, input.server_password) {
209 s.push_str(&format!(
210 "$env:OPENCODE_SERVER_PASSWORD = {}\n",
211 quote_ps(pw)
212 ));
213 s.push_str("Remove-Item Env:OPENCODE_PASSWORD -ErrorAction SilentlyContinue\n");
214 }
215 s.push_str("try {\n");
216 let mut cmd = format!(
217 "& {} {} -s {}",
218 q(input.opencode_bin),
219 q(input.directory),
220 quote_ps(input.session_id)
221 );
222 if let Some(url) = input.server_url {
223 cmd.push_str(&format!(" --server {}", quote_ps(url)));
224 }
225 cmd.push('\n');
226 s.push_str(&cmd);
228 s.push_str("} finally {\n");
229 s.push_str(
230 "[System.IO.File]::WriteAllText($tuiExited, [string][DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())\n",
231 );
232 s.push_str("}\n");
233 s
234}