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
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct Lease {
50 pub pid: u32,
52 pub started_at: String,
54 pub root: PathBuf,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub host: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub boot: Option<String>,
64}
65
66impl Lease {
67 pub fn is_live(&self) -> bool {
71 let here = this_host();
72 if self
73 .host
74 .as_deref()
75 .is_some_and(|host| Some(host) != here.0.as_deref())
76 {
77 return false;
78 }
79 if let (Some(boot), Some(now)) = (self.boot.as_deref(), here.1.as_deref()) {
80 if boot != now {
81 return false;
82 }
83 }
84 pid_is_live(self.pid)
85 }
86}
87
88pub fn this_host() -> (Option<String>, Option<String>) {
90 let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
91 .ok()
92 .map(|text| text.trim().to_string())
93 .filter(|text| !text.is_empty());
94 (hostname(), boot)
95}
96
97fn hostname() -> Option<String> {
98 #[cfg(unix)]
99 {
100 let mut buffer = [0u8; 256];
101 let status = unsafe { libc::gethostname(buffer.as_mut_ptr().cast(), buffer.len()) };
103 if status != 0 {
104 return None;
105 }
106 let end = buffer
107 .iter()
108 .position(|byte| *byte == 0)
109 .unwrap_or(buffer.len());
110 String::from_utf8(buffer[..end].to_vec())
111 .ok()
112 .filter(|name| !name.is_empty())
113 }
114 #[cfg(not(unix))]
115 {
116 std::env::var("COMPUTERNAME").ok()
117 }
118}
119
120#[derive(Debug, thiserror::Error)]
122pub enum OrchestratorError {
123 #[error("the orchestrator is not running for `{0}` (no live lease at `{1}`)", root.display(), lock.display())]
125 NotRunning {
126 root: PathBuf,
128 lock: PathBuf,
130 },
131 #[error("the orchestrator is already running for `{}` (pid {pid})", root.display())]
133 AlreadyRunning {
134 root: PathBuf,
136 pid: u32,
138 },
139 #[error("no orchestrator daemon entry found (looked for `{DAEMON_ENTRY}` under: {searched})")]
141 NoDaemonEntry {
142 searched: String,
144 },
145 #[error("orchestrator lease `{}`: {source}", path.display())]
147 Lease {
148 path: PathBuf,
150 source: std::io::Error,
152 },
153 #[error("orchestrator service: {action} failed: {detail}")]
155 Service {
156 action: &'static str,
158 detail: String,
160 },
161}
162
163pub fn lock_path(root: &Path) -> PathBuf {
165 root.join(LOCK_FILE)
166}
167
168pub fn read_lease(root: &Path) -> Option<Lease> {
170 let text = std::fs::read_to_string(lock_path(root)).ok()?;
171 serde_json::from_str(&text).ok()
172}
173
174pub fn write_lease(root: &Path, lease: &Lease) -> Result<(), OrchestratorError> {
176 let path = lock_path(root);
177 if let Some(parent) = path.parent() {
178 std::fs::create_dir_all(parent).map_err(|source| OrchestratorError::Lease {
179 path: path.clone(),
180 source,
181 })?;
182 }
183 let text = serde_json::to_string_pretty(lease).unwrap_or_default();
184 std::fs::write(&path, format!("{text}\n")).map_err(|source| OrchestratorError::Lease {
185 path: path.clone(),
186 source,
187 })
188}
189
190pub fn clear_lease(root: &Path) -> Result<(), OrchestratorError> {
193 let path = lock_path(root);
194 match std::fs::remove_file(&path) {
195 Ok(()) => Ok(()),
196 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
197 Err(source) => Err(OrchestratorError::Lease { path, source }),
198 }
199}
200
201pub fn pid_is_live(pid: u32) -> bool {
207 #[cfg(unix)]
208 {
209 if pid == 0 {
210 return false;
211 }
212 unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
214 }
215 #[cfg(not(unix))]
216 {
217 let _ = pid;
218 true
219 }
220}
221
222pub fn live_lease(root: &Path) -> Option<Lease> {
224 read_lease(root).filter(Lease::is_live)
225}
226
227pub fn stop(root: &Path) -> Result<Lease, OrchestratorError> {
233 let Some(lease) = live_lease(root) else {
234 return Err(OrchestratorError::NotRunning {
235 root: root.to_path_buf(),
236 lock: lock_path(root),
237 });
238 };
239 #[cfg(unix)]
240 unsafe {
242 libc::kill(lease.pid as libc::pid_t, libc::SIGTERM);
243 }
244 clear_lease(root)?;
245 Ok(lease)
246}
247
248pub fn daemon_entry() -> Result<PathBuf, OrchestratorError> {
256 let mut searched = Vec::new();
257 if let Some(explicit) = std::env::var_os("SUPERCODE_ORCHESTRATOR_ENTRY") {
258 let path = PathBuf::from(explicit);
259 if path.is_file() {
260 return Ok(path);
261 }
262 searched.push(path.display().to_string());
263 }
264 for dir in std::env::var_os("PATH")
267 .iter()
268 .flat_map(std::env::split_paths)
269 {
270 let command = dir.join("supercode-orchestrator");
271 if let Ok(entry) = std::fs::canonicalize(&command) {
272 if entry.is_file() && entry.ends_with(DAEMON_ENTRY) {
273 return Ok(entry);
274 }
275 }
276 }
277 searched.push("supercode-orchestrator on PATH".to_string());
278 let mut roots: Vec<PathBuf> = Vec::new();
279 if let Ok(exe) = std::env::current_exe() {
280 roots.extend(exe.ancestors().skip(1).take(4).map(Path::to_path_buf));
282 }
283 if let Ok(cwd) = std::env::current_dir() {
284 roots.push(cwd);
285 }
286 if let Some(workspace) = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2) {
291 roots.push(workspace.to_path_buf());
292 }
293 for root in roots {
294 let candidate = root.join("sdk/orchestrator").join(DAEMON_ENTRY);
295 if candidate.is_file() {
296 return Ok(candidate);
297 }
298 searched.push(candidate.display().to_string());
299 }
300 Err(OrchestratorError::NoDaemonEntry {
301 searched: searched.join(", "),
302 })
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
307pub struct ServiceUnit {
308 pub kind: &'static str,
310 pub path: PathBuf,
312 pub text: String,
314 pub install_command: String,
316}
317
318pub fn service_unit(root: &Path, entry: &Path, node: &str) -> ServiceUnit {
324 let root_display = root.display().to_string();
325 let entry_display = entry.display().to_string();
326 if cfg!(target_os = "macos") {
327 let path = root.join(SERVICE_DIR).join(format!("{SERVICE_NAME}.plist"));
328 let text = format!(
329 r#"<?xml version="1.0" encoding="UTF-8"?>
330<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
331<plist version="1.0">
332<dict>
333 <key>Label</key><string>{SERVICE_NAME}</string>
334 <key>ProgramArguments</key>
335 <array>
336 <string>{node}</string>
337 <string>{entry_display}</string>
338 <string>--root</string>
339 <string>{root_display}</string>
340 </array>
341 <key>RunAtLoad</key><true/>
342 <key>KeepAlive</key><true/>
343 <key>StandardOutPath</key><string>{root_display}/service/orchestrator.out.log</string>
344 <key>StandardErrorPath</key><string>{root_display}/service/orchestrator.err.log</string>
345</dict>
346</plist>
347"#
348 );
349 let install = format!("launchctl bootstrap gui/$(id -u) {}", path.display());
350 ServiceUnit {
351 kind: "launchd",
352 path,
353 text,
354 install_command: install,
355 }
356 } else {
357 let path = root
358 .join(SERVICE_DIR)
359 .join(format!("{SERVICE_NAME}.service"));
360 let text = format!(
361 "[Unit]\n\
362 Description=supercode orchestrator ({root_display})\n\
363 After=network.target\n\
364 \n\
365 [Service]\n\
366 ExecStart={node} {entry_display} --root {root_display}\n\
367 Restart=on-failure\n\
368 KillSignal=SIGTERM\n\
369 \n\
370 [Install]\n\
371 WantedBy=default.target\n"
372 );
373 let install = format!(
374 "systemctl --user link {} && systemctl --user enable --now {SERVICE_NAME}",
375 path.display()
376 );
377 ServiceUnit {
378 kind: "systemd",
379 path,
380 text,
381 install_command: install,
382 }
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
388pub struct ServiceState {
389 pub kind: &'static str,
391 pub label: String,
393 pub installed: bool,
395 pub pid: Option<u32>,
397 pub detail: String,
399}
400
401pub fn absolute_program(program: &str) -> String {
404 if program.contains('/') {
405 return program.to_string();
406 }
407 if let Some(path) = std::env::var_os("PATH") {
408 for dir in std::env::split_paths(&path) {
409 let candidate = dir.join(program);
410 if candidate.is_file() {
411 return candidate.display().to_string();
412 }
413 }
414 }
415 program.to_string()
416}
417
418fn run_tool(program: &str, args: &[&str]) -> Result<(bool, String), std::io::Error> {
420 let output = std::process::Command::new(program).args(args).output()?;
421 let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
422 text.push_str(&String::from_utf8_lossy(&output.stderr));
423 Ok((output.status.success(), text.trim().to_string()))
424}
425
426#[cfg(target_os = "macos")]
427fn gui_domain() -> String {
428 format!("gui/{}", unsafe { libc::getuid() })
430}
431
432pub fn service_status(root: &Path) -> ServiceState {
436 platform_status(root)
437}
438
439#[cfg(target_os = "macos")]
440fn platform_status(_root: &Path) -> ServiceState {
441 let label = SERVICE_NAME.to_string();
442 let target = format!("{}/{SERVICE_NAME}", gui_domain());
443 match run_tool("launchctl", &["print", &target]) {
444 Ok((true, text)) => ServiceState {
445 kind: "launchd",
446 label,
447 installed: true,
448 pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
449 detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
450 },
451 Ok((false, _)) => ServiceState {
452 kind: "launchd",
453 label,
454 installed: false,
455 pid: None,
456 detail: format!("not bootstrapped in {}", gui_domain()),
457 },
458 Err(error) => ServiceState {
459 kind: "launchd",
460 label,
461 installed: false,
462 pid: None,
463 detail: format!("launchctl unavailable: {error}"),
464 },
465 }
466}
467
468#[cfg(all(unix, not(target_os = "macos")))]
469fn platform_status(_root: &Path) -> ServiceState {
470 let label = SERVICE_NAME.to_string();
471 match run_tool("systemctl", &["--user", "is-active", SERVICE_NAME]) {
472 Ok((active, text)) => {
473 let known = run_tool("systemctl", &["--user", "is-enabled", SERVICE_NAME])
474 .map(|(ok, _)| ok)
475 .unwrap_or(false);
476 ServiceState {
477 kind: "systemd",
478 label,
479 installed: active || known,
480 pid: None,
481 detail: if text.is_empty() {
482 "unknown".into()
483 } else {
484 text
485 },
486 }
487 }
488 Err(error) => ServiceState {
489 kind: "systemd",
490 label,
491 installed: false,
492 pid: None,
493 detail: format!("systemctl unavailable: {error}"),
494 },
495 }
496}
497
498#[cfg(not(unix))]
499fn platform_status(_root: &Path) -> ServiceState {
500 ServiceState {
501 kind: "none",
502 label: SERVICE_NAME.to_string(),
503 installed: false,
504 pid: None,
505 detail: "no service manager on this platform".into(),
506 }
507}
508
509#[cfg(target_os = "macos")]
511fn field_of(text: &str, key: &str) -> Option<String> {
512 text.lines()
513 .find_map(|line| line.trim().strip_prefix(key))
514 .map(|value| value.trim().to_string())
515}
516
517pub fn install_service(
523 root: &Path,
524 entry: &Path,
525 node: &str,
526) -> Result<(ServiceUnit, ServiceState), OrchestratorError> {
527 let existing = service_status(root);
528 if existing.installed {
529 return Err(OrchestratorError::Service {
530 action: "install",
531 detail: format!(
532 "`{}` is already installed ({}); `supercode orchestrator setup --uninstall` first",
533 existing.label, existing.detail
534 ),
535 });
536 }
537 let unit = service_unit(root, entry, &absolute_program(node));
538 write_unit(&unit)?;
539 platform_install(&unit)?;
540 Ok((unit, service_status(root)))
541}
542
543#[cfg(target_os = "macos")]
544fn platform_install(unit: &ServiceUnit) -> Result<(), OrchestratorError> {
545 let path = unit.path.display().to_string();
546 let (ok, text) =
547 run_tool("launchctl", &["bootstrap", &gui_domain(), &path]).map_err(|error| {
548 OrchestratorError::Service {
549 action: "install",
550 detail: format!("launchctl: {error}"),
551 }
552 })?;
553 if !ok {
554 return Err(OrchestratorError::Service {
555 action: "install",
556 detail: format!("launchctl bootstrap {}: {text}", gui_domain()),
557 });
558 }
559 Ok(())
560}
561
562#[cfg(all(unix, not(target_os = "macos")))]
565fn platform_install(unit: &ServiceUnit) -> Result<(), OrchestratorError> {
566 let path = unit.path.display().to_string();
567 for args in [
568 vec!["--user", "link", path.as_str()],
569 vec!["--user", "enable", "--now", SERVICE_NAME],
570 ] {
571 let (ok, text) =
572 run_tool("systemctl", &args).map_err(|error| OrchestratorError::Service {
573 action: "install",
574 detail: format!("systemctl: {error}"),
575 })?;
576 if !ok {
577 return Err(OrchestratorError::Service {
578 action: "install",
579 detail: format!("systemctl {}: {text}", args.join(" ")),
580 });
581 }
582 }
583 Ok(())
584}
585
586#[cfg(not(unix))]
587fn platform_install(_unit: &ServiceUnit) -> Result<(), OrchestratorError> {
588 Err(OrchestratorError::Service {
589 action: "install",
590 detail: "no service manager on this platform".into(),
591 })
592}
593
594pub fn uninstall_service(root: &Path) -> Result<ServiceState, OrchestratorError> {
599 platform_uninstall()?;
600 let unit_path = root.join(SERVICE_DIR).join(unit_file_name());
601 match std::fs::remove_file(&unit_path) {
602 Ok(()) => {}
603 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
604 Err(source) => {
605 return Err(OrchestratorError::Lease {
606 path: unit_path,
607 source,
608 })
609 }
610 }
611 let mut state = service_status(root);
614 for _ in 0..40 {
615 if !state.installed {
616 break;
617 }
618 std::thread::sleep(std::time::Duration::from_millis(100));
619 state = service_status(root);
620 }
621 Ok(state)
622}
623
624#[cfg(target_os = "macos")]
625fn platform_uninstall() -> Result<(), OrchestratorError> {
626 let target = format!("{}/{SERVICE_NAME}", gui_domain());
627 let (ok, text) = run_tool("launchctl", &["bootout", &target]).map_err(|error| {
628 OrchestratorError::Service {
629 action: "uninstall",
630 detail: format!("launchctl: {error}"),
631 }
632 })?;
633 if !ok && !text.contains("No such process") && !text.contains("not find") {
635 return Err(OrchestratorError::Service {
636 action: "uninstall",
637 detail: format!("launchctl bootout {target}: {text}"),
638 });
639 }
640 Ok(())
641}
642
643#[cfg(all(unix, not(target_os = "macos")))]
644fn platform_uninstall() -> Result<(), OrchestratorError> {
645 let _ = run_tool("systemctl", &["--user", "disable", "--now", SERVICE_NAME]);
646 Ok(())
647}
648
649#[cfg(not(unix))]
650fn platform_uninstall() -> Result<(), OrchestratorError> {
651 Ok(())
652}
653
654fn unit_file_name() -> String {
656 if cfg!(target_os = "macos") {
657 format!("{SERVICE_NAME}.plist")
658 } else {
659 format!("{SERVICE_NAME}.service")
660 }
661}
662
663pub fn write_unit(unit: &ServiceUnit) -> Result<(), OrchestratorError> {
665 if let Some(parent) = unit.path.parent() {
666 std::fs::create_dir_all(parent).map_err(|source| OrchestratorError::Lease {
667 path: unit.path.clone(),
668 source,
669 })?;
670 }
671 std::fs::write(&unit.path, &unit.text).map_err(|source| OrchestratorError::Lease {
672 path: unit.path.clone(),
673 source,
674 })
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680
681 fn scratch(label: &str) -> PathBuf {
683 let root = std::env::temp_dir().join(format!(
684 "supercode-orchestrator-{label}-{}-{}",
685 std::process::id(),
686 std::time::SystemTime::now()
687 .duration_since(std::time::UNIX_EPOCH)
688 .unwrap()
689 .as_nanos()
690 ));
691 std::fs::create_dir_all(&root).unwrap();
692 root
693 }
694
695 #[test]
696 fn a_lease_round_trips_and_a_missing_one_is_not_running() {
697 let root = &scratch("lease");
698 let root = root.as_path();
699 assert!(read_lease(root).is_none());
700 assert!(live_lease(root).is_none());
701 let lease = Lease {
702 pid: std::process::id(),
703 started_at: "2026-09-04T00:00:00Z".into(),
704 root: root.to_path_buf(),
705 };
706 write_lease(root, &lease).unwrap();
707 assert_eq!(read_lease(root).as_ref(), Some(&lease));
708 assert!(live_lease(root).is_some());
710 clear_lease(root).unwrap();
711 assert!(read_lease(root).is_none());
712 assert!(matches!(
714 stop(root),
715 Err(OrchestratorError::NotRunning { .. })
716 ));
717 std::fs::remove_dir_all(root).ok();
718 }
719
720 #[test]
723 fn a_stale_lease_is_not_live() {
724 let root = &scratch("stale");
725 let root = root.as_path();
726 write_lease(
727 root,
728 &Lease {
729 pid: 0x7FFF_FFFF,
731 started_at: "2026-09-04T00:00:00Z".into(),
732 root: root.to_path_buf(),
733 },
734 )
735 .unwrap();
736 assert!(read_lease(root).is_some(), "the file is still there");
737 assert!(live_lease(root).is_none(), "but nothing is serving it");
738 std::fs::remove_dir_all(root).ok();
739 }
740
741 #[test]
742 fn the_service_unit_names_the_home_the_entry_and_its_install_command() {
743 let root = &scratch("unit");
744 let root = root.as_path();
745 let entry = PathBuf::from("/opt/supercode/sdk/orchestrator/bin/orchestrator.mjs");
746 let unit = service_unit(root, &entry, "/usr/bin/node");
747 assert!(unit.text.contains(&root.display().to_string()));
748 assert!(unit.text.contains("orchestrator.mjs"));
749 assert!(unit.text.contains(SERVICE_NAME));
750 assert!(unit
751 .install_command
752 .contains(&unit.path.display().to_string()));
753 assert!(unit.path.starts_with(root.join(SERVICE_DIR)));
754 assert_eq!(
755 unit.kind,
756 if cfg!(target_os = "macos") {
757 "launchd"
758 } else {
759 "systemd"
760 }
761 );
762 std::fs::remove_dir_all(root).ok();
763 }
764
765 #[test]
768 fn service_status_reports_the_label_and_installs_nothing() {
769 let root = &scratch("service-status");
770 let root = root.as_path();
771 let state = service_status(root);
772 assert_eq!(state.label, SERVICE_NAME);
773 assert!(
774 matches!(state.kind, "launchd" | "systemd" | "none"),
775 "{state:?}"
776 );
777 assert!(
778 !root.join(SERVICE_DIR).exists(),
779 "asking never writes a unit"
780 );
781 std::fs::remove_dir_all(root).ok();
782 }
783
784 #[test]
787 fn a_program_is_resolved_absolutely_for_the_service_manager() {
788 assert_eq!(absolute_program("/usr/bin/env"), "/usr/bin/env");
789 let resolved = absolute_program("sh");
790 assert!(resolved.starts_with('/'), "{resolved}");
791 assert_eq!(
793 absolute_program("definitely-not-a-program"),
794 "definitely-not-a-program"
795 );
796 }
797
798 #[test]
801 fn the_daemon_entry_resolves_in_this_checkout() {
802 let entry = daemon_entry().expect("sdk/orchestrator/bin/orchestrator.mjs");
803 assert!(entry.ends_with(DAEMON_ENTRY));
804 }
805}