Skip to content

Commit 6d1ab80

Browse files
committed
Move Darwin installer steps into installer pkg
1 parent b27ff47 commit 6d1ab80

File tree

12 files changed

+380
-151
lines changed

12 files changed

+380
-151
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,53 @@
11
package installer
22

3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
)
9+
310
const (
411
LogFile = "installer.log"
512
)
613

714
func (u *Installer) TempDir() string {
815
return u.tempDir
916
}
17+
18+
func (u *Installer) copyUpdater() (string, error) {
19+
if err := os.MkdirAll(u.tempDir, 0o755); err != nil {
20+
return "", fmt.Errorf("failed to create temp dir: %w", err)
21+
}
22+
23+
dstPath := filepath.Join(u.tempDir, updaterBinary)
24+
25+
execPath, err := os.Executable()
26+
if err != nil {
27+
return "", fmt.Errorf("failed to get executable path: %w", err)
28+
}
29+
30+
updaterSrcPath := filepath.Join(filepath.Dir(execPath), uiName)
31+
32+
srcFile, err := os.Open(updaterSrcPath)
33+
if err != nil {
34+
return "", fmt.Errorf("failed to open source file: %w", err)
35+
}
36+
defer srcFile.Close()
37+
38+
dstFile, err := os.Create(dstPath)
39+
if err != nil {
40+
return "", fmt.Errorf("failed to create destination file: %w", err)
41+
}
42+
defer dstFile.Close()
43+
44+
if _, err := io.Copy(dstFile, srcFile); err != nil {
45+
return "", fmt.Errorf("failed to copy file: %w", err)
46+
}
47+
48+
if err := os.Chmod(dstPath, 0755); err != nil {
49+
return "", fmt.Errorf("failed to set permissions: %w", err)
50+
}
51+
52+
return dstPath, nil
53+
}
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package installer
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"os/user"
9+
"path/filepath"
10+
"strings"
11+
"syscall"
12+
"time"
13+
14+
log "github.com/sirupsen/logrus"
15+
)
16+
17+
// RunInstallation starts the updater process to run the installation
18+
// This will run by the original service process
19+
func (u *Installer) RunInstallation(installerFile string) error {
20+
log.Infof("running installer")
21+
// copy the current binary to a temp location as an updater binary
22+
updaterPath, err := u.copyUpdater()
23+
if err != nil {
24+
return err
25+
}
26+
27+
// the directory where the service has been installed
28+
workspace, err := getServiceDir()
29+
if err != nil {
30+
return err
31+
}
32+
33+
installerFullPath := filepath.Join(u.tempDir, installerFile)
34+
35+
log.Infof("run updater binary: %s, %s, %s", updaterPath, installerFullPath, workspace)
36+
37+
updateCmd := exec.Command(updaterPath, "--temp-dir", u.tempDir, "--installer-file", installerFullPath, "--service-dir", workspace, "--dry-run=true")
38+
39+
// Start the updater process asynchronously
40+
if err := updateCmd.Start(); err != nil {
41+
return err
42+
}
43+
44+
// Release the process so the OS can fully detach it
45+
if err := updateCmd.Process.Release(); err != nil {
46+
log.Warnf("failed to release updater process: %v", err)
47+
}
48+
49+
log.Infof("updater started with PID %d", updateCmd.Process.Pid)
50+
return nil
51+
}
52+
53+
// Setup runs the installer with appropriate arguments and manages the daemon/UI state
54+
// This will be run by the updater process
55+
func (u *Installer) Setup(ctx context.Context, dryRun bool, installerPath string, daemonFolder string) (resultErr error) {
56+
installerFolder := filepath.Dir(installerPath)
57+
resultHandler := NewResultHandler(installerFolder)
58+
it := TypeOfInstaller(ctx)
59+
// Always ensure daemon and UI are restarted after setup
60+
defer func() {
61+
log.Infof("starting daemon back")
62+
if err := u.startDaemon(daemonFolder); err != nil {
63+
log.Errorf("failed to start daemon: %v", err)
64+
}
65+
66+
// todo prevent to run UI multiple times
67+
log.Infof("starting UI back")
68+
if err := u.startUIAsUser(daemonFolder); err != nil {
69+
log.Errorf("failed to start UI: %v", err)
70+
}
71+
72+
result := Result{
73+
Success: resultErr == nil,
74+
ExecutedAt: time.Now(),
75+
}
76+
if resultErr != nil {
77+
result.Error = resultErr.Error()
78+
}
79+
log.Infof("write out result")
80+
if err := resultHandler.Write(result); err != nil {
81+
log.Errorf("failed to write update result: %v", err)
82+
}
83+
}()
84+
85+
if dryRun {
86+
log.Infof("dry-run mode enabled, skipping actual installation")
87+
resultErr = fmt.Errorf("dry-run mode enabled")
88+
return
89+
}
90+
91+
switch it {
92+
case TypePKG:
93+
log.Infof("installing pkg file")
94+
if err := u.installPkgFile(ctx, installerPath); err != nil {
95+
resultErr = err
96+
break
97+
}
98+
log.Infof("pkg file installed successfully")
99+
return
100+
case TypeHomebrew:
101+
log.Infof("updating homebrew")
102+
if err := u.updateHomeBrew(ctx); err != nil {
103+
resultErr = err
104+
break
105+
}
106+
log.Infof("homebrew updated successfully")
107+
}
108+
109+
return nil
110+
}
111+
112+
func (u *Installer) startDaemon(daemonFolder string) error {
113+
log.Infof("starting netbird service")
114+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
115+
defer cancel()
116+
117+
cmd := exec.CommandContext(ctx, filepath.Join(daemonFolder, daemonName), "service", "start")
118+
if output, err := cmd.CombinedOutput(); err != nil {
119+
log.Warnf("failed to start netbird service: %v, output: %s", err, string(output))
120+
return err
121+
}
122+
log.Infof("netbird service started successfully")
123+
return nil
124+
}
125+
126+
func (u *Installer) startUIAsUser(daemonFolder string) error {
127+
uiPath := filepath.Join(daemonFolder, uiName)
128+
log.Infof("starting netbird-ui: %s", uiPath)
129+
130+
// Get the current console user
131+
cmd := exec.Command("stat", "-f", "%Su", "/dev/console")
132+
output, err := cmd.Output()
133+
if err != nil {
134+
return fmt.Errorf("failed to get console user: %w", err)
135+
}
136+
137+
username := strings.TrimSpace(string(output))
138+
if username == "" || username == "root" {
139+
return fmt.Errorf("no active user session found")
140+
}
141+
142+
log.Infof("starting UI for user: %s", username)
143+
144+
// Get user's UID
145+
userInfo, err := user.Lookup(username)
146+
if err != nil {
147+
return fmt.Errorf("failed to lookup user %s: %w", username, err)
148+
}
149+
150+
// Start the UI process as the console user using launchctl
151+
// This ensures the app runs in the user's context with proper GUI access
152+
launchCmd := exec.Command("launchctl", "asuser", userInfo.Uid, "sudo", "-u", username, uiPath)
153+
154+
// Set the user's home directory for proper macOS app behavior
155+
launchCmd.Env = append(os.Environ(), "HOME="+userInfo.HomeDir)
156+
157+
if err := launchCmd.Start(); err != nil {
158+
return fmt.Errorf("failed to start UI process: %w", err)
159+
}
160+
161+
// Release the process so it can run independently
162+
if err := launchCmd.Process.Release(); err != nil {
163+
log.Warnf("failed to release UI process: %v", err)
164+
}
165+
166+
log.Infof("netbird-ui started successfully for user %s", username)
167+
return nil
168+
}
169+
170+
func (u *Installer) installPkgFile(ctx context.Context, path string) error {
171+
volume := "/"
172+
for _, v := range strings.Split(path, "\n") {
173+
trimmed := strings.TrimSpace(v)
174+
if strings.HasPrefix(trimmed, "volume: ") {
175+
volume = strings.Split(trimmed, ": ")[1]
176+
}
177+
}
178+
179+
cmd := exec.CommandContext(ctx, "installer", "-pkg", path, "-target", volume)
180+
if err := cmd.Start(); err != nil {
181+
return fmt.Errorf("error running pkg file: %w", err)
182+
}
183+
res, err := cmd.CombinedOutput()
184+
// todo write out log result to file too
185+
if err != nil {
186+
return fmt.Errorf("error running pkg file: %w, output: %s", err, string(res))
187+
}
188+
return nil
189+
}
190+
191+
func (u *Installer) updateHomeBrew(ctx context.Context) error {
192+
// Homebrew must be run as a non-root user
193+
// To find out which user installed NetBird using HomeBrew we can check the owner of our brew tap directory
194+
fileInfo, err := os.Stat("/opt/homebrew/Library/Taps/netbirdio/homebrew-tap/")
195+
if err != nil {
196+
return fmt.Errorf("error getting homebrew installation path info: %w", err)
197+
}
198+
199+
fileSysInfo, ok := fileInfo.Sys().(*syscall.Stat_t)
200+
if !ok {
201+
return fmt.Errorf("error checking file owner, sysInfo type is %T not *syscall.Stat_t", fileInfo.Sys())
202+
}
203+
204+
// Get username from UID
205+
installer, err := user.LookupId(fmt.Sprintf("%d", fileSysInfo.Uid))
206+
if err != nil {
207+
return fmt.Errorf("error looking up brew installer user: %w", err)
208+
}
209+
userName := installer.Name
210+
// Get user HOME, required for brew to run correctly
211+
// https://github.com/Homebrew/brew/issues/15833
212+
homeDir := installer.HomeDir
213+
// Homebrew does not support installing specific versions
214+
// Thus it will always update to latest and ignore targetVersion
215+
upgradeArgs := []string{"-u", userName, "/opt/homebrew/bin/brew", "upgrade", "netbirdio/tap/netbird"}
216+
// Check if netbird-ui is installed
217+
cmd := exec.CommandContext(ctx, "brew", "info", "--json", "netbirdio/tap/netbird-ui")
218+
err = cmd.Run()
219+
if err == nil {
220+
// netbird-ui is installed
221+
upgradeArgs = append(upgradeArgs, "netbirdio/tap/netbird-ui")
222+
}
223+
cmd = exec.CommandContext(ctx, "sudo", upgradeArgs...)
224+
cmd.Env = append(cmd.Env, "HOME="+homeDir)
225+
226+
// Homebrew upgrade doesn't restart the client on its own
227+
// So we have to wait for it to finish running and ensure it's done
228+
// And then basically restart the netbird service
229+
err = cmd.Run()
230+
if err != nil {
231+
return fmt.Errorf("error running brew upgrade: %w", err)
232+
}
233+
234+
currentPID := os.Getpid()
235+
236+
// Restart netbird service after the fact
237+
// This is a workaround since attempting to restart using launchctl will kill the service and die before starting
238+
// the service again as it's a child process
239+
// using SIGTERM should ensure a clean shutdown
240+
process, err := os.FindProcess(currentPID)
241+
if err != nil {
242+
return fmt.Errorf("error finding current process: %w", err)
243+
}
244+
err = process.Signal(syscall.SIGTERM)
245+
if err != nil {
246+
return fmt.Errorf("error sending SIGTERM to current process: %w", err)
247+
}
248+
// We're dying now, which should restart us
249+
250+
return nil
251+
}
252+
253+
func getServiceDir() (string, error) {
254+
exePath, err := os.Executable()
255+
if err != nil {
256+
return "", err
257+
}
258+
return filepath.Dir(exePath), nil
259+
}

client/internal/updatemanager/installer/installer_log.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ func (c *Installer) LogFiles() []string {
66
return []string{}
77
}
88

9-
func (u *Installer) CleanUpInstallerFile() error {
9+
func (u *Installer) CleanUpInstallerFiles() error {
1010
return nil
1111
}

client/internal/updatemanager/installer/installer_new.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !windows
1+
//go:build !windows && !darwin
22

33
package installer
44

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package installer
2+
3+
import "os"
4+
5+
var (
6+
// defaultTempDir is OS specific
7+
defaultTempDir = "/var/lib/netbird/tmp-install"
8+
)
9+
10+
type Installer struct {
11+
tempDir string
12+
}
13+
14+
// New used by the service
15+
func New() (*Installer, error) {
16+
return &Installer{
17+
tempDir: defaultTempDir,
18+
}, nil
19+
}
20+
21+
// NewWithDir used by the updater process, get the tempDir from the service via cmd line
22+
func NewWithDir(tempDir string) *Installer {
23+
return &Installer{
24+
tempDir: tempDir,
25+
}
26+
}
27+
28+
func (u *Installer) MakeTempDir() (string, error) {
29+
if err := os.MkdirAll(u.tempDir, 0o755); err != nil {
30+
return "", err
31+
}
32+
return u.tempDir, nil
33+
}

0 commit comments

Comments
 (0)