Skip to content

Commit aa8c1d5

Browse files
committed
ghost: implement service installation
1 parent ce8cd9a commit aa8c1d5

File tree

6 files changed

+667
-8
lines changed

6 files changed

+667
-8
lines changed

cmd/ghost/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ var tlsModeFlag = flag.String("tls", "detect",
3030
var download = flag.String("download", "", "file to download")
3131
var reset = flag.Bool("reset", false, "reset ghost and reload all configs")
3232
var status = flag.Bool("status", false, "show status of the client")
33+
var install = flag.Bool("install", false, "install system service")
3334

3435
func usage() {
3536
fmt.Fprintf(os.Stderr, "Usage: ghost OVERLORD_ADDR\n")
@@ -63,6 +64,15 @@ func main() {
6364
tlsMode = overlord.TLSForceDisable
6465
}
6566

67+
if *install {
68+
err := overlord.Install()
69+
if err != nil {
70+
log.Fatalf("Failed to install system service: %v", err)
71+
}
72+
fmt.Println("System service installed successfully")
73+
os.Exit(0)
74+
}
75+
6676
overlord.StartGhost(args, finalMid, *noLanDisc, *noRPCServer, *tlsCertFile,
6777
!*tlsNoVerify, *allowlist, *propFile, *download, *reset, *status, tlsMode)
6878
}

overlord/ghost.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,11 +1845,3 @@ func StartGhost(args []string, mid string, noLanDisc bool, noRPCServer bool,
18451845
}
18461846
}
18471847
}
1848-
1849-
func getCurrentUser() string {
1850-
user := os.Getenv("USER")
1851-
if user == "" {
1852-
return "root"
1853-
}
1854-
return user
1855-
}

overlord/sysutils_darwin.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import (
1212
"bytes"
1313
"errors"
1414
"fmt"
15+
"io"
16+
"os"
1517
"os/exec"
18+
"os/user"
19+
"path/filepath"
1620
"regexp"
1721
"unsafe"
1822
)
@@ -72,3 +76,160 @@ func Ttyname(fd uintptr) (string, error) {
7276
}
7377
return C.GoString(ttyname), nil
7478
}
79+
80+
// getCurrentUserHomeDir gets the home directory for the current user on macOS
81+
func getCurrentUserHomeDir() string {
82+
// Use os/user package to get current user's home
83+
if currentUser, err := user.Current(); err == nil {
84+
return currentUser.HomeDir
85+
}
86+
87+
// Fallback to HOME environment variable
88+
if homeDir := os.Getenv("HOME"); homeDir != "" {
89+
return homeDir
90+
}
91+
92+
// macOS-specific fallback using dscl (Directory Service Command Line)
93+
if username := getCurrentUser(); username != "unknown" {
94+
out, err := exec.Command("dscl", ".", "-read", "/Users/"+username, "NFSHomeDirectory").Output()
95+
if err == nil {
96+
re := regexp.MustCompile("NFSHomeDirectory: (.*)")
97+
ret := re.FindStringSubmatch(string(out))
98+
if len(ret) == 2 {
99+
return ret[1]
100+
}
101+
}
102+
}
103+
104+
// Ultimate fallback for macOS
105+
return "/Users/" + getCurrentUser()
106+
}
107+
108+
// Install installs and configures the ghost service for automatic startup on macOS
109+
func Install() error {
110+
execPath, err := os.Executable()
111+
if err != nil {
112+
return fmt.Errorf("failed to get executable path: %v", err)
113+
}
114+
115+
homeDir := getCurrentUserHomeDir()
116+
targetPath := filepath.Join(homeDir, ".local", "bin", "ghost")
117+
binDir := filepath.Join(homeDir, ".local", "bin")
118+
119+
err = os.MkdirAll(binDir, 0755)
120+
if err != nil {
121+
return fmt.Errorf("failed to create ~/.local/bin directory: %v", err)
122+
}
123+
124+
srcFile, err := os.Open(execPath)
125+
if err != nil {
126+
return fmt.Errorf("failed to open source file: %v", err)
127+
}
128+
defer srcFile.Close()
129+
130+
dstFile, err := os.Create(targetPath)
131+
if err != nil {
132+
return fmt.Errorf("failed to create target file: %v", err)
133+
}
134+
defer dstFile.Close()
135+
136+
_, err = io.Copy(dstFile, srcFile)
137+
if err != nil {
138+
return fmt.Errorf("failed to copy ghost binary: %v", err)
139+
}
140+
141+
err = os.Chmod(targetPath, 0755)
142+
if err != nil {
143+
return fmt.Errorf("failed to set executable permissions: %v", err)
144+
}
145+
146+
cmdParts := getServiceCommand()
147+
var programArgs []string
148+
programArgs = append(programArgs, targetPath)
149+
programArgs = append(programArgs, cmdParts...)
150+
151+
argsXML := ""
152+
for _, arg := range programArgs {
153+
if arg != "" { // Skip empty strings
154+
argsXML += fmt.Sprintf("\t\t<string>%s</string>\n", arg)
155+
}
156+
}
157+
158+
plistContent := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
159+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
160+
<plist version="1.0">
161+
<dict>
162+
<key>Label</key>
163+
<string>com.overlord.ghost</string>
164+
<key>ProgramArguments</key>
165+
<array>
166+
%s </array>
167+
<key>EnvironmentVariables</key>
168+
<dict>
169+
<key>SHELL</key>
170+
<string>/bin/bash</string>
171+
<key>HOME</key>
172+
<string>%s</string>
173+
<key>TERM</key>
174+
<string>xterm-256color</string>
175+
</dict>
176+
<key>RunAtLoad</key>
177+
<true/>
178+
<key>KeepAlive</key>
179+
<true/>
180+
<key>StandardOutPath</key>
181+
<string>/var/log/ghost.log</string>
182+
<key>StandardErrorPath</key>
183+
<string>/var/log/ghost.log</string>
184+
</dict>
185+
</plist>
186+
`, argsXML, homeDir)
187+
188+
launchAgentsDir := filepath.Join(homeDir, "Library", "LaunchAgents")
189+
plistFilePath := filepath.Join(launchAgentsDir, "com.overlord.ghost.plist")
190+
err = os.MkdirAll(launchAgentsDir, 0755)
191+
if err != nil {
192+
return fmt.Errorf("failed to create LaunchAgents directory: %v", err)
193+
}
194+
195+
plistFile, err := os.Create(plistFilePath)
196+
if err != nil {
197+
return fmt.Errorf("failed to create plist file: %v", err)
198+
}
199+
defer plistFile.Close()
200+
201+
_, err = plistFile.WriteString(plistContent)
202+
if err != nil {
203+
return fmt.Errorf("failed to write plist file: %v", err)
204+
}
205+
206+
fmt.Printf("Launchd service installed at %s\n", plistFilePath)
207+
208+
cmd := exec.Command("launchctl", "load", plistFilePath)
209+
err = cmd.Run()
210+
if err != nil {
211+
return fmt.Errorf("failed to load ghost service: %v", err)
212+
}
213+
fmt.Printf("Ghost service loaded and enabled for automatic startup\n")
214+
215+
if !isGhostRunning() {
216+
fmt.Printf("Starting ghost service...\n")
217+
cmd = exec.Command("launchctl", "start", "com.overlord.ghost")
218+
err = cmd.Run()
219+
if err != nil {
220+
fmt.Printf("Warning: failed to start ghost service: %v\n", err)
221+
fmt.Printf("The service should start automatically on next login\n")
222+
} else {
223+
fmt.Printf("Ghost service started successfully\n")
224+
}
225+
} else {
226+
fmt.Printf("Ghost service is already running\n")
227+
}
228+
229+
fmt.Printf("Ghost service installation completed successfully!\n")
230+
fmt.Printf("The ghost service will start automatically on user login.\n")
231+
fmt.Printf("To check if service is loaded, run: launchctl list | grep ghost\n")
232+
fmt.Printf("To unload the service, run: launchctl unload %s\n", plistFilePath)
233+
234+
return nil
235+
}

overlord/sysutils_linux.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"encoding/hex"
1010
"fmt"
1111
"os"
12+
"os/user"
13+
"path/filepath"
1214
"strings"
1315

1416
uuid "github.com/satori/go.uuid"
@@ -107,3 +109,169 @@ func Ttyname(fd uintptr) (string, error) {
107109

108110
return ttyPath, nil
109111
}
112+
113+
// getCurrentUserHomeDir gets the home directory for the current user on Linux
114+
func getCurrentUserHomeDir() string {
115+
// Use os/user package to get current user's home
116+
if currentUser, err := user.Current(); err == nil {
117+
return currentUser.HomeDir
118+
}
119+
120+
// Fallback to HOME environment variable
121+
if homeDir := os.Getenv("HOME"); homeDir != "" {
122+
return homeDir
123+
}
124+
125+
// Linux-specific fallback using /etc/passwd
126+
if username := getCurrentUser(); username != "unknown" {
127+
// Try to read from /etc/passwd
128+
if file, err := os.Open("/etc/passwd"); err == nil {
129+
defer file.Close()
130+
scanner := bufio.NewScanner(file)
131+
for scanner.Scan() {
132+
line := scanner.Text()
133+
fields := strings.Split(line, ":")
134+
if len(fields) >= 6 && fields[0] == username {
135+
return fields[5] // Home directory is the 6th field
136+
}
137+
}
138+
}
139+
}
140+
141+
return "/home/" + getCurrentUser()
142+
}
143+
144+
// Install installs and configures the ghost service for automatic startup on Linux
145+
func Install() error {
146+
execPath, err := os.Executable()
147+
if err != nil {
148+
return fmt.Errorf("failed to get executable path: %v", err)
149+
}
150+
151+
targetPath := "/opt/bin/ghost"
152+
153+
err = runWithSudo("mkdir", "-p", "/opt/bin")
154+
if err != nil {
155+
return fmt.Errorf("failed to create /opt/bin directory: %v", err)
156+
}
157+
158+
err = cpWithSudo(execPath, targetPath)
159+
if err != nil {
160+
return fmt.Errorf("failed to copy ghost binary: %v", err)
161+
}
162+
err = chmodWithSudo("755", targetPath)
163+
if err != nil {
164+
return fmt.Errorf("failed to set executable permissions: %v", err)
165+
}
166+
167+
homeDir := getCurrentUserHomeDir()
168+
currentUser := getCurrentUser()
169+
170+
cmdParts := getServiceCommand()
171+
cmdArgs := strings.Join(cmdParts, " ")
172+
173+
serviceContent := fmt.Sprintf(`[Unit]
174+
Description=Overlord Ghost Client
175+
After=network-online.target local-fs.target
176+
Wants=network-online.target
177+
RequiresMountsFor=%s
178+
179+
[Service]
180+
Type=simple
181+
User=%s
182+
Environment=SHELL=/bin/bash
183+
Environment=HOME=%s
184+
Environment=TERM=xterm-256color
185+
ExecStart=%s %s
186+
Restart=always
187+
RestartSec=5
188+
189+
[Install]
190+
WantedBy=multi-user.target
191+
`, homeDir, currentUser, homeDir, targetPath, cmdArgs)
192+
193+
serviceFilePath, err := getSystemdServicePath()
194+
if err != nil {
195+
return fmt.Errorf("failed to determine systemd directory: %v", err)
196+
}
197+
198+
tempServiceFile, err := os.CreateTemp("", "ghost-*.service")
199+
if err != nil {
200+
return fmt.Errorf("failed to create temp service file: %v", err)
201+
}
202+
defer os.Remove(tempServiceFile.Name())
203+
204+
_, err = tempServiceFile.WriteString(serviceContent)
205+
if err != nil {
206+
return fmt.Errorf("failed to write temp service file: %v", err)
207+
}
208+
tempServiceFile.Close()
209+
210+
err = cpWithSudo(tempServiceFile.Name(), serviceFilePath)
211+
if err != nil {
212+
return fmt.Errorf("failed to install systemd service file: %v", err)
213+
}
214+
215+
fmt.Printf("Systemd service installed at %s\n", serviceFilePath)
216+
217+
err = runWithSudo("systemctl", "daemon-reload")
218+
if err != nil {
219+
return fmt.Errorf("failed to reload systemd daemon: %v", err)
220+
}
221+
fmt.Printf("Systemd daemon reloaded\n")
222+
223+
err = runWithSudo("systemctl", "enable", "ghost")
224+
if err != nil {
225+
return fmt.Errorf("failed to enable ghost service: %v", err)
226+
}
227+
fmt.Printf("Ghost service enabled for automatic startup\n")
228+
229+
if !isGhostRunning() {
230+
fmt.Printf("Starting ghost service...\n")
231+
err = runWithSudo("systemctl", "start", "ghost")
232+
if err != nil {
233+
fmt.Printf("Warning: failed to start ghost service: %v\n", err)
234+
fmt.Printf("You can start it manually with: sudo systemctl start ghost\n")
235+
} else {
236+
fmt.Printf("Ghost service started successfully\n")
237+
}
238+
} else {
239+
fmt.Printf("Ghost service is already running\n")
240+
}
241+
242+
fmt.Printf("Ghost service installation completed successfully!\n")
243+
fmt.Printf("The ghost service will start automatically on system boot.\n")
244+
fmt.Printf("To check service status, run: sudo systemctl status ghost\n")
245+
246+
return nil
247+
}
248+
249+
// getSystemdServicePath determines the best systemd directory for service installation
250+
func getSystemdServicePath() (string, error) {
251+
// Systemd service directories in order of preference:
252+
// 1. /etc/systemd/system - Local configuration (highest priority, for admin-installed services)
253+
// 2. /usr/lib/systemd/system - Package manager installed services (RHEL/CentOS/Fedora)
254+
// 3. /lib/systemd/system - Package manager installed services (Debian/Ubuntu)
255+
256+
serviceDirs := []string{
257+
"/usr/lib/systemd/system",
258+
"/etc/systemd/system",
259+
"/lib/systemd/system",
260+
}
261+
262+
// Try to find an existing systemd directory
263+
for _, dir := range serviceDirs {
264+
if _, err := os.Stat(dir); err == nil {
265+
return filepath.Join(dir, "ghost.service"), nil
266+
}
267+
}
268+
269+
// If no existing directory found, try to create /etc/systemd/system (preferred for admin installs)
270+
preferredDir := "/etc/systemd/system"
271+
err := runWithSudo("mkdir", "-p", preferredDir)
272+
if err != nil {
273+
return "", fmt.Errorf("failed to create systemd directory %s: %v", preferredDir, err)
274+
}
275+
276+
return filepath.Join(preferredDir, "ghost.service"), nil
277+
}

0 commit comments

Comments
 (0)