feat: add cross-platform support via platform detection module

Introduces src/platform.rs with OS detection and env var overrides.
All hardcoded Linux paths replaced with Platform::detect() across
8 source files. Key changes:

- New Platform struct with 11 fields (all overridable via env vars)
- /proc/ access gated to Linux (#[cfg(target_os = "linux")])
- pgrep/pkill patterns broadened for cross-platform LS discovery
- sec-ch-ua-platform header now dynamic per OS
- Token, traces, config, CA cert paths use platform module
- LD_PRELOAD DNS redirect gated to Linux only
- Setup scripts for Linux (systemd) and macOS (launchd)
- find_ls_binary_path has cross-platform stubs

All 46 tests pass, cargo check clean.
This commit is contained in:
Nikketryhard
2026-02-18 02:13:23 -06:00
parent 7136c0e53c
commit 8a9662edea
10 changed files with 587 additions and 108 deletions

View File

@@ -51,7 +51,7 @@ static STATIC_HEADERS: LazyLock<HeaderMap> = LazyLock::new(|| {
h.insert(HeaderName::from_static("sec-ch-ua-mobile"), hv("?0"));
h.insert(
HeaderName::from_static("sec-ch-ua-platform"),
hv("\"Linux\""),
hv(&format!("\"{}\"", crate::platform::Platform::detect().os_name)),
);
h.insert("Sec-Fetch-Dest", hv("empty"));
h.insert("Sec-Fetch-Mode", hv("cors"));
@@ -499,12 +499,11 @@ impl Backend {
fn discover() -> Result<BackendInner, String> {
// Try to find the real LS binary first (when MITM wrapper is installed,
// the wrapper is a shell script named language_server_linux_x64, while
// the real binary is language_server_linux_x64.real)
// the wrapper is a shell script, while the real binary has .real suffix)
let pid_output = Command::new("sh")
.args([
"-c",
"pgrep -f 'language_server_linux_x64\\.real' | head -1",
"pgrep -f 'language_server.*\\.real' | head -1",
])
.output()
.map_err(|e| format!("pgrep failed: {e}"))?;
@@ -513,10 +512,10 @@ fn discover() -> Result<BackendInner, String> {
.trim()
.to_string();
// Fallback: find any language_server_linux process
// Fallback: find any language_server process
if pid.is_empty() {
let pid_output = Command::new("sh")
.args(["-c", "pgrep -f language_server_linux | head -1"])
.args(["-c", "pgrep -f language_server | head -1"])
.output()
.map_err(|e| format!("pgrep failed: {e}"))?;
pid = String::from_utf8_lossy(&pid_output.stdout)
@@ -611,8 +610,7 @@ fn discover() -> Result<BackendInner, String> {
.ok()
.filter(|s| !s.is_empty())
.or_else(|| {
let home = std::env::var("HOME").unwrap_or_default();
let path = format!("{home}/.config/zerogravity/token");
let path = token_file_path();
fs::read_to_string(&path)
.ok()
.map(|s| s.trim().to_string())

View File

@@ -21,26 +21,54 @@ struct DetectedVersions {
/// back to its binary, then walking up to the app root. Falls back to
/// well-known install paths.
fn find_install_dir() -> Option<String> {
// 1. Try tracing the running language server → /usr/share/antigravity/resources/app/extensions/...
if let Ok(output) = Command::new("sh")
.args(["-c", "pgrep -f language_server_linux | head -1"])
.output()
let p = crate::platform::Platform::detect();
// 1. Check if platform-detected app_root exists
let app_root_parent = std::path::Path::new(&p.app_root)
.parent()
.and_then(|p| p.parent())
.map(|p| p.to_string_lossy().to_string());
if let Some(ref dir) = app_root_parent {
if fs::metadata(format!("{dir}/resources/app/product.json")).is_ok() {
return Some(dir.clone());
}
}
// 2. Try tracing the running language server via /proc (Linux only)
#[cfg(target_os = "linux")]
{
let pid = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !pid.is_empty() {
if let Ok(exe) = fs::read_link(format!("/proc/{pid}/exe")) {
let exe_str = exe.to_string_lossy().to_string();
// exe is like: /usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64
// We want: /usr/share/antigravity
if let Some(idx) = exe_str.find("/resources/") {
return Some(exe_str[..idx].to_string());
if let Ok(output) = Command::new("sh")
.args(["-c", "pgrep -f language_server | head -1"])
.output()
{
let pid = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !pid.is_empty() {
if let Ok(exe) = fs::read_link(format!("/proc/{pid}/exe")) {
let exe_str = exe.to_string_lossy().to_string();
if let Some(idx) = exe_str.find("/resources/") {
return Some(exe_str[..idx].to_string());
}
}
}
}
}
// 2. Fall back to well-known install paths
for path in &["/usr/share/antigravity", "/opt/Antigravity"] {
// 3. Fall back to well-known install paths
#[cfg(target_os = "linux")]
let candidates = ["/usr/share/antigravity", "/opt/Antigravity"];
#[cfg(target_os = "macos")]
let candidates = [
"/Applications/Antigravity.app/Contents",
&format!("{}/Applications/Antigravity.app/Contents", std::env::var("HOME").unwrap_or_default()),
];
#[cfg(target_os = "windows")]
let candidates = [
&format!("{}\\Programs\\Antigravity", std::env::var("LOCALAPPDATA").unwrap_or_default()),
];
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
let candidates: [&str; 0] = [];
for path in &candidates {
if fs::metadata(format!("{path}/resources/app/product.json")).is_ok() {
return Some(path.to_string());
}
@@ -178,14 +206,23 @@ pub const LS_SERVICE: &str = "exa.language_server_pb.LanguageServerService";
/// Log base directory for Antigravity.
pub fn log_base() -> String {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
format!("{home}/.config/Antigravity/logs")
let p = crate::platform::Platform::detect();
// Antigravity logs live next to its state DB
let state_parent = std::path::Path::new(&p.state_db_path)
.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
format!("{home}/.config/Antigravity")
});
format!("{state_parent}/logs")
}
/// Token file path.
pub fn token_file_path() -> String {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
format!("{home}/.config/zerogravity/token")
crate::platform::Platform::detect().token_path.to_string_lossy().to_string()
}
/// User-Agent string matching the Electron webview — computed once.

View File

@@ -8,6 +8,7 @@ mod api;
mod backend;
mod constants;
mod mitm;
mod platform;
mod proto;
mod quota;
mod session;
@@ -123,7 +124,7 @@ async fn main() {
.status();
// Also kill any leftover standalone LS processes
let _ = std::process::Command::new("pkill")
.args(["-f", "language_server_linux.*antigravity-standalone"])
.args(["-f", "language_server.*antigravity-standalone"])
.status();
// Retry once
match tokio::net::TcpListener::bind(&addr).await {
@@ -240,8 +241,7 @@ async fn main() {
.ok()
.filter(|s| !s.is_empty())
.or_else(|| {
let home = std::env::var("HOME").unwrap_or_default();
let path = format!("{home}/.config/zerogravity/token");
let path = crate::constants::token_file_path();
std::fs::read_to_string(&path)
.ok()
.map(|s| s.trim().to_string())
@@ -483,6 +483,7 @@ fn check_wrapper_installed() -> bool {
}
/// Find the LS binary path by reading /proc/<pid>/exe for known language server processes.
#[cfg(target_os = "linux")]
fn find_ls_binary_path() -> Option<String> {
// Try all running processes, look for ones that look like the LS
let proc = std::path::Path::new("/proc");
@@ -505,6 +506,8 @@ fn find_ls_binary_path() -> Option<String> {
let target_clean = target_str.trim_end_matches(" (deleted)");
// Match any binary that looks like the Antigravity LS
if target_clean.contains("language_server_linux")
|| target_clean.contains("language_server_darwin")
|| target_clean.contains("language_server_windows")
|| target_clean.contains("antigravity-language-server")
{
// Strip .real suffix — if the wrapper exec'd the backup, we want the base name
@@ -517,10 +520,12 @@ fn find_ls_binary_path() -> Option<String> {
None
}
#[cfg(not(target_os = "linux"))]
fn find_ls_binary_path() -> Option<String> {
None
}
/// Get the data directory for storing MITM CA cert/key.
fn dirs_data_dir() -> std::path::PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
std::path::PathBuf::from(home)
.join(".config")
.join("zerogravity")
crate::platform::Platform::detect().config_dir
}

316
src/platform.rs Normal file
View File

@@ -0,0 +1,316 @@
//! Platform detection and path resolution.
//!
//! All platform-specific paths are resolved here. Every path can be overridden
//! via environment variables, falling back to OS-specific defaults.
//!
//! # Env Var Overrides
//!
//! | Variable | Description |
//! |----------|-------------|
//! | `ZEROGRAVITY_LS_PATH` | Path to the Language Server binary |
//! | `ZEROGRAVITY_APP_ROOT` | Antigravity app root directory |
//! | `ZEROGRAVITY_DATA_DIR` | Standalone LS data directory |
//! | `ZEROGRAVITY_CONFIG_DIR` | ZeroGravity config directory |
//! | `ZEROGRAVITY_LS_USER` | System user for LS isolation (Linux only) |
//! | `ZEROGRAVITY_STATE_DB` | Path to Antigravity's state.vscdb |
//! | `SSL_CERT_FILE` | System CA certificate bundle |
use std::path::PathBuf;
/// All platform-specific paths, resolved once at startup.
#[derive(Debug, Clone)]
pub struct Platform {
/// Path to the Language Server binary.
pub ls_binary_path: String,
/// Antigravity app root (for ANTIGRAVITY_EDITOR_APP_ROOT).
pub app_root: String,
/// Data directory for standalone LS runtime files.
pub data_dir: String,
/// Config directory (~/.config/zerogravity or platform equivalent).
pub config_dir: PathBuf,
/// System CA certificate bundle path.
pub ca_cert_path: String,
/// System user for UID-scoped isolation (Linux only).
pub ls_user: String,
/// Path to Antigravity's state.vscdb.
pub state_db_path: String,
/// Token file path.
pub token_path: PathBuf,
/// Traces directory.
pub traces_dir: PathBuf,
/// DNS redirect shared library path (Linux only).
pub dns_redirect_so_path: String,
/// OS display name for sec-ch-ua-platform header ("Linux", "macOS", "Windows").
pub os_name: &'static str,
}
impl Platform {
/// Detect platform and resolve all paths.
///
/// Environment variables override platform defaults.
pub fn detect() -> Self {
let home = home_dir();
let config_dir = env_or("ZEROGRAVITY_CONFIG_DIR", || default_config_dir(&home));
let ls_binary_path = env_or("ZEROGRAVITY_LS_PATH", || default_ls_binary_path());
let app_root = env_or("ZEROGRAVITY_APP_ROOT", || default_app_root());
let data_dir = env_or("ZEROGRAVITY_DATA_DIR", || default_data_dir());
let ca_cert_path = env_or("SSL_CERT_FILE", || default_ca_cert_path());
let ls_user = env_or("ZEROGRAVITY_LS_USER", || "zerogravity-ls".into());
let state_db_path = env_or("ZEROGRAVITY_STATE_DB", || default_state_db_path(&home));
let dns_redirect_so_path = format!("{}/dns-redirect.so", &data_dir);
let config_dir = PathBuf::from(&config_dir);
let token_path = config_dir.join("token");
let traces_dir = config_dir.join("traces");
Self {
ls_binary_path,
app_root,
data_dir,
config_dir,
ca_cert_path,
ls_user,
state_db_path,
token_path,
traces_dir,
dns_redirect_so_path,
os_name: default_os_name(),
}
}
}
// ── Helpers ──
fn home_dir() -> String {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE")) // Windows
.unwrap_or_else(|_| "/tmp".into())
}
fn env_or(var: &str, default: impl FnOnce() -> String) -> String {
std::env::var(var).unwrap_or_else(|_| default())
}
// ── Platform defaults ──
#[cfg(target_os = "linux")]
fn default_ls_binary_path() -> String {
"/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64"
.into()
}
#[cfg(target_os = "macos")]
fn default_ls_binary_path() -> String {
let home = home_dir();
// Check both /Applications and ~/Applications
for base in &[
"/Applications/Antigravity.app",
&format!("{home}/Applications/Antigravity.app"),
] {
let path = format!(
"{base}/Contents/Resources/app/extensions/antigravity/bin/language_server_darwin_arm64"
);
if std::path::Path::new(&path).exists() {
return path;
}
}
"/Applications/Antigravity.app/Contents/Resources/app/extensions/antigravity/bin/language_server_darwin_arm64".into()
}
#[cfg(target_os = "windows")]
fn default_ls_binary_path() -> String {
let local = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Users\\Default\\AppData\\Local".into());
format!("{local}\\Programs\\Antigravity\\resources\\app\\extensions\\antigravity\\bin\\language_server_windows_x64.exe")
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn default_ls_binary_path() -> String {
"language_server".into()
}
// ── App root ──
#[cfg(target_os = "linux")]
fn default_app_root() -> String {
"/usr/share/antigravity/resources/app".into()
}
#[cfg(target_os = "macos")]
fn default_app_root() -> String {
"/Applications/Antigravity.app/Contents/Resources/app".into()
}
#[cfg(target_os = "windows")]
fn default_app_root() -> String {
let local = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Users\\Default\\AppData\\Local".into());
format!("{local}\\Programs\\Antigravity\\resources\\app")
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn default_app_root() -> String {
".".into()
}
// ── Data dir ──
fn default_data_dir() -> String {
#[cfg(target_os = "windows")]
{
let temp = std::env::var("TEMP").unwrap_or_else(|_| "C:\\Temp".into());
format!("{temp}\\zerogravity-standalone")
}
#[cfg(not(target_os = "windows"))]
{
"/tmp/zerogravity-standalone".into()
}
}
// ── Config dir ──
fn default_config_dir(home: &str) -> String {
#[cfg(target_os = "macos")]
{
format!("{home}/Library/Application Support/zerogravity")
}
#[cfg(target_os = "windows")]
{
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| format!("{home}\\AppData\\Roaming"));
format!("{appdata}\\zerogravity")
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
format!("{home}/.config/zerogravity")
}
}
// ── CA certs ──
fn default_ca_cert_path() -> String {
#[cfg(target_os = "macos")]
{
"/etc/ssl/cert.pem".into()
}
#[cfg(target_os = "windows")]
{
// Windows uses native cert store, this is a fallback
String::new()
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
// Try common Linux paths
for path in &[
"/etc/ssl/certs/ca-certificates.crt",
"/etc/pki/tls/certs/ca-bundle.crt",
"/etc/ssl/ca-bundle.pem",
] {
if std::path::Path::new(path).exists() {
return path.to_string();
}
}
"/etc/ssl/certs/ca-certificates.crt".into()
}
}
// ── State DB ──
fn default_state_db_path(home: &str) -> String {
#[cfg(target_os = "macos")]
{
format!("{home}/Library/Application Support/Antigravity/User/globalStorage/state.vscdb")
}
#[cfg(target_os = "windows")]
{
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| format!("{home}\\AppData\\Roaming"));
format!("{appdata}\\Antigravity\\User\\globalStorage\\state.vscdb")
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
format!("{home}/.config/Antigravity/User/globalStorage/state.vscdb")
}
}
// ── OS name ──
fn default_os_name() -> &'static str {
#[cfg(target_os = "linux")]
{ "Linux" }
#[cfg(target_os = "macos")]
{ "macOS" }
#[cfg(target_os = "windows")]
{ "Windows" }
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{ "Unknown" }
}
// ── Platform queries ──
/// Returns true if running on Linux.
pub fn is_linux() -> bool {
cfg!(target_os = "linux")
}
/// Returns true if running on macOS.
#[allow(dead_code)]
pub fn is_macos() -> bool {
cfg!(target_os = "macos")
}
/// Returns true if running on Windows.
#[allow(dead_code)]
pub fn is_windows() -> bool {
cfg!(target_os = "windows")
}
/// Returns true if UID isolation (iptables + dedicated user) is available.
///
/// Only supported on Linux with the zerogravity-ls system user.
pub fn supports_uid_isolation() -> bool {
#[cfg(target_os = "linux")]
{
std::process::Command::new("id")
.args(["-u", "zerogravity-ls"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
/// Returns true if LD_PRELOAD DNS redirect is supported (Linux only).
pub fn supports_ld_preload() -> bool {
cfg!(target_os = "linux")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_doesnt_panic() {
let p = Platform::detect();
assert!(!p.ls_binary_path.is_empty());
assert!(!p.app_root.is_empty());
assert!(!p.data_dir.is_empty());
}
#[test]
fn test_env_override() {
std::env::set_var("ZEROGRAVITY_LS_PATH", "/custom/ls");
let p = Platform::detect();
assert_eq!(p.ls_binary_path, "/custom/ls");
std::env::remove_var("ZEROGRAVITY_LS_PATH");
}
#[test]
fn test_config_dir_has_token_and_traces() {
let p = Platform::detect();
assert!(p.token_path.ends_with("token"));
assert!(p.traces_dir.ends_with("traces"));
}
}

View File

@@ -1,6 +1,7 @@
//! LS process discovery — finding, inspecting, and managing LS processes.
use super::{MainLSConfig, LS_USER};
use super::{paths, MainLSConfig};
use crate::platform;
use crate::proto::wire::extract_proto_string;
use std::net::TcpListener;
use std::process::{Command, Stdio};
@@ -87,6 +88,8 @@ pub(super) fn find_main_ls_pid() -> Result<String, String> {
let target_clean = target_str.trim_end_matches(" (deleted)");
// Must be the actual LS binary, not a bash script
if target_clean.contains("language_server_linux")
|| target_clean.contains("language_server_darwin")
|| target_clean.contains("language_server_windows")
|| target_clean.contains("antigravity-language-server")
{
return Ok(name_str.to_string());
@@ -107,18 +110,9 @@ pub(super) fn find_free_port() -> Result<u16, String> {
.map_err(|e| format!("Failed to get port: {e}"))
}
/// Check if the dedicated LS system user exists.
///
/// When the user exists, the proxy spawns the LS as that UID so iptables
/// can scope the :443 redirect to only the standalone LS process.
/// Check if the dedicated LS system user exists (Linux only).
pub(super) fn has_ls_user() -> bool {
Command::new("id")
.args(["-u", LS_USER])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
platform::supports_uid_isolation()
}
/// Find the PID of a language_server process owned by a specific user.
@@ -126,7 +120,7 @@ pub(super) fn has_ls_user() -> bool {
/// Used to discover the actual LS process after sudo spawns it as a different user.
pub(super) fn find_ls_pid_for_user(user: &str) -> Result<u32, String> {
let output = Command::new("pgrep")
.args(["-u", user, "-f", "language_server_linux"])
.args(["-u", user, "-f", "language_server"])
.output()
.map_err(|e| format!("pgrep failed: {e}"))?;
@@ -152,9 +146,11 @@ pub(super) fn cleanup_orphaned_ls() {
return;
}
// Find all LS processes owned by antigravity-ls user
let ls_user = &paths().ls_user;
// Find all LS processes owned by the LS user
let output = Command::new("pgrep")
.args(["-u", LS_USER, "-f", "language_server_linux"])
.args(["-u", ls_user.as_str(), "-f", "language_server"])
.output();
let pids: Vec<u32> = match output {
@@ -180,7 +176,7 @@ pub(super) fn cleanup_orphaned_ls() {
// and the sudoers rule allows ALL commands as antigravity-ls.
for pid in &pids {
let ok = Command::new("sudo")
.args(["-n", "-u", LS_USER, "kill", "-TERM", &pid.to_string()])
.args(["-n", "-u", ls_user.as_str(), "kill", "-TERM", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
@@ -204,7 +200,7 @@ pub(super) fn cleanup_orphaned_ls() {
// Force-kill any survivors
let still_alive = Command::new("pgrep")
.args(["-u", LS_USER, "-f", "language_server_linux"])
.args(["-u", ls_user.as_str(), "-f", "language_server"])
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false);
@@ -213,7 +209,7 @@ pub(super) fn cleanup_orphaned_ls() {
info!("Orphaned LS still alive, force killing");
for pid in &pids {
let _ = Command::new("sudo")
.args(["-n", "-u", LS_USER, "kill", "-KILL", &pid.to_string()])
.args(["-n", "-u", ls_user.as_str(), "kill", "-KILL", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
@@ -222,14 +218,14 @@ pub(super) fn cleanup_orphaned_ls() {
// Final check
let still_alive = Command::new("pgrep")
.args(["-u", LS_USER, "-f", "language_server_linux"])
.args(["-u", ls_user.as_str(), "-f", "language_server"])
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false);
if still_alive {
eprintln!("\n \x1b[1;31m⚠ Cannot kill orphaned LS process\x1b[0m");
eprintln!(" Run: \x1b[1msudo pkill -u {LS_USER} -f language_server_linux\x1b[0m\n");
eprintln!(" Run: \x1b[1msudo pkill -u {} -f language_server\x1b[0m\n", ls_user);
}
} else {
info!("Orphaned LS processes cleaned up");
@@ -244,8 +240,7 @@ pub(super) fn cleanup_orphaned_ls() {
pub(super) fn read_oauth_from_state_db() -> Option<(String, Vec<u8>)> {
use base64::Engine;
let home = std::env::var("HOME").ok()?;
let db_path = format!("{home}/.config/Antigravity/User/globalStorage/state.vscdb");
let db_path = paths().state_db_path;
// Check the DB file exists
if !std::path::Path::new(&db_path).exists() {

View File

@@ -9,6 +9,7 @@ mod discovery;
mod spawn;
mod stub;
use crate::platform::{self, Platform};
use std::process::Command;
use tracing::info;
use uuid::Uuid;
@@ -16,25 +17,14 @@ use uuid::Uuid;
// Re-export public API
pub use spawn::StandaloneLS;
/// Default path to the LS binary.
const LS_BINARY_PATH: &str =
"/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64";
/// App root for ANTIGRAVITY_EDITOR_APP_ROOT env var.
const APP_ROOT: &str = "/usr/share/antigravity/resources/app";
/// Data directory for the standalone LS.
const DATA_DIR: &str = "/tmp/zerogravity-standalone";
/// System user for UID-scoped iptables isolation.
const LS_USER: &str = "zerogravity-ls";
/// Path for the compiled dns_redirect.so preload library.
const DNS_REDIRECT_SO_PATH: &str = "/tmp/zerogravity-dns-redirect.so";
/// Source file for the DNS redirect preload library (relative to binary).
/// Source for the DNS redirect preload library (compiled at runtime, Linux only).
const DNS_REDIRECT_C_SOURCE: &str = include_str!("../mitm/dns_redirect.c");
/// Get platform-resolved paths. Convenience accessor.
pub(crate) fn paths() -> Platform {
Platform::detect()
}
/// Config needed to bootstrap the standalone LS.
///
/// In normal mode, discovered from the running main LS.
@@ -76,14 +66,17 @@ pub fn discover_main_ls_config() -> Result<MainLSConfig, String> {
/// Build the dns_redirect.so preload library if it doesn't already exist.
///
/// The library hooks `getaddrinfo()` via LD_PRELOAD to redirect Google API
/// domain lookups to 127.0.0.1. This is needed because the LS binary uses
/// CGO for DNS resolution (libc getaddrinfo) but raw syscalls for connect(),
/// so only DNS can be intercepted via LD_PRELOAD.
/// Linux only — hooks `getaddrinfo()` via LD_PRELOAD to redirect Google API
/// domain lookups to 127.0.0.1.
///
/// Returns the path to the .so on success, None on failure.
fn build_dns_redirect_so() -> Option<String> {
let so_path = DNS_REDIRECT_SO_PATH;
if !platform::supports_ld_preload() {
return None;
}
let p = paths();
let so_path = &p.dns_redirect_so_path;
// Skip rebuild if already exists
if std::path::Path::new(so_path).exists() {
@@ -99,13 +92,12 @@ fn build_dns_redirect_so() -> Option<String> {
// Compile: gcc -shared -fPIC -o dns_redirect.so dns_redirect.c -ldl
let output = Command::new("gcc")
.args(["-shared", "-fPIC", "-o", so_path, &c_path, "-ldl"])
.args(["-shared", "-fPIC", "-o", so_path.as_str(), &c_path, "-ldl"])
.output();
match output {
Ok(out) if out.status.success() => {
info!("Built dns_redirect.so at {so_path}");
// Clean up source
let _ = std::fs::remove_file(&c_path);
Some(so_path.to_string())
}

View File

@@ -1,8 +1,9 @@
//! StandaloneLS — process lifecycle (spawn, wait, kill).
use super::discovery::{cleanup_orphaned_ls, find_free_port, find_ls_pid_for_user, has_ls_user, read_oauth_from_state_db};
use super::discovery::{cleanup_orphaned_ls, find_free_port, find_ls_pid_for_user, read_oauth_from_state_db};
use super::stub::stub_handle_connection;
use super::{build_dns_redirect_so, MainLSConfig, StandaloneMitmConfig, APP_ROOT, DATA_DIR, LS_BINARY_PATH, LS_USER};
use super::{build_dns_redirect_so, paths, MainLSConfig, StandaloneMitmConfig};
use crate::platform;
use crate::constants;
use crate::proto;
use std::io::Write;
@@ -57,13 +58,16 @@ impl StandaloneLS {
1, // DETECT_AND_USE_PROXY_ENABLED
);
let p = paths();
let data_dir = &p.data_dir;
// Setup data dir (mode 1777 so both current user and zerogravity-ls can write)
let gemini_dir = format!("{DATA_DIR}/.gemini");
let app_data_dir = format!("{DATA_DIR}/.gemini/zerogravity-standalone");
let gemini_dir = format!("{data_dir}/.gemini");
let app_data_dir = format!("{data_dir}/.gemini/zerogravity-standalone");
let annotations_dir = format!("{app_data_dir}/annotations");
let brain_dir = format!("{app_data_dir}/brain");
for dir in [
DATA_DIR,
data_dir.as_str(),
&gemini_dir,
&app_data_dir,
&annotations_dir,
@@ -83,7 +87,7 @@ impl StandaloneLS {
eprintln!(
"\n ⚠ Data dir {} is not writable (owned by another user from previous sudo run)\n \
Fix with: sudo chmod -R a+rwX {}\n",
app_data_dir, DATA_DIR
app_data_dir, data_dir
);
} else {
let _ = std::fs::remove_file(&test_path);
@@ -142,9 +146,7 @@ impl StandaloneLS {
.ok()
.filter(|s| !s.is_empty())
.or_else(|| {
let home = std::env::var("HOME").unwrap_or_default();
let path = format!("{home}/.config/zerogravity/token");
std::fs::read_to_string(&path)
std::fs::read_to_string(&p.token_path)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
@@ -234,7 +236,7 @@ impl StandaloneLS {
// Build env vars for the LS process
let mut env_vars: Vec<(String, String)> =
vec![("ANTIGRAVITY_EDITOR_APP_ROOT".into(), APP_ROOT.into())];
vec![("ANTIGRAVITY_EDITOR_APP_ROOT".into(), p.app_root.clone())];
// If MITM is enabled, add SSL + proxy env vars
if let Some(mitm) = mitm_config {
@@ -242,9 +244,9 @@ impl StandaloneLS {
// need a combined bundle: system CAs + our MITM CA
// Write to /tmp — accessible by zerogravity-ls user
// (user's ~/.config/ is not traversable by other UIDs)
let combined_ca_path = "/tmp/zerogravity-mitm-ca.pem".to_string();
let combined_ca_path = format!("{}/mitm-ca.pem", data_dir);
let system_ca =
std::fs::read_to_string("/etc/ssl/certs/ca-certificates.crt").unwrap_or_default();
std::fs::read_to_string(&p.ca_cert_path).unwrap_or_default();
let mitm_ca = std::fs::read_to_string(&mitm.ca_cert_path)
.map_err(|e| format!("Failed to read MITM CA cert: {e}"))?;
std::fs::write(&combined_ca_path, format!("{system_ca}\n{mitm_ca}"))
@@ -271,7 +273,7 @@ impl StandaloneLS {
// OR when running in headless mode (no sudo at all).
// With iptables, all outbound traffic is transparently redirected at the
// kernel level — setting HTTPS_PROXY on top causes double-proxying.
if headless || !has_ls_user() {
if headless || !platform::supports_uid_isolation() {
// proxy_addr already includes the scheme (e.g. "http://127.0.0.1:8742")
env_vars.push(("HTTPS_PROXY".into(), mitm.proxy_addr.clone()));
env_vars.push(("HTTP_PROXY".into(), mitm.proxy_addr.clone()));
@@ -286,7 +288,7 @@ impl StandaloneLS {
env_vars.push(("LD_PRELOAD".into(), so));
env_vars.push((
"DNS_REDIRECT_LOG".into(),
"/tmp/zerogravity-dns-redirect.log".into(),
format!("{data_dir}/dns-redirect.log"),
));
}
}
@@ -294,21 +296,23 @@ impl StandaloneLS {
// In headless mode, never use sudo — run as current user
// In normal mode, use sudo if 'zerogravity-ls' user exists
let use_sudo = !headless && has_ls_user();
let use_sudo = !headless && platform::supports_uid_isolation();
let ls_binary = &p.ls_binary_path;
let ls_user = &p.ls_user;
let mut cmd = if use_sudo {
info!("Using UID isolation: spawning LS as 'zerogravity-ls' user");
info!("Using UID isolation: spawning LS as '{}' user", ls_user);
let mut c = Command::new("sudo");
c.args(["-n", "-u", LS_USER, "--", "/usr/bin/env"]);
c.args(["-n", "-u", ls_user.as_str(), "--", "/usr/bin/env"]);
for (k, v) in &env_vars {
c.arg(format!("{k}={v}"));
}
c.arg(LS_BINARY_PATH);
c.arg(ls_binary.as_str());
c.args(&args);
c
} else {
debug!("Spawning LS as current user");
let mut c = Command::new(LS_BINARY_PATH);
let mut c = Command::new(ls_binary.as_str());
c.args(&args);
for (k, v) in &env_vars {
c.env(k, v);
@@ -317,7 +321,7 @@ impl StandaloneLS {
};
// Capture stderr for debugging — logs to /tmp so we can diagnose LS failures
let stderr_file = std::fs::File::create("/tmp/zerogravity-ls-debug.log")
let stderr_file = std::fs::File::create(format!("{data_dir}/ls-debug.log"))
.map_err(|e| format!("Failed to create LS debug log: {e}"))?;
cmd.stdin(Stdio::piped())
.stdout(Stdio::null())
@@ -343,7 +347,7 @@ impl StandaloneLS {
// Give sudo a moment to spawn the real process
std::thread::sleep(std::time::Duration::from_millis(500));
// Find the LS process owned by zerogravity-ls user
find_ls_pid_for_user(LS_USER).ok()
find_ls_pid_for_user(ls_user).ok()
} else {
Some(child.id())
};
@@ -423,10 +427,11 @@ impl StandaloneLS {
if self.use_sudo {
// The child is sudo which already exited. Kill the actual LS.
if let Some(pid) = self.ls_pid {
info!(pid, "Killing LS process via sudo -u {}", LS_USER);
let ls_user = &paths().ls_user;
info!(pid, "Killing LS process via sudo -u {}", ls_user);
// Run kill AS the zerogravity-ls user (same UID can signal)
let ok = std::process::Command::new("sudo")
.args(["-n", "-u", LS_USER, "kill", "-TERM", &pid.to_string()])
.args(["-n", "-u", ls_user.as_str(), "kill", "-TERM", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
@@ -437,7 +442,7 @@ impl StandaloneLS {
std::thread::sleep(std::time::Duration::from_millis(500));
// Force kill if still alive
let _ = std::process::Command::new("sudo")
.args(["-n", "-u", LS_USER, "kill", "-KILL", &pid.to_string()])
.args(["-n", "-u", ls_user.as_str(), "kill", "-KILL", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();

View File

@@ -20,11 +20,7 @@ pub struct TraceCollector {
impl TraceCollector {
pub fn new(enabled: bool) -> Self {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
let traces_dir = PathBuf::from(home)
.join(".config")
.join("zerogravity")
.join("traces");
let traces_dir = crate::platform::Platform::detect().traces_dir;
Self {
enabled,
traces_dir,