Skip to main content

vtcode_core/scheduler/
service.rs

1use anyhow::{Context, Result, anyhow};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use super::{LAUNCHD_LABEL, SERVICE_NAME, atomic_write};
6use vtcode_commons::VtCodePaths;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ServiceManager {
10    Launchd,
11    SystemdUser,
12}
13
14impl ServiceManager {
15    #[must_use]
16    pub fn current() -> Option<Self> {
17        #[cfg(target_os = "macos")]
18        {
19            return Some(Self::Launchd);
20        }
21        #[cfg(all(unix, not(target_os = "macos")))]
22        {
23            return Some(Self::SystemdUser);
24        }
25        #[cfg_attr(
26            unix,
27            allow(
28                unreachable_code,
29                reason = "Platform-specific returns make the fallback unreachable on Unix."
30            )
31        )]
32        None
33    }
34}
35
36#[derive(Debug, Clone)]
37pub struct ServiceInstallPlan {
38    pub manager: ServiceManager,
39    pub path: PathBuf,
40    pub contents: String,
41}
42
43pub fn render_service_install_plan(executable_path: &Path) -> Result<ServiceInstallPlan> {
44    let manager = ServiceManager::current()
45        .ok_or_else(|| anyhow!("Durable scheduler services are unsupported on this platform"))?;
46    let path = match manager {
47        ServiceManager::Launchd => dirs::home_dir()
48            .ok_or_else(|| anyhow!("Failed to resolve home directory"))?
49            .join("Library/LaunchAgents")
50            .join(format!("{LAUNCHD_LABEL}.plist")),
51        ServiceManager::SystemdUser => dirs::home_dir()
52            .ok_or_else(|| anyhow!("Failed to resolve home directory"))?
53            .join(".config/systemd/user")
54            .join(format!("{SERVICE_NAME}.service")),
55    };
56    let contents = match manager {
57        ServiceManager::Launchd => render_launchd_plist(executable_path),
58        ServiceManager::SystemdUser => render_systemd_unit(executable_path),
59    };
60    Ok(ServiceInstallPlan { manager, path, contents })
61}
62
63pub fn install_service_file(executable_path: &Path) -> Result<ServiceInstallPlan> {
64    let plan = render_service_install_plan(executable_path)?;
65    if let Some(parent) = plan.path.parent() {
66        VtCodePaths::ensure_user_dir(parent).with_context(|| format!("Failed to create {}", parent.display()))?;
67    }
68    atomic_write(&plan.path, plan.contents.as_bytes())?;
69    Ok(plan)
70}
71
72pub fn uninstall_service_file() -> Result<Option<(ServiceManager, PathBuf, bool)>> {
73    let Some(manager) = ServiceManager::current() else {
74        return Ok(None);
75    };
76    let path = render_service_install_plan(Path::new("/tmp/vtcode"))?.path;
77    if path.exists() {
78        fs::remove_file(&path).with_context(|| format!("Failed to remove {}", path.display()))?;
79        return Ok(Some((manager, path, true)));
80    }
81    Ok(Some((manager, path, false)))
82}
83
84#[must_use]
85pub fn render_launchd_plist(executable_path: &Path) -> String {
86    format!(
87        r#"<?xml version="1.0" encoding="UTF-8"?>
88<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
89<plist version="1.0">
90  <dict>
91    <key>Label</key>
92    <string>{LAUNCHD_LABEL}</string>
93    <key>ProgramArguments</key>
94    <array>
95      <string>{}</string>
96      <string>schedule</string>
97      <string>serve</string>
98    </array>
99    <key>RunAtLoad</key>
100    <true/>
101    <key>KeepAlive</key>
102    <true/>
103  </dict>
104</plist>
105"#,
106        xml_escape(executable_path.display().to_string())
107    )
108}
109
110#[must_use]
111pub fn render_systemd_unit(executable_path: &Path) -> String {
112    format!(
113        "[Unit]\nDescription=VT Code scheduler\n\n[Service]\nType=simple\nExecStart={} schedule serve\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n",
114        shell_words::quote(executable_path.to_string_lossy().as_ref())
115    )
116}
117
118fn xml_escape(value: String) -> String {
119    value
120        .replace('&', "&amp;")
121        .replace('<', "&lt;")
122        .replace('>', "&gt;")
123        .replace('"', "&quot;")
124        .replace('\'', "&apos;")
125}