Skip to content

Commit 0ab1da5

Browse files
authored
feat(updater): refactor installer logic and implement automatic daemon background updates (#33)
EXTENDED DESCRIPTION: Refactors the installer and updater for smarter, self-contained operation: - Installer download-on-demand: auto-downloads latest release if files are missing - Graceful shutdown via PID file (SIGTERM + SIGKILL safety check), replaces pkill -f - Updater delegates entirely to rag-code-install via --upgrade handoff - AutoUpdate background worker in daemon (goroutine, 10s after boot) - Shared DownloadVerifyAndApply helper eliminates code duplication - Checksum-verified downloads (supply-chain security) - Proper tempDir cleanup on all error paths - Dependency fix: golang.org/x/tools downgraded to v0.33.0 for Go 1.24 compat
2 parents 0988f6a + 7248209 commit 0ab1da5

9 files changed

Lines changed: 575 additions & 203 deletions

File tree

cmd/rag-code-install/main.go

Lines changed: 138 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"encoding/json"
56
"flag"
67
"fmt"
@@ -10,12 +11,16 @@ import (
1011
"os/exec"
1112
"path/filepath"
1213
"runtime"
14+
"strconv"
1315
"strings"
16+
"syscall"
1417
"time"
1518

19+
"github.com/codeclysm/extract/v3"
1620
"github.com/doITmagic/rag-code-mcp/internal/config"
1721
"github.com/doITmagic/rag-code-mcp/internal/healthcheck"
1822
"github.com/doITmagic/rag-code-mcp/internal/uninstall"
23+
"github.com/doITmagic/rag-code-mcp/internal/updater"
1924
)
2025

2126
var (
@@ -70,24 +75,41 @@ func main() {
7075
fail(fmt.Sprintf("Failed to create directories: %v", err))
7176
}
7277

73-
// 2. Determine current location
78+
// 2. Determine current location strictly
7479
execPath, _ := os.Executable()
7580
sourceDir := filepath.Dir(execPath)
76-
resourceDir := sourceDir
7781

78-
// If we are running from bin/, resources might be in the parent dir
79-
if _, err := os.Stat(filepath.Join(resourceDir, "README.md")); os.IsNotExist(err) {
80-
parentDir := filepath.Dir(resourceDir)
81-
if _, err := os.Stat(filepath.Join(parentDir, "README.md")); err == nil {
82-
resourceDir = parentDir
83-
}
84-
}
85-
86-
// 3. Define files to install
8782
binaries := []string{"rag-code-mcp", "rag-code-install"}
8883
resources := []string{"README.md", "llms.txt", "LICENSE", "config.yaml"}
8984

90-
log("Copying files from: " + sourceDir)
85+
// Fast Check: Are files present?
86+
missingFiles := false
87+
for _, f := range append(binaries, resources...) {
88+
name := f
89+
if runtime.GOOS == "windows" && (f == "rag-code-mcp" || f == "rag-code-install") {
90+
name += ".exe"
91+
}
92+
if _, err := os.Stat(filepath.Join(sourceDir, name)); os.IsNotExist(err) {
93+
missingFiles = true
94+
break
95+
}
96+
}
97+
98+
// If files are missing, we must download the latest release and use its contents as source
99+
tempDir := ""
100+
if missingFiles {
101+
log("Required files missing in current directory. Downloading latest release...")
102+
var err error
103+
tempDir, err = downloadAndExtractLatest()
104+
if err != nil {
105+
fail(fmt.Sprintf("Failed to download and extract release: %v", err))
106+
}
107+
defer os.RemoveAll(tempDir)
108+
sourceDir = tempDir
109+
log("Files extracted successfully from release.")
110+
} else {
111+
log("Copying files from: " + sourceDir)
112+
}
91113

92114
// Copy Binaries to bin/
93115
for _, b := range binaries {
@@ -110,7 +132,7 @@ func main() {
110132

111133
// Copy Resources to bin/
112134
for _, r := range resources {
113-
src := filepath.Join(resourceDir, r)
135+
src := filepath.Join(sourceDir, r)
114136
dst := filepath.Join(binPath, r)
115137

116138
if r == "config.yaml" {
@@ -373,29 +395,67 @@ func stopRunningProcess(binPath string) {
373395
return
374396
}
375397

376-
log("Stopping existing process running: " + binPath)
398+
log("Stopping existing process gracefully: " + binPath)
399+
400+
// Attempt Graceful Shutdown using PID file
401+
home, err := os.UserHomeDir()
402+
if err == nil {
403+
pidPath := filepath.Join(home, ".ragcode", "daemon.pid")
404+
if data, err := os.ReadFile(pidPath); err == nil {
405+
pidStr := strings.TrimSpace(string(data))
406+
if pid, err := strconv.Atoi(pidStr); err == nil {
407+
log(fmt.Sprintf("Found daemon PID: %d. Sending termination signal...", pid))
408+
409+
// For Windows
410+
if runtime.GOOS == "windows" {
411+
_ = exec.Command("taskkill", "/PID", pidStr).Run()
412+
time.Sleep(2 * time.Second)
413+
_ = exec.Command("taskkill", "/F", "/PID", pidStr).Run()
414+
return
415+
}
416+
417+
// For Unix
418+
process, err := os.FindProcess(pid)
419+
if err == nil {
420+
// Send SIGTERM
421+
_ = process.Signal(syscall.SIGTERM)
422+
423+
// Wait up to 5 seconds for it to exit gracefully
424+
for i := 0; i < 50; i++ {
425+
if err := process.Signal(syscall.Signal(0)); err != nil {
426+
// Process is gone
427+
break
428+
}
429+
time.Sleep(100 * time.Millisecond)
430+
}
431+
432+
// After grace period, only SIGKILL if process still appears alive
433+
if err := process.Signal(syscall.Signal(0)); err == nil {
434+
_ = process.Signal(syscall.SIGKILL)
435+
time.Sleep(200 * time.Millisecond)
436+
}
437+
return
438+
}
439+
}
440+
}
441+
}
442+
443+
log("PID file not found or process untrackable. Using fallback termination...")
377444

378445
if runtime.GOOS == "windows" {
446+
_ = exec.Command("taskkill", "/IM", filepath.Base(binPath)).Run()
447+
time.Sleep(1 * time.Second)
379448
_ = exec.Command("taskkill", "/F", "/IM", filepath.Base(binPath)).Run()
380-
time.Sleep(500 * time.Millisecond)
381449
return
382450
}
383451

384-
// 1. Precise kill using full path
385-
_ = exec.Command("pkill", "-f", binPath).Run()
452+
// 1. Soft kill (SIGTERM)
453+
_ = exec.Command("pkill", "-15", "-f", binPath).Run()
454+
time.Sleep(1 * time.Second)
386455

387-
// 2. Fallback using lsof to find PIDs mapping this binary
388-
if _, err := exec.LookPath("lsof"); err == nil {
389-
cmd := exec.Command("lsof", "-t", binPath)
390-
if output, err := cmd.Output(); err == nil {
391-
pids := strings.Fields(string(output))
392-
for _, pid := range pids {
393-
_ = exec.Command("kill", "-9", pid).Run()
394-
}
395-
}
396-
}
456+
// 2. Hard kill (SIGKILL) fallback
457+
_ = exec.Command("pkill", "-9", "-f", binPath).Run()
397458

398-
// Give it a moment to release file handles
399459
time.Sleep(500 * time.Millisecond)
400460
}
401461

@@ -810,6 +870,56 @@ func updateZedConfig(displayName, path, binPath, transport string, ssePort int)
810870
return false
811871
}
812872

873+
func downloadAndExtractLatest() (string, error) {
874+
// Pass "v0.0.0" to force a network check to get the absolute latest if we're a naked installer
875+
ctx := context.Background()
876+
info, err := updater.CheckForUpdates(ctx, "v0.0.0", true)
877+
if err != nil {
878+
return "", fmt.Errorf("failed to check for updates: %w", err)
879+
}
880+
if info == nil || info.AssetURL == "" {
881+
return "", fmt.Errorf("no update asset found")
882+
}
883+
884+
tempDir, err := os.MkdirTemp("", "ragcode-install-*")
885+
if err != nil {
886+
return "", err
887+
}
888+
cleanup := true
889+
defer func() {
890+
if cleanup {
891+
os.RemoveAll(tempDir)
892+
}
893+
}()
894+
895+
archivePath := filepath.Join(tempDir, "release-archive")
896+
log("Downloading " + info.AssetURL)
897+
898+
// Use DownloadAndVerify for checksum-verified downloads (supply-chain security)
899+
if err := info.DownloadAndVerify(ctx, archivePath); err != nil {
900+
return "", fmt.Errorf("failed to download and verify release: %w", err)
901+
}
902+
903+
log("Extracting archive...")
904+
f, err := os.Open(archivePath)
905+
if err != nil {
906+
return "", err
907+
}
908+
defer f.Close()
909+
910+
extractDir := filepath.Join(tempDir, "extracted")
911+
if err := os.MkdirAll(extractDir, 0755); err != nil {
912+
return "", err
913+
}
914+
915+
if err := extract.Archive(ctx, f, extractDir, nil); err != nil {
916+
return "", err
917+
}
918+
919+
cleanup = false // success — caller is responsible for cleanup
920+
return extractDir, nil
921+
}
922+
813923
// buildMCPServerEntry builds the stdio (binary) MCP server entry.
814924
func buildMCPServerEntry(ideKey, binPath string) map[string]interface{} {
815925
entry := map[string]interface{}{

cmd/rag-code-mcp/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
)
1818

1919
var (
20-
Version = "2.1.56"
20+
Version = "2.1.58"
2121
Commit = "none"
2222
Date = "24.10.2025"
2323
)

0 commit comments

Comments
 (0)