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:
70
scripts/setup-linux.sh
Executable file
70
scripts/setup-linux.sh
Executable file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ZeroGravity — Linux setup
|
||||||
|
# Creates the zerogravity-ls system user for UID-scoped iptables isolation,
|
||||||
|
# installs the systemd user service, and builds the dns_redirect.so preload lib.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# ── 1. System user for UID isolation ──
|
||||||
|
echo "→ Creating zerogravity-ls system user…"
|
||||||
|
if id -u zerogravity-ls &>/dev/null; then
|
||||||
|
echo " Already exists."
|
||||||
|
else
|
||||||
|
sudo useradd --system --no-create-home --shell /usr/sbin/nologin zerogravity-ls
|
||||||
|
echo " Created."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. Sudoers rule (run commands as zerogravity-ls without password) ──
|
||||||
|
SUDOERS="/etc/sudoers.d/zerogravity"
|
||||||
|
echo "→ Installing sudoers rule…"
|
||||||
|
if [ -f "$SUDOERS" ]; then
|
||||||
|
echo " Already exists."
|
||||||
|
else
|
||||||
|
echo "$USER ALL=(zerogravity-ls) NOPASSWD: ALL" | sudo tee "$SUDOERS" > /dev/null
|
||||||
|
sudo chmod 0440 "$SUDOERS"
|
||||||
|
echo " Installed: $SUDOERS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Data directory permissions ──
|
||||||
|
echo "→ Setting up /tmp/zerogravity-standalone…"
|
||||||
|
sudo mkdir -p /tmp/zerogravity-standalone
|
||||||
|
sudo chmod 1777 /tmp/zerogravity-standalone
|
||||||
|
|
||||||
|
# ── 4. Config directory ──
|
||||||
|
echo "→ Setting up ~/.config/zerogravity…"
|
||||||
|
mkdir -p "$HOME/.config/zerogravity"
|
||||||
|
|
||||||
|
# ── 5. Systemd user service ──
|
||||||
|
echo "→ Installing systemd user service…"
|
||||||
|
UNIT_DIR="$HOME/.config/systemd/user"
|
||||||
|
mkdir -p "$UNIT_DIR"
|
||||||
|
cat > "$UNIT_DIR/zerogravity.service" << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=ZeroGravity Proxy
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=$PROJECT_DIR/target/release/zerogravity
|
||||||
|
WorkingDirectory=$PROJECT_DIR
|
||||||
|
Environment=RUST_LOG=info
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
EOF
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
echo " Installed: $UNIT_DIR/zerogravity.service"
|
||||||
|
echo " Enable with: systemctl --user enable zerogravity"
|
||||||
|
|
||||||
|
# ── 6. Build ──
|
||||||
|
echo "→ Building release binary…"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
cargo build --release 2>&1 | tail -1
|
||||||
|
echo ""
|
||||||
|
echo "✓ Setup complete. Start with: zg start"
|
||||||
65
scripts/setup-macos.sh
Executable file
65
scripts/setup-macos.sh
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ZeroGravity — macOS setup
|
||||||
|
# Installs a launchd plist for automatic startup and sets up config directories.
|
||||||
|
# No UID isolation on macOS — runs in headless/HTTPS_PROXY mode only.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
CONFIG_DIR="$HOME/Library/Application Support/zerogravity"
|
||||||
|
|
||||||
|
# ── 1. Config directory ──
|
||||||
|
echo "→ Setting up config directory…"
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
|
||||||
|
# ── 2. Data directory ──
|
||||||
|
echo "→ Setting up /tmp/zerogravity-standalone…"
|
||||||
|
mkdir -p /tmp/zerogravity-standalone
|
||||||
|
|
||||||
|
# ── 3. Launchd plist ──
|
||||||
|
echo "→ Installing launchd plist…"
|
||||||
|
PLIST_DIR="$HOME/Library/LaunchAgents"
|
||||||
|
PLIST="$PLIST_DIR/com.zerogravity.proxy.plist"
|
||||||
|
mkdir -p "$PLIST_DIR"
|
||||||
|
cat > "$PLIST" << EOF
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.zerogravity.proxy</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>$PROJECT_DIR/target/release/zerogravity</string>
|
||||||
|
</array>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>$PROJECT_DIR</string>
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>RUST_LOG</key>
|
||||||
|
<string>info</string>
|
||||||
|
</dict>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<dict>
|
||||||
|
<key>SuccessfulExit</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>$HOME/Library/Logs/zerogravity.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>$HOME/Library/Logs/zerogravity.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
echo " Installed: $PLIST"
|
||||||
|
echo " Start with: launchctl load $PLIST"
|
||||||
|
echo " Stop with: launchctl unload $PLIST"
|
||||||
|
|
||||||
|
# ── 4. Build ──
|
||||||
|
echo "→ Building release binary…"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
cargo build --release 2>&1 | tail -1
|
||||||
|
echo ""
|
||||||
|
echo "✓ Setup complete."
|
||||||
|
echo " Start with: launchctl load $PLIST"
|
||||||
|
echo " Or manually: zg start"
|
||||||
@@ -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-mobile"), hv("?0"));
|
||||||
h.insert(
|
h.insert(
|
||||||
HeaderName::from_static("sec-ch-ua-platform"),
|
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-Dest", hv("empty"));
|
||||||
h.insert("Sec-Fetch-Mode", hv("cors"));
|
h.insert("Sec-Fetch-Mode", hv("cors"));
|
||||||
@@ -499,12 +499,11 @@ impl Backend {
|
|||||||
|
|
||||||
fn discover() -> Result<BackendInner, String> {
|
fn discover() -> Result<BackendInner, String> {
|
||||||
// Try to find the real LS binary first (when MITM wrapper is installed,
|
// 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 wrapper is a shell script, while the real binary has .real suffix)
|
||||||
// the real binary is language_server_linux_x64.real)
|
|
||||||
let pid_output = Command::new("sh")
|
let pid_output = Command::new("sh")
|
||||||
.args([
|
.args([
|
||||||
"-c",
|
"-c",
|
||||||
"pgrep -f 'language_server_linux_x64\\.real' | head -1",
|
"pgrep -f 'language_server.*\\.real' | head -1",
|
||||||
])
|
])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("pgrep failed: {e}"))?;
|
.map_err(|e| format!("pgrep failed: {e}"))?;
|
||||||
@@ -513,10 +512,10 @@ fn discover() -> Result<BackendInner, String> {
|
|||||||
.trim()
|
.trim()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
// Fallback: find any language_server_linux process
|
// Fallback: find any language_server process
|
||||||
if pid.is_empty() {
|
if pid.is_empty() {
|
||||||
let pid_output = Command::new("sh")
|
let pid_output = Command::new("sh")
|
||||||
.args(["-c", "pgrep -f language_server_linux | head -1"])
|
.args(["-c", "pgrep -f language_server | head -1"])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("pgrep failed: {e}"))?;
|
.map_err(|e| format!("pgrep failed: {e}"))?;
|
||||||
pid = String::from_utf8_lossy(&pid_output.stdout)
|
pid = String::from_utf8_lossy(&pid_output.stdout)
|
||||||
@@ -611,8 +610,7 @@ fn discover() -> Result<BackendInner, String> {
|
|||||||
.ok()
|
.ok()
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
let home = std::env::var("HOME").unwrap_or_default();
|
let path = token_file_path();
|
||||||
let path = format!("{home}/.config/zerogravity/token");
|
|
||||||
fs::read_to_string(&path)
|
fs::read_to_string(&path)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
|
|||||||
@@ -21,26 +21,54 @@ struct DetectedVersions {
|
|||||||
/// back to its binary, then walking up to the app root. Falls back to
|
/// back to its binary, then walking up to the app root. Falls back to
|
||||||
/// well-known install paths.
|
/// well-known install paths.
|
||||||
fn find_install_dir() -> Option<String> {
|
fn find_install_dir() -> Option<String> {
|
||||||
// 1. Try tracing the running language server → /usr/share/antigravity/resources/app/extensions/...
|
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")]
|
||||||
|
{
|
||||||
if let Ok(output) = Command::new("sh")
|
if let Ok(output) = Command::new("sh")
|
||||||
.args(["-c", "pgrep -f language_server_linux | head -1"])
|
.args(["-c", "pgrep -f language_server | head -1"])
|
||||||
.output()
|
.output()
|
||||||
{
|
{
|
||||||
let pid = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let pid = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
if !pid.is_empty() {
|
if !pid.is_empty() {
|
||||||
if let Ok(exe) = fs::read_link(format!("/proc/{pid}/exe")) {
|
if let Ok(exe) = fs::read_link(format!("/proc/{pid}/exe")) {
|
||||||
let exe_str = exe.to_string_lossy().to_string();
|
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/") {
|
if let Some(idx) = exe_str.find("/resources/") {
|
||||||
return Some(exe_str[..idx].to_string());
|
return Some(exe_str[..idx].to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Fall back to well-known install paths
|
// 3. Fall back to well-known install paths
|
||||||
for path in &["/usr/share/antigravity", "/opt/Antigravity"] {
|
#[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() {
|
if fs::metadata(format!("{path}/resources/app/product.json")).is_ok() {
|
||||||
return Some(path.to_string());
|
return Some(path.to_string());
|
||||||
}
|
}
|
||||||
@@ -178,14 +206,23 @@ pub const LS_SERVICE: &str = "exa.language_server_pb.LanguageServerService";
|
|||||||
|
|
||||||
/// Log base directory for Antigravity.
|
/// Log base directory for Antigravity.
|
||||||
pub fn log_base() -> String {
|
pub fn log_base() -> String {
|
||||||
|
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());
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
||||||
format!("{home}/.config/Antigravity/logs")
|
format!("{home}/.config/Antigravity")
|
||||||
|
});
|
||||||
|
format!("{state_parent}/logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Token file path.
|
/// Token file path.
|
||||||
pub fn token_file_path() -> String {
|
pub fn token_file_path() -> String {
|
||||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
crate::platform::Platform::detect().token_path.to_string_lossy().to_string()
|
||||||
format!("{home}/.config/zerogravity/token")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User-Agent string matching the Electron webview — computed once.
|
/// User-Agent string matching the Electron webview — computed once.
|
||||||
|
|||||||
19
src/main.rs
19
src/main.rs
@@ -8,6 +8,7 @@ mod api;
|
|||||||
mod backend;
|
mod backend;
|
||||||
mod constants;
|
mod constants;
|
||||||
mod mitm;
|
mod mitm;
|
||||||
|
mod platform;
|
||||||
mod proto;
|
mod proto;
|
||||||
mod quota;
|
mod quota;
|
||||||
mod session;
|
mod session;
|
||||||
@@ -123,7 +124,7 @@ async fn main() {
|
|||||||
.status();
|
.status();
|
||||||
// Also kill any leftover standalone LS processes
|
// Also kill any leftover standalone LS processes
|
||||||
let _ = std::process::Command::new("pkill")
|
let _ = std::process::Command::new("pkill")
|
||||||
.args(["-f", "language_server_linux.*antigravity-standalone"])
|
.args(["-f", "language_server.*antigravity-standalone"])
|
||||||
.status();
|
.status();
|
||||||
// Retry once
|
// Retry once
|
||||||
match tokio::net::TcpListener::bind(&addr).await {
|
match tokio::net::TcpListener::bind(&addr).await {
|
||||||
@@ -240,8 +241,7 @@ async fn main() {
|
|||||||
.ok()
|
.ok()
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
let home = std::env::var("HOME").unwrap_or_default();
|
let path = crate::constants::token_file_path();
|
||||||
let path = format!("{home}/.config/zerogravity/token");
|
|
||||||
std::fs::read_to_string(&path)
|
std::fs::read_to_string(&path)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.trim().to_string())
|
.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.
|
/// 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> {
|
fn find_ls_binary_path() -> Option<String> {
|
||||||
// Try all running processes, look for ones that look like the LS
|
// Try all running processes, look for ones that look like the LS
|
||||||
let proc = std::path::Path::new("/proc");
|
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)");
|
let target_clean = target_str.trim_end_matches(" (deleted)");
|
||||||
// Match any binary that looks like the Antigravity LS
|
// Match any binary that looks like the Antigravity LS
|
||||||
if target_clean.contains("language_server_linux")
|
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")
|
|| target_clean.contains("antigravity-language-server")
|
||||||
{
|
{
|
||||||
// Strip .real suffix — if the wrapper exec'd the backup, we want the base name
|
// 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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn find_ls_binary_path() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the data directory for storing MITM CA cert/key.
|
/// Get the data directory for storing MITM CA cert/key.
|
||||||
fn dirs_data_dir() -> std::path::PathBuf {
|
fn dirs_data_dir() -> std::path::PathBuf {
|
||||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
crate::platform::Platform::detect().config_dir
|
||||||
std::path::PathBuf::from(home)
|
|
||||||
.join(".config")
|
|
||||||
.join("zerogravity")
|
|
||||||
}
|
}
|
||||||
|
|||||||
316
src/platform.rs
Normal file
316
src/platform.rs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
//! LS process discovery — finding, inspecting, and managing LS processes.
|
//! 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 crate::proto::wire::extract_proto_string;
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::process::{Command, Stdio};
|
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)");
|
let target_clean = target_str.trim_end_matches(" (deleted)");
|
||||||
// Must be the actual LS binary, not a bash script
|
// Must be the actual LS binary, not a bash script
|
||||||
if target_clean.contains("language_server_linux")
|
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")
|
|| target_clean.contains("antigravity-language-server")
|
||||||
{
|
{
|
||||||
return Ok(name_str.to_string());
|
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}"))
|
.map_err(|e| format!("Failed to get port: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the dedicated LS system user exists.
|
/// Check if the dedicated LS system user exists (Linux only).
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
pub(super) fn has_ls_user() -> bool {
|
pub(super) fn has_ls_user() -> bool {
|
||||||
Command::new("id")
|
platform::supports_uid_isolation()
|
||||||
.args(["-u", LS_USER])
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.status()
|
|
||||||
.map(|s| s.success())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the PID of a language_server process owned by a specific user.
|
/// 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.
|
/// 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> {
|
pub(super) fn find_ls_pid_for_user(user: &str) -> Result<u32, String> {
|
||||||
let output = Command::new("pgrep")
|
let output = Command::new("pgrep")
|
||||||
.args(["-u", user, "-f", "language_server_linux"])
|
.args(["-u", user, "-f", "language_server"])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("pgrep failed: {e}"))?;
|
.map_err(|e| format!("pgrep failed: {e}"))?;
|
||||||
|
|
||||||
@@ -152,9 +146,11 @@ pub(super) fn cleanup_orphaned_ls() {
|
|||||||
return;
|
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")
|
let output = Command::new("pgrep")
|
||||||
.args(["-u", LS_USER, "-f", "language_server_linux"])
|
.args(["-u", ls_user.as_str(), "-f", "language_server"])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let pids: Vec<u32> = match 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.
|
// and the sudoers rule allows ALL commands as antigravity-ls.
|
||||||
for pid in &pids {
|
for pid in &pids {
|
||||||
let ok = Command::new("sudo")
|
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())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.status()
|
.status()
|
||||||
@@ -204,7 +200,7 @@ pub(super) fn cleanup_orphaned_ls() {
|
|||||||
|
|
||||||
// Force-kill any survivors
|
// Force-kill any survivors
|
||||||
let still_alive = Command::new("pgrep")
|
let still_alive = Command::new("pgrep")
|
||||||
.args(["-u", LS_USER, "-f", "language_server_linux"])
|
.args(["-u", ls_user.as_str(), "-f", "language_server"])
|
||||||
.output()
|
.output()
|
||||||
.map(|o| !o.stdout.is_empty())
|
.map(|o| !o.stdout.is_empty())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@@ -213,7 +209,7 @@ pub(super) fn cleanup_orphaned_ls() {
|
|||||||
info!("Orphaned LS still alive, force killing");
|
info!("Orphaned LS still alive, force killing");
|
||||||
for pid in &pids {
|
for pid in &pids {
|
||||||
let _ = Command::new("sudo")
|
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())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.status();
|
.status();
|
||||||
@@ -222,14 +218,14 @@ pub(super) fn cleanup_orphaned_ls() {
|
|||||||
|
|
||||||
// Final check
|
// Final check
|
||||||
let still_alive = Command::new("pgrep")
|
let still_alive = Command::new("pgrep")
|
||||||
.args(["-u", LS_USER, "-f", "language_server_linux"])
|
.args(["-u", ls_user.as_str(), "-f", "language_server"])
|
||||||
.output()
|
.output()
|
||||||
.map(|o| !o.stdout.is_empty())
|
.map(|o| !o.stdout.is_empty())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if still_alive {
|
if still_alive {
|
||||||
eprintln!("\n \x1b[1;31m⚠ Cannot kill orphaned LS process\x1b[0m");
|
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 {
|
} else {
|
||||||
info!("Orphaned LS processes cleaned up");
|
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>)> {
|
pub(super) fn read_oauth_from_state_db() -> Option<(String, Vec<u8>)> {
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
|
||||||
let home = std::env::var("HOME").ok()?;
|
let db_path = paths().state_db_path;
|
||||||
let db_path = format!("{home}/.config/Antigravity/User/globalStorage/state.vscdb");
|
|
||||||
|
|
||||||
// Check the DB file exists
|
// Check the DB file exists
|
||||||
if !std::path::Path::new(&db_path).exists() {
|
if !std::path::Path::new(&db_path).exists() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod discovery;
|
|||||||
mod spawn;
|
mod spawn;
|
||||||
mod stub;
|
mod stub;
|
||||||
|
|
||||||
|
use crate::platform::{self, Platform};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -16,25 +17,14 @@ use uuid::Uuid;
|
|||||||
// Re-export public API
|
// Re-export public API
|
||||||
pub use spawn::StandaloneLS;
|
pub use spawn::StandaloneLS;
|
||||||
|
|
||||||
/// Default path to the LS binary.
|
/// Source for the DNS redirect preload library (compiled at runtime, Linux only).
|
||||||
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).
|
|
||||||
const DNS_REDIRECT_C_SOURCE: &str = include_str!("../mitm/dns_redirect.c");
|
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.
|
/// Config needed to bootstrap the standalone LS.
|
||||||
///
|
///
|
||||||
/// In normal mode, discovered from the running main 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.
|
/// Build the dns_redirect.so preload library if it doesn't already exist.
|
||||||
///
|
///
|
||||||
/// The library hooks `getaddrinfo()` via LD_PRELOAD to redirect Google API
|
/// Linux only — hooks `getaddrinfo()` via LD_PRELOAD to redirect Google API
|
||||||
/// domain lookups to 127.0.0.1. This is needed because the LS binary uses
|
/// domain lookups to 127.0.0.1.
|
||||||
/// CGO for DNS resolution (libc getaddrinfo) but raw syscalls for connect(),
|
|
||||||
/// so only DNS can be intercepted via LD_PRELOAD.
|
|
||||||
///
|
///
|
||||||
/// Returns the path to the .so on success, None on failure.
|
/// Returns the path to the .so on success, None on failure.
|
||||||
fn build_dns_redirect_so() -> Option<String> {
|
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
|
// Skip rebuild if already exists
|
||||||
if std::path::Path::new(so_path).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
|
// Compile: gcc -shared -fPIC -o dns_redirect.so dns_redirect.c -ldl
|
||||||
let output = Command::new("gcc")
|
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();
|
.output();
|
||||||
|
|
||||||
match output {
|
match output {
|
||||||
Ok(out) if out.status.success() => {
|
Ok(out) if out.status.success() => {
|
||||||
info!("Built dns_redirect.so at {so_path}");
|
info!("Built dns_redirect.so at {so_path}");
|
||||||
// Clean up source
|
|
||||||
let _ = std::fs::remove_file(&c_path);
|
let _ = std::fs::remove_file(&c_path);
|
||||||
Some(so_path.to_string())
|
Some(so_path.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
//! StandaloneLS — process lifecycle (spawn, wait, kill).
|
//! 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::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::constants;
|
||||||
use crate::proto;
|
use crate::proto;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -57,13 +58,16 @@ impl StandaloneLS {
|
|||||||
1, // DETECT_AND_USE_PROXY_ENABLED
|
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)
|
// Setup data dir (mode 1777 so both current user and zerogravity-ls can write)
|
||||||
let gemini_dir = format!("{DATA_DIR}/.gemini");
|
let gemini_dir = format!("{data_dir}/.gemini");
|
||||||
let app_data_dir = format!("{DATA_DIR}/.gemini/zerogravity-standalone");
|
let app_data_dir = format!("{data_dir}/.gemini/zerogravity-standalone");
|
||||||
let annotations_dir = format!("{app_data_dir}/annotations");
|
let annotations_dir = format!("{app_data_dir}/annotations");
|
||||||
let brain_dir = format!("{app_data_dir}/brain");
|
let brain_dir = format!("{app_data_dir}/brain");
|
||||||
for dir in [
|
for dir in [
|
||||||
DATA_DIR,
|
data_dir.as_str(),
|
||||||
&gemini_dir,
|
&gemini_dir,
|
||||||
&app_data_dir,
|
&app_data_dir,
|
||||||
&annotations_dir,
|
&annotations_dir,
|
||||||
@@ -83,7 +87,7 @@ impl StandaloneLS {
|
|||||||
eprintln!(
|
eprintln!(
|
||||||
"\n ⚠ Data dir {} is not writable (owned by another user from previous sudo run)\n \
|
"\n ⚠ Data dir {} is not writable (owned by another user from previous sudo run)\n \
|
||||||
Fix with: sudo chmod -R a+rwX {}\n",
|
Fix with: sudo chmod -R a+rwX {}\n",
|
||||||
app_data_dir, DATA_DIR
|
app_data_dir, data_dir
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let _ = std::fs::remove_file(&test_path);
|
let _ = std::fs::remove_file(&test_path);
|
||||||
@@ -142,9 +146,7 @@ impl StandaloneLS {
|
|||||||
.ok()
|
.ok()
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
let home = std::env::var("HOME").unwrap_or_default();
|
std::fs::read_to_string(&p.token_path)
|
||||||
let path = format!("{home}/.config/zerogravity/token");
|
|
||||||
std::fs::read_to_string(&path)
|
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
@@ -234,7 +236,7 @@ impl StandaloneLS {
|
|||||||
|
|
||||||
// Build env vars for the LS process
|
// Build env vars for the LS process
|
||||||
let mut env_vars: Vec<(String, String)> =
|
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 MITM is enabled, add SSL + proxy env vars
|
||||||
if let Some(mitm) = mitm_config {
|
if let Some(mitm) = mitm_config {
|
||||||
@@ -242,9 +244,9 @@ impl StandaloneLS {
|
|||||||
// need a combined bundle: system CAs + our MITM CA
|
// need a combined bundle: system CAs + our MITM CA
|
||||||
// Write to /tmp — accessible by zerogravity-ls user
|
// Write to /tmp — accessible by zerogravity-ls user
|
||||||
// (user's ~/.config/ is not traversable by other UIDs)
|
// (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 =
|
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)
|
let mitm_ca = std::fs::read_to_string(&mitm.ca_cert_path)
|
||||||
.map_err(|e| format!("Failed to read MITM CA cert: {e}"))?;
|
.map_err(|e| format!("Failed to read MITM CA cert: {e}"))?;
|
||||||
std::fs::write(&combined_ca_path, format!("{system_ca}\n{mitm_ca}"))
|
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).
|
// OR when running in headless mode (no sudo at all).
|
||||||
// With iptables, all outbound traffic is transparently redirected at the
|
// With iptables, all outbound traffic is transparently redirected at the
|
||||||
// kernel level — setting HTTPS_PROXY on top causes double-proxying.
|
// 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")
|
// 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(("HTTPS_PROXY".into(), mitm.proxy_addr.clone()));
|
||||||
env_vars.push(("HTTP_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(("LD_PRELOAD".into(), so));
|
||||||
env_vars.push((
|
env_vars.push((
|
||||||
"DNS_REDIRECT_LOG".into(),
|
"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 headless mode, never use sudo — run as current user
|
||||||
// In normal mode, use sudo if 'zerogravity-ls' user exists
|
// 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 {
|
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");
|
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 {
|
for (k, v) in &env_vars {
|
||||||
c.arg(format!("{k}={v}"));
|
c.arg(format!("{k}={v}"));
|
||||||
}
|
}
|
||||||
c.arg(LS_BINARY_PATH);
|
c.arg(ls_binary.as_str());
|
||||||
c.args(&args);
|
c.args(&args);
|
||||||
c
|
c
|
||||||
} else {
|
} else {
|
||||||
debug!("Spawning LS as current user");
|
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);
|
c.args(&args);
|
||||||
for (k, v) in &env_vars {
|
for (k, v) in &env_vars {
|
||||||
c.env(k, v);
|
c.env(k, v);
|
||||||
@@ -317,7 +321,7 @@ impl StandaloneLS {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Capture stderr for debugging — logs to /tmp so we can diagnose LS failures
|
// 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}"))?;
|
.map_err(|e| format!("Failed to create LS debug log: {e}"))?;
|
||||||
cmd.stdin(Stdio::piped())
|
cmd.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
@@ -343,7 +347,7 @@ impl StandaloneLS {
|
|||||||
// Give sudo a moment to spawn the real process
|
// Give sudo a moment to spawn the real process
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
// Find the LS process owned by zerogravity-ls user
|
// 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 {
|
} else {
|
||||||
Some(child.id())
|
Some(child.id())
|
||||||
};
|
};
|
||||||
@@ -423,10 +427,11 @@ impl StandaloneLS {
|
|||||||
if self.use_sudo {
|
if self.use_sudo {
|
||||||
// The child is sudo which already exited. Kill the actual LS.
|
// The child is sudo which already exited. Kill the actual LS.
|
||||||
if let Some(pid) = self.ls_pid {
|
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)
|
// Run kill AS the zerogravity-ls user (same UID can signal)
|
||||||
let ok = std::process::Command::new("sudo")
|
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())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.status()
|
.status()
|
||||||
@@ -437,7 +442,7 @@ impl StandaloneLS {
|
|||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
// Force kill if still alive
|
// Force kill if still alive
|
||||||
let _ = std::process::Command::new("sudo")
|
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())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.status();
|
.status();
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ pub struct TraceCollector {
|
|||||||
|
|
||||||
impl TraceCollector {
|
impl TraceCollector {
|
||||||
pub fn new(enabled: bool) -> Self {
|
pub fn new(enabled: bool) -> Self {
|
||||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
let traces_dir = crate::platform::Platform::detect().traces_dir;
|
||||||
let traces_dir = PathBuf::from(home)
|
|
||||||
.join(".config")
|
|
||||||
.join("zerogravity")
|
|
||||||
.join("traces");
|
|
||||||
Self {
|
Self {
|
||||||
enabled,
|
enabled,
|
||||||
traces_dir,
|
traces_dir,
|
||||||
|
|||||||
Reference in New Issue
Block a user