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> {
279 let mut searched = Vec::new();
280 if let Some(explicit) = std::env::var_os("SUPERCODE_ORCHESTRATOR_ENTRY") {
281 let path = PathBuf::from(explicit);
282 if path.is_file() {
283 return Ok(path);
284 }
285 searched.push(path.display().to_string());
286 }
287 for dir in std::env::var_os("PATH")
290 .iter()
291 .flat_map(std::env::split_paths)
292 {
293 let command = dir.join("supercode-orchestrator");
294 if let Ok(entry) = std::fs::canonicalize(&command) {
295 if entry.is_file() && entry.ends_with(DAEMON_ENTRY) {
296 return Ok(entry);
297 }
298 }
299 }
300 searched.push("supercode-orchestrator on PATH".to_string());
301 let mut roots: Vec<PathBuf> = Vec::new();
302 if let Ok(exe) = std::env::current_exe() {
303 roots.extend(exe.ancestors().skip(1).take(4).map(Path::to_path_buf));
305 }
306 if let Some(workspace) = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2) {
311 roots.push(workspace.to_path_buf());
312 }
313 for root in roots {
314 let candidate = root.join("sdk/orchestrator").join(DAEMON_ENTRY);
315 if candidate.is_file() {
316 return Ok(candidate);
317 }
318 searched.push(candidate.display().to_string());
319 }
320 Err(OrchestratorError::NoDaemonEntry {
321 searched: searched.join(", "),
322 })
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
327pub struct ServiceUnit {
328 pub kind: &'static str,
330 pub path: PathBuf,
332 pub text: String,
334 pub install_command: String,
336 #[serde(skip)]
338 pub files: Vec<(PathBuf, String)>,
339}
340
341pub fn service_unit(root: &Path, entry: &Path, node: &str) -> ServiceUnit {
347 let root_display = root.display().to_string();
348 let entry_display = entry.display().to_string();
349 let name = service_name(root);
350 let (env_path, env_home) = unit_environment();
351 if cfg!(target_os = "macos") {
352 let path = root.join(SERVICE_DIR).join(format!("{name}.plist"));
353 let text = format!(
354 r#"<?xml version="1.0" encoding="UTF-8"?>
355<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
356<plist version="1.0">
357<dict>
358 <key>Label</key><string>{name}</string>
359 <key>EnvironmentVariables</key>
360 <dict>
361 <key>PATH</key><string>{env_path}</string>
362 <key>HOME</key><string>{env_home}</string>
363 </dict>
364 <key>ProgramArguments</key>
365 <array>
366 <string>{node}</string>
367 <string>{entry_display}</string>
368 <string>--root</string>
369 <string>{root_display}</string>
370 </array>
371 <key>RunAtLoad</key><true/>
372 <key>KeepAlive</key><true/>
373 <key>StandardOutPath</key><string>{root_display}/service/orchestrator.out.log</string>
374 <key>StandardErrorPath</key><string>{root_display}/service/orchestrator.err.log</string>
375</dict>
376</plist>
377"#
378 );
379 let install = format!("launchctl bootstrap gui/$(id -u) {}", path.display());
380 ServiceUnit {
381 kind: "launchd",
382 path,
383 text,
384 install_command: install,
385 files: Vec::new(),
386 }
387 } else {
388 let path = root.join(SERVICE_DIR).join(format!("{name}.service"));
389 let text = format!(
390 "[Unit]\n\
391 Description=supercode orchestrator ({root_display})\n\
392 After=network.target\n\
393 \n\
394 [Service]\n\
395 Environment=PATH={env_path}\n\
396 Environment=HOME={env_home}\n\
397 ExecStart={node} {entry_display} --root {root_display}\n\
398 Restart=on-failure\n\
399 KillSignal=SIGTERM\n\
400 \n\
401 [Install]\n\
402 WantedBy=default.target\n"
403 );
404 let install = format!(
405 "systemctl --user link {} && systemctl --user enable --now {name}",
406 path.display()
407 );
408 ServiceUnit {
409 kind: "systemd",
410 path,
411 text,
412 install_command: install,
413 files: Vec::new(),
414 }
415 }
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
420pub struct ServiceState {
421 pub kind: &'static str,
423 pub label: String,
425 pub installed: bool,
427 pub pid: Option<u32>,
429 pub detail: String,
431}
432
433pub fn absolute_program(program: &str) -> String {
436 resolve_program(program).display().to_string()
437}
438
439pub fn resolve_program(program: &str) -> PathBuf {
443 resolve_program_in(
444 program,
445 std::env::var_os("PATH").as_deref(),
446 std::env::var("PATHEXT").ok().as_deref(),
447 cfg!(windows),
448 )
449}
450
451fn resolve_program_in(
455 program: &str,
456 path: Option<&std::ffi::OsStr>,
457 pathext: Option<&str>,
458 windows: bool,
459) -> PathBuf {
460 if program.contains('/') || (windows && program.contains('\\')) {
461 return PathBuf::from(program);
462 }
463 let extensions: Vec<String> = if windows && Path::new(program).extension().is_none() {
464 pathext
465 .unwrap_or(".COM;.EXE;.BAT;.CMD")
466 .split(';')
467 .filter(|extension| !extension.is_empty())
468 .map(str::to_ascii_lowercase)
469 .collect()
470 } else {
471 vec![String::new()]
472 };
473 for dir in path.map(std::env::split_paths).into_iter().flatten() {
474 for extension in &extensions {
475 let candidate = dir.join(format!("{program}{extension}"));
476 if candidate.is_file() {
477 return candidate;
478 }
479 }
480 }
481 PathBuf::from(program)
482}
483
484#[allow(dead_code)]
487fn unit_label(unit: &ServiceUnit) -> String {
488 unit.path
489 .file_stem()
490 .map(|stem| stem.to_string_lossy().into_owned())
491 .unwrap_or_default()
492}
493
494fn run_tool(program: &str, args: &[&str]) -> Result<(bool, String), std::io::Error> {
495 let output = std::process::Command::new(program).args(args).output()?;
496 let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
497 text.push_str(&String::from_utf8_lossy(&output.stderr));
498 Ok((output.status.success(), text.trim().to_string()))
499}
500
501#[cfg(target_os = "macos")]
502fn gui_domain() -> String {
503 format!("gui/{}", unsafe { libc::getuid() })
505}
506
507pub fn service_status(root: &Path) -> ServiceState {
511 platform_status(root)
512}
513
514#[cfg(target_os = "macos")]
515fn platform_status(root: &Path) -> ServiceState {
516 let label = service_name(root);
517 let target = format!("{}/{label}", gui_domain());
518 match run_tool("launchctl", &["print", &target]) {
519 Ok((true, text)) => ServiceState {
520 kind: "launchd",
521 label,
522 installed: true,
523 pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
524 detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
525 },
526 Ok((false, _)) => ServiceState {
527 kind: "launchd",
528 label,
529 installed: false,
530 pid: None,
531 detail: format!("not bootstrapped in {}", gui_domain()),
532 },
533 Err(error) => ServiceState {
534 kind: "launchd",
535 label,
536 installed: false,
537 pid: None,
538 detail: format!("launchctl unavailable: {error}"),
539 },
540 }
541}
542
543#[cfg(all(unix, not(target_os = "macos")))]
544fn platform_status(root: &Path) -> ServiceState {
545 let label = service_name(root);
546 match run_tool("systemctl", &["--user", "is-active", &label]) {
547 Ok((active, text)) => {
548 let known = run_tool("systemctl", &["--user", "is-enabled", &label])
549 .map(|(ok, _)| ok)
550 .unwrap_or(false);
551 ServiceState {
552 kind: "systemd",
553 label,
554 installed: active || known,
555 pid: None,
556 detail: if text.is_empty() {
557 "unknown".into()
558 } else {
559 text
560 },
561 }
562 }
563 Err(error) => ServiceState {
564 kind: "systemd",
565 label,
566 installed: false,
567 pid: None,
568 detail: format!("systemctl unavailable: {error}"),
569 },
570 }
571}
572
573#[cfg(not(unix))]
574fn platform_status(root: &Path) -> ServiceState {
575 ServiceState {
576 kind: "none",
577 label: service_name(root),
578 installed: false,
579 pid: None,
580 detail: "no service manager on this platform".into(),
581 }
582}
583
584#[cfg(target_os = "macos")]
586fn field_of(text: &str, key: &str) -> Option<String> {
587 text.lines()
588 .find_map(|line| line.trim().strip_prefix(key))
589 .map(|value| value.trim().to_string())
590}
591
592pub fn install_service(
598 root: &Path,
599 entry: &Path,
600 node: &str,
601) -> Result<(ServiceUnit, ServiceState), OrchestratorError> {
602 let existing = service_status(root);
603 if existing.installed {
604 return Err(OrchestratorError::Service {
605 action: "install",
606 detail: format!(
607 "`{}` is already installed ({}); `supercode orchestrator setup --uninstall` first",
608 existing.label, existing.detail
609 ),
610 });
611 }
612 let unit = service_unit(root, entry, &absolute_program(node));
613 write_unit(&unit)?;
614 platform_install(&unit)?;
615 Ok((unit, service_status(root)))
616}
617
618#[cfg(target_os = "macos")]
619fn platform_install(unit: &ServiceUnit) -> Result<(), OrchestratorError> {
620 let path = unit.path.display().to_string();
621 let (ok, text) =
622 run_tool("launchctl", &["bootstrap", &gui_domain(), &path]).map_err(|error| {
623 OrchestratorError::Service {
624 action: "install",
625 detail: format!("launchctl: {error}"),
626 }
627 })?;
628 if !ok {
629 return Err(OrchestratorError::Service {
630 action: "install",
631 detail: format!("launchctl bootstrap {}: {text}", gui_domain()),
632 });
633 }
634 Ok(())
635}
636
637#[cfg(all(unix, not(target_os = "macos")))]
640fn platform_install(unit: &ServiceUnit) -> Result<(), OrchestratorError> {
641 let path = unit.path.display().to_string();
642 for args in [
643 vec!["--user", "link", path.as_str()],
644 vec!["--user", "enable", "--now", unit_label(unit).as_str()],
645 ] {
646 let (ok, text) =
647 run_tool("systemctl", &args).map_err(|error| OrchestratorError::Service {
648 action: "install",
649 detail: format!("systemctl: {error}"),
650 })?;
651 if !ok {
652 return Err(OrchestratorError::Service {
653 action: "install",
654 detail: format!("systemctl {}: {text}", args.join(" ")),
655 });
656 }
657 }
658 Ok(())
659}
660
661#[cfg(not(unix))]
662fn platform_install(_unit: &ServiceUnit) -> Result<(), OrchestratorError> {
663 Err(OrchestratorError::Service {
664 action: "install",
665 detail: "no service manager on this platform".into(),
666 })
667}
668
669pub fn uninstall_service(root: &Path) -> Result<ServiceState, OrchestratorError> {
674 platform_uninstall(root)?;
675 let unit_path = root.join(SERVICE_DIR).join(unit_file_name(root));
676 match std::fs::remove_file(&unit_path) {
677 Ok(()) => {}
678 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
679 Err(source) => {
680 return Err(OrchestratorError::Lease {
681 path: unit_path,
682 source,
683 })
684 }
685 }
686 let mut state = service_status(root);
689 for _ in 0..40 {
690 if !state.installed {
691 break;
692 }
693 std::thread::sleep(std::time::Duration::from_millis(100));
694 state = service_status(root);
695 }
696 Ok(state)
697}
698
699#[cfg(target_os = "macos")]
700fn platform_uninstall(root: &Path) -> Result<(), OrchestratorError> {
701 let target = format!("{}/{}", gui_domain(), service_name(root));
702 let (ok, text) = run_tool("launchctl", &["bootout", &target]).map_err(|error| {
703 OrchestratorError::Service {
704 action: "uninstall",
705 detail: format!("launchctl: {error}"),
706 }
707 })?;
708 if !ok && !text.contains("No such process") && !text.contains("not find") {
710 return Err(OrchestratorError::Service {
711 action: "uninstall",
712 detail: format!("launchctl bootout {target}: {text}"),
713 });
714 }
715 Ok(())
716}
717
718#[cfg(all(unix, not(target_os = "macos")))]
719fn platform_uninstall(root: &Path) -> Result<(), OrchestratorError> {
720 let _ = run_tool(
721 "systemctl",
722 &["--user", "disable", "--now", &service_name(root)],
723 );
724 Ok(())
725}
726
727#[cfg(not(unix))]
728fn platform_uninstall(_root: &Path) -> Result<(), OrchestratorError> {
729 Ok(())
730}
731
732fn unit_file_name(root: &Path) -> String {
734 if cfg!(target_os = "macos") {
735 format!("{}.plist", service_name(root))
736 } else {
737 format!("{}.service", service_name(root))
738 }
739}
740
741pub fn write_unit(unit: &ServiceUnit) -> Result<(), OrchestratorError> {
743 if let Some(parent) = unit.path.parent() {
744 std::fs::create_dir_all(parent).map_err(|source| OrchestratorError::Lease {
745 path: unit.path.clone(),
746 source,
747 })?;
748 }
749 std::fs::write(&unit.path, &unit.text).map_err(|source| OrchestratorError::Lease {
750 path: unit.path.clone(),
751 source,
752 })
753}