package codeserver import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" "zlh-agent/internal/provision/executil" "zlh-agent/internal/state" ) const ( marker = "/opt/zlh/.zlh/addons/code-server.installed" pidFile = "/opt/zlh/.zlh/addons/code-server.pid" binPath = "/opt/zlh/services/code-server/bin/code-server" ) func Installed() bool { _, err := os.Stat(marker) return err == nil } func Running() bool { raw, err := os.ReadFile(pidFile) if err == nil { pid, convErr := strconv.Atoi(strings.TrimSpace(string(raw))) if convErr == nil && pid > 0 { process, findErr := os.FindProcess(pid) if findErr == nil && process.Signal(syscall.Signal(0)) == nil { return true } } _ = os.Remove(pidFile) } pid, err := findRunningPID() if err != nil || pid <= 0 { return false } _ = os.WriteFile(pidFile, []byte(strconv.Itoa(pid)), 0o644) return true } func Verify() error { if !Installed() { return fmt.Errorf("code-server addon marker missing") } if _, err := os.Stat(binPath); err != nil { return fmt.Errorf("code-server binary missing at %s", binPath) } if _, err := exec.LookPath("code-server"); err != nil { return fmt.Errorf("code-server binary not found in PATH") } return nil } func findRunningPID() (int, error) { matches, err := filepath.Glob("/proc/[0-9]*/cmdline") if err != nil { return 0, err } for _, match := range matches { raw, err := os.ReadFile(match) if err != nil || len(raw) == 0 { continue } cmdline := strings.ReplaceAll(string(raw), "\x00", " ") if !strings.Contains(cmdline, "code-server") { continue } if !strings.Contains(cmdline, "--bind-addr 0.0.0.0:8080") { continue } pidStr := filepath.Base(filepath.Dir(match)) pid, err := strconv.Atoi(pidStr) if err != nil || pid <= 0 { continue } return pid, nil } return 0, fmt.Errorf("code-server process not found") } func Start(cfg state.Config) error { if !Installed() { return fmt.Errorf("code-server addon not installed") } if Running() { if err := Stop(); err != nil { return err } } return executil.RunEmbeddedScript("addons/codeserver/install.sh", launchEnv(cfg)...) } func Stop() error { raw, err := os.ReadFile(pidFile) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("read code-server pid: %w", err) } pid, err := strconv.Atoi(strings.TrimSpace(string(raw))) if err != nil || pid <= 0 { _ = os.Remove(pidFile) return fmt.Errorf("invalid code-server pid") } process, err := os.FindProcess(pid) if err != nil { _ = os.Remove(pidFile) return fmt.Errorf("find code-server process: %w", err) } if err := process.Signal(syscall.SIGTERM); err != nil && !errorsIsProcessDone(err) { return fmt.Errorf("stop code-server: %w", err) } for i := 0; i < 20; i++ { if !Running() { _ = os.Remove(pidFile) return nil } time.Sleep(250 * time.Millisecond) } if err := process.Signal(syscall.SIGKILL); err != nil && !errorsIsProcessDone(err) { return fmt.Errorf("kill code-server: %w", err) } _ = os.Remove(pidFile) return nil } func Restart(cfg state.Config) error { if err := Stop(); err != nil { return err } return Start(cfg) } func errorsIsProcessDone(err error) bool { return err == os.ErrProcessDone || strings.Contains(strings.ToLower(err.Error()), "process already finished") }