1use std::path::{Path, PathBuf};
31
32use serde::{Deserialize, Serialize};
33
34pub const LOCK_FILE: &str = "orchestrator.lock";
36
37pub const SERVICE_DIR: &str = "service";
39
40pub const DAEMON_ENTRY: &str = "bin/orchestrator.mjs";
42
43pub const SERVICE_NAME: &str = "ai.volter.supercode.orchestrator";
45
46pub fn service_name(root: &Path) -> String {
49 let mut hash = 0xcbf29ce484222325_u64;
50 for byte in root.to_string_lossy().bytes() {
51 hash ^= u64::from(byte);
52 hash = hash.wrapping_mul(0x100000001b3);
53 }
54 format!("{SERVICE_NAME}-{hash:016x}")
55}
56
57fn unit_environment() -> (String, String) {
60 let path = std::env::var("PATH")
61 .ok()
62 .filter(|path| !path.trim().is_empty())
63 .unwrap_or_else(|| "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin".into());
64 (path, std::env::var("HOME").unwrap_or_default())
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct Lease {
71 pub pid: u32,
73 pub started_at: String,
75 pub root: PathBuf,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub host: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub boot: Option<String>,
85}
86
87impl Lease {
88 pub fn is_live(&self) -> bool {
92 let here = this_host();
93 if self
94 .host
95 .as_deref()
96 .is_some_and(|host| Some(host) != here.0.as_deref())
97 {
98 return false;
99 }
100 if let (Some(boot), Some(now)) = (self.boot.as_deref(), here.1.as_deref()) {
101 if boot != now {
102 return false;
103 }
104 }
105 pid_is_live(self.pid)
106 }
107}
108
109pub fn this_host() -> (Option<String>, Option<String>) {
111 let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
112 .ok()
113 .map(|text| text.trim().to_string())
114 .filter(|text| !text.is_empty());
115 (hostname(), boot)
116}
117
118fn hostname() -> Option<String> {
119 #[cfg(unix)]
120 {
121 let mut buffer = [0u8; 256];
122 let status = unsafe { libc::gethostname(buffer.as_mut_ptr().cast(), buffer.len()) };
124 if status != 0 {
125 return None;
126 }
127 let end = buffer
128 .iter()
129 .position(|byte| *byte == 0)
130 .unwrap_or(buffer.len());
131 String::from_utf8(buffer[..end].to_vec())
132 .ok()
133 .filter(|name| !name.is_empty())
134 }
135 #[cfg(not(unix))]
136 {
137 std::env::var("COMPUTERNAME").ok()
138 }
139}
140
141#[derive(Debug, thiserror::Error)]
143pub enum OrchestratorError {
144 #[error("the orchestrator is not running for `{0}` (no live lease at `{1}`)", root.display(), lock.display())]
146 NotRunning {
147 root: PathBuf,
149 lock: PathBuf,
151 },
152 #[error("the orchestrator is already running for `{}` (pid {pid})", root.display())]
154 AlreadyRunning {
155 root: PathBuf,
157 pid: u32,
159 },
160 #[error("no orchestrator daemon entry found (looked for `{DAEMON_ENTRY}` under: {searched})")]
162 NoDaemonEntry {
163 searched: String,
165 },
166 #[error("orchestrator lease `{}`: {source}", path.display())]
168 Lease {
169 path: PathBuf,
171 source: std::io::Error,
173 },
174 #[error("orchestrator service: {action} failed: {detail}")]
176 Service {
177 action: &'static str,
179 detail: String,
181 },
182}
183
184pub fn lock_path(root: &Path) -> PathBuf {
186 root.join(LOCK_FILE)
187}
188
189pub fn read_lease(root: &Path) -> Option<Lease> {
191 let text = std::fs::read_to_string(lock_path(root)).ok()?;
192 serde_json::from_str(&text).ok()
193}
194
195pub fn write_lease(root: &Path, lease: &Lease) -> Result<(), OrchestratorError> {
197 let path = lock_path(root);
198 if let Some(parent) = path.parent() {
199 std::fs::create_dir_all(parent).map_err(|source| OrchestratorError::Lease {
200 path: path.clone(),
201 source,
202 })?;
203 }
204 let text = serde_json::to_string_pretty(lease).unwrap_or_default();
205 std::fs::write(&path, format!("{text}\n")).map_err(|source| OrchestratorError::Lease {
206 path: path.clone(),
207 source,
208 })
209}
210
211pub fn clear_lease(root: &Path) -> Result<(), OrchestratorError> {
214 let path = lock_path(root);
215 match std::fs::remove_file(&path) {
216 Ok(()) => Ok(()),
217 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
218 Err(source) => Err(OrchestratorError::Lease { path, source }),
219 }
220}
221
222pub fn pid_is_live(pid: u32) -> bool {
228 #[cfg(unix)]
229 {
230 if pid == 0 {
231 return false;
232 }
233 unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
235 }
236 #[cfg(not(unix))]
237 {
238 let _ = pid;
239 true
240 }
241}
242
243pub fn live_lease(root: &Path) -> Option<Lease> {
245 read_lease(root).filter(Lease::is_live)
246}
247
248pub fn stop(root: &Path) -> Result<Lease, OrchestratorError> {
254 let Some(lease) = live_lease(root) else {
255 return Err(OrchestratorError::NotRunning {
256 root: root.to_path_buf(),
257 lock: lock_path(root),
258 });
259 };
260 #[cfg(unix)]
261 unsafe {
263 libc::kill(lease.pid as libc::pid_t, libc::SIGTERM);
264 }
265 clear_lease(root)?;
266 Ok(lease)
267}
268
269pub fn daemon_entry() -> Result<PathBuf, OrchestratorError> {
280 let mut searched = Vec::new();
281 if let Some(explicit) = std::env::var_os("SUPERCODE_ORCHESTRATOR_ENTRY") {
282 let path = PathBuf::from(explicit);
283 if path.is_file() {
284 return Ok(path);
285 }
286 searched.push(path.display().to_string());
287 }
288 let mut roots: Vec<PathBuf> = Vec::new();
289 if let Ok(exe) = std::env::current_exe() {
290 roots.extend(exe.ancestors().skip(1).take(4).map(Path::to_path_buf));
292 }
293 if let Some(workspace) = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2) {
298 roots.push(workspace.to_path_buf());
299 }
300 for root in roots {
301 let candidate = root.join("sdk/orchestrator").join(DAEMON_ENTRY);
302 if candidate.is_file() {
303 return Ok(candidate);
304 }
305 searched.push(candidate.display().to_string());
306 }
307 for dir in std::env::var_os("PATH")
310 .iter()
311 .flat_map(std::env::split_paths)
312 {
313 let command = dir.join("supercode-orchestrator");
314 if let Ok(entry) = std::fs::canonicalize(&command) {
315 if entry.is_file() && entry.ends_with(DAEMON_ENTRY) {
316 return Ok(entry);
317 }
318 }
319 }
320 searched.push("supercode-orchestrator on PATH".to_string());
321 Err(OrchestratorError::NoDaemonEntry {
322 searched: searched.join(", "),
323 })
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
328pub struct ServiceUnit {
329 pub kind: &'static str,
331 pub path: PathBuf,
333 pub text: String,
335 pub install_command: String,
337 #[serde(skip)]
339 pub files: Vec<(PathBuf, String)>,
340}
341
342pub fn service_unit(root: &Path, entry: &Path, node: &str) -> ServiceUnit {
348 let root_display = root.display().to_string();
349 let entry_display = entry.display().to_string();
350 let name = service_name(root);
351 let (env_path, env_home) = unit_environment();
352 if cfg!(target_os = "macos") {
353 let path = root.join(SERVICE_DIR).join(format!("{name}.plist"));
354 let text = format!(
355 r#"<?xml version="1.0" encoding="UTF-8"?>
356<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
357<plist version="1.0">
358<dict>
359 <key>Label</key><string>{name}</string>
360 <key>EnvironmentVariables</key>
361 <dict>
362 <key>PATH</key><string>{env_path}</string>
363 <key>HOME</key><string>{env_home}</string>
364 </dict>
365 <key>ProgramArguments</key>
366 <array>
367 <string>{node}</string>
368 <string>{entry_display}</string>
369 <string>--root</string>
370 <string>{root_display}</string>
371 </array>
372 <key>RunAtLoad</key><true/>
373 <key>KeepAlive</key><true/>
374 <key>StandardOutPath</key><string>{root_display}/service/orchestrator.out.log</string>
375 <key>StandardErrorPath</key><string>{root_display}/service/orchestrator.err.log</string>
376</dict>
377</plist>
378"#
379 );
380 let install = format!("launchctl bootstrap gui/$(id -u) {}", path.display());
381 ServiceUnit {
382 kind: "launchd",
383 path,
384 text,
385 install_command: install,
386 files: Vec::new(),
387 }
388 } else {
389 let path = root.join(SERVICE_DIR).join(format!("{name}.service"));
390 let text = format!(
391 "[Unit]\n\
392 Description=supercode orchestrator ({root_display})\n\
393 After=network.target\n\
394 \n\
395 [Service]\n\
396 Environment=PATH={env_path}\n\
397 Environment=HOME={env_home}\n\
398 ExecStart={node} {entry_display} --root {root_display}\n\
399 Restart=on-failure\n\
400 KillSignal=SIGTERM\n\
401 \n\
402 [Install]\n\
403 WantedBy=default.target\n"
404 );
405 let install = format!(
406 "systemctl --user link {} && systemctl --user enable --now {name}",
407 path.display()
408 );
409 ServiceUnit {
410 kind: "systemd",
411 path,
412 text,
413 install_command: install,
414 files: Vec::new(),
415 }
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
421pub struct ServiceState {
422 pub kind: &'static str,
424 pub label: String,
426 pub installed: bool,
428 pub pid: Option<u32>,
430 pub detail: String,
432}
433
434pub fn absolute_program(program: &str) -> String {
437 resolve_program(program).display().to_string()
438}
439
440pub fn resolve_program(program: &str) -> PathBuf {
444 resolve_program_in(
445 program,
446 std::env::var_os("PATH").as_deref(),
447 std::env::var("PATHEXT").ok().as_deref(),
448 cfg!(windows),
449 )
450}
451
452fn resolve_program_in(
456 program: &str,
457 path: Option<&std::ffi::OsStr>,
458 pathext: Option<&str>,
459 windows: bool,
460) -> PathBuf {
461 if program.contains('/') || (windows && program.contains('\\')) {
462 return PathBuf::from(program);
463 }
464 let extensions: Vec<String> = if windows && Path::new(program).extension().is_none() {
465 pathext
466 .unwrap_or(".COM;.EXE;.BAT;.CMD")
467 .split(';')
468 .filter(|extension| !extension.is_empty())
469 .map(str::to_ascii_lowercase)
470 .collect()
471 } else {
472 vec![String::new()]
473 };
474 for dir in path.map(std::env::split_paths).into_iter().flatten() {
475 for extension in &extensions {
476 let candidate = dir.join(format!("{program}{extension}"));
477 if candidate.is_file() {
478 return candidate;
479 }
480 }
481 }
482 PathBuf::from(program)
483}
484
485#[allow(dead_code)]
488fn unit_label(unit: &ServiceUnit) -> String {
489 unit.path
490 .file_stem()
491 .map(|stem| stem.to_string_lossy().into_owned())
492 .unwrap_or_default()
493}
494
495fn run_tool(program: &str, args: &[&str]) -> Result<(bool, String), std::io::Error> {
496 let output = std::process::Command::new(program).args(args).output()?;
497 let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
498 text.push_str(&String::from_utf8_lossy(&output.stderr));
499 Ok((output.status.success(), text.trim().to_string()))
500}
501
502#[cfg(target_os = "macos")]
503fn gui_domain() -> String {
504 format!("gui/{}", unsafe { libc::getuid() })
506}
507
508pub fn service_status(root: &Path) -> ServiceState {
512 platform_status(root)
513}
514
515#[cfg(target_os = "macos")]
516fn platform_status(root: &Path) -> ServiceState {
517 let label = service_name(root);
518 let target = format!("{}/{label}", gui_domain());
519 match run_tool("launchctl", &["print", &target]) {
520 Ok((true, text)) => ServiceState {
521 kind: "launchd",
522 label,
523 installed: true,
524 pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
525 detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
526 },
527 Ok((false, _)) => ServiceState {
528 kind: "launchd",
529 label,
530 installed: false,
531 pid: None,
532 detail: format!("not bootstrapped in {}", gui_domain()),
533 },
534 Err(error) => ServiceState {
535 kind: "launchd",
536 label,
537 installed: false,
538 pid: None,
539 detail: format!("launchctl unavailable: {error}"),
540 },
541 }
542}
543
544#[cfg(all(unix, not(target_os = "macos")))]
545fn platform_status(root: &Path) -> ServiceState {
546 let label = service_name(root);
547 match run_tool("systemctl", &["--user", "is-active", &label]) {
548 Ok((active, text)) => {
549 let known = run_tool("systemctl", &["--user", "is-enabled", &label])
550 .map(|(ok, _)| ok)
551 .unwrap_or(false);
552 ServiceState {
553 kind: "systemd",
554 label,
555 installed: active || known,
556 pid: None,
557 detail: if text.is_empty() {
558 "unknown".into()
559 } else {
560 text
561 },
562 }
563 }
564 Err(error) => ServiceState {
565 kind: "systemd",
566 label,
567 installed: false,
568 pid: None,
569 detail: format!("systemctl unavailable: {error}"),
570 },
571 }
572}
573
574#[cfg(not(unix))]
575fn platform_status(root: &Path) -> ServiceState {
576 ServiceState {
577 kind: "none",
578 label: service_name(root),
579 installed: false,
580 pid: None,
581 detail: "no service manager on this platform".into(),
582 }
583}
584
585#[cfg(target_os = "macos")]
587fn field_of(text: &str, key: &str) -> Option<String> {
588 text.lines()
589 .find_map(|line| line.trim().strip_prefix(key))
590 .map(|value| value.trim().to_string())
591}
592
593pub fn install_service(
599 root: &Path,
600 entry: &Path,
601 node: &str,
602) -> Result<(ServiceUnit, ServiceState), OrchestratorError> {
603 let existing = service_status(root);
604 if existing.installed {
605 return Err(OrchestratorError::Service {
606 action: "install",
607 detail: format!(
608 "`{}` is already installed ({}); `supercode orchestrator setup --uninstall` first",
609 existing.label, existing.detail
610 ),
611 });
612 }
613 let unit = service_unit(root, entry, &absolute_program(node));
614 write_unit(&unit)?;
615 platform_install(&unit)?;
616 Ok((unit, service_status(root)))
617}
618
619#[cfg(target_os = "macos")]
620fn platform_install(unit: &ServiceUnit) -> Result<(), OrchestratorError> {
621 let path = unit.path.display().to_string();
622 let (ok, text) =
623 run_tool("launchctl", &["bootstrap", &gui_domain(), &path]).map_err(|error| {
624 OrchestratorError::Service {
625 action: "install",
626 detail: format!("launchctl: {error}"),
627 }
628 })?;
629 if !ok {
630 return Err(OrchestratorError::Service {
631 action: "install",
632 detail: format!("launchctl bootstrap {}: {text}", gui_domain()),
633 });
634 }
635 Ok(())
636}
637
638#[cfg(all(unix, not(target_os = "macos")))]
641fn platform_install(unit: &ServiceUnit) -> Result<(), OrchestratorError> {
642 let path = unit.path.display().to_string();
643 for args in [
644 vec!["--user", "link", path.as_str()],
645 vec!["--user", "enable", "--now", unit_label(unit).as_str()],
646 ] {
647 let (ok, text) =
648 run_tool("systemctl", &args).map_err(|error| OrchestratorError::Service {
649 action: "install",
650 detail: format!("systemctl: {error}"),
651 })?;
652 if !ok {
653 return Err(OrchestratorError::Service {
654 action: "install",
655 detail: format!("systemctl {}: {text}", args.join(" ")),
656 });
657 }
658 }
659 Ok(())
660}
661
662#[cfg(not(unix))]
663fn platform_install(_unit: &ServiceUnit) -> Result<(), OrchestratorError> {
664 Err(OrchestratorError::Service {
665 action: "install",
666 detail: "no service manager on this platform".into(),
667 })
668}
669
670pub fn uninstall_service(root: &Path) -> Result<ServiceState, OrchestratorError> {
675 platform_uninstall(root)?;
676 let unit_path = root.join(SERVICE_DIR).join(unit_file_name(root));
677 match std::fs::remove_file(&unit_path) {
678 Ok(()) => {}
679 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
680 Err(source) => {
681 return Err(OrchestratorError::Lease {
682 path: unit_path,
683 source,
684 })
685 }
686 }
687 let mut state = service_status(root);
690 for _ in 0..40 {
691 if !state.installed {
692 break;
693 }
694 std::thread::sleep(std::time::Duration::from_millis(100));
695 state = service_status(root);
696 }
697 Ok(state)
698}
699
700#[cfg(target_os = "macos")]
701fn platform_uninstall(root: &Path) -> Result<(), OrchestratorError> {
702 let target = format!("{}/{}", gui_domain(), service_name(root));
703 let (ok, text) = run_tool("launchctl", &["bootout", &target]).map_err(|error| {
704 OrchestratorError::Service {
705 action: "uninstall",
706 detail: format!("launchctl: {error}"),
707 }
708 })?;
709 if !ok && !text.contains("No such process") && !text.contains("not find") {
711 return Err(OrchestratorError::Service {
712 action: "uninstall",
713 detail: format!("launchctl bootout {target}: {text}"),
714 });
715 }
716 Ok(())
717}
718
719#[cfg(all(unix, not(target_os = "macos")))]
720fn platform_uninstall(root: &Path) -> Result<(), OrchestratorError> {
721 let _ = run_tool(
722 "systemctl",
723 &["--user", "disable", "--now", &service_name(root)],
724 );
725 Ok(())
726}
727
728#[cfg(not(unix))]
729fn platform_uninstall(_root: &Path) -> Result<(), OrchestratorError> {
730 Ok(())
731}
732
733fn unit_file_name(root: &Path) -> String {
735 if cfg!(target_os = "macos") {
736 format!("{}.plist", service_name(root))
737 } else {
738 format!("{}.service", service_name(root))
739 }
740}
741
742pub fn write_unit(unit: &ServiceUnit) -> Result<(), OrchestratorError> {
744 if let Some(parent) = unit.path.parent() {
745 std::fs::create_dir_all(parent).map_err(|source| OrchestratorError::Lease {
746 path: unit.path.clone(),
747 source,
748 })?;
749 }
750 std::fs::write(&unit.path, &unit.text).map_err(|source| OrchestratorError::Lease {
751 path: unit.path.clone(),
752 source,
753 })
754}