Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions cmd/internal/agent_daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ const (
busyGracePeriod = 20 * time.Minute
)

var daemonRuntimeLockPath = machinediagnostics.DefaultRuntimeLockPath
var (
daemonRuntimeLockPath = machinediagnostics.DefaultRuntimeLockPath
daemonLocatorPath = machinediagnostics.DefaultLocatorPath
)

type DaemonCmd struct {
*flags.GlobalFlags
Expand Down Expand Up @@ -105,6 +108,10 @@ func (cmd *DaemonCmd) Run(ctx context.Context) error {
if fallbackPath := os.Getenv(machinediagnostics.RuntimeLockPathEnv); fallbackPath != "" {
runtimeLockPath = fallbackPath
}
locatorPath := daemonLocatorPath
if fallbackPath := os.Getenv(machinediagnostics.RuntimeLocatorPathEnv); fallbackPath != "" {
locatorPath = fallbackPath
}
runtimeLock, err := machinediagnostics.AcquireRuntimeLock(runtimeLockPath)
if err != nil {
return err
Expand All @@ -125,7 +132,7 @@ func (cmd *DaemonCmd) Run(ctx context.Context) error {
} else {
cmd.recorder = recorder
if err := machinediagnostics.WriteLocator(
machinediagnostics.DefaultLocatorPath,
locatorPath,
machinediagnostics.Locator{
SessionID: recorder.SessionID(),
DiagnosticsDir: machinediagnostics.DiagnosticsDir(location.Root),
Expand Down
6 changes: 3 additions & 3 deletions cmd/internal/agent_daemon_diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ func (cmd *DaemonDiagnosticsCmd) readResponse() (machinediagnostics.ReadResponse
Now: time.Now(),
}
if cmd.StateRoot == "" && cmd.StateLayout == "" {
return machinediagnostics.ReadFromLocator(
machinediagnostics.DefaultLocatorPath,
return machinediagnostics.ReadActive(
options,
), nil
os.UserCacheDir,
)
}
if cmd.StateRoot == "" || cmd.StateLayout == "" {
return machinediagnostics.ReadResponse{}, fmt.Errorf(
Expand Down
7 changes: 5 additions & 2 deletions cmd/internal/agentworkspace/logs_daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ func NewLogsDaemonCmd(flags *flags.GlobalFlags) *cobra.Command {

func (cmd *LogsDaemonCmd) Run(ctx context.Context) error {
_ = ctx
response := machinediagnostics.ReadFromLocator(
machinediagnostics.DefaultLocatorPath,
response, err := machinediagnostics.ReadActive(
machinediagnostics.ReadOptions{
Limit: machinediagnostics.DefaultReadEvents, Now: time.Now(),
},
os.UserCacheDir,
)
if err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(response)
}
53 changes: 34 additions & 19 deletions pkg/daemon/agent/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,11 @@ func InstallDaemon(

if !isSystemdAvailable() {
log.Warnf("systemd not available, falling back to background process")
lockPath, err := fallbackRuntimeLockPath(os.Geteuid(), os.UserCacheDir)
runtimePaths, err := fallbackRuntimePaths(os.Geteuid(), os.UserCacheDir)
if err != nil {
return err
}
return startFallbackDaemon(executable, args, lockPath)
return startFallbackDaemon(executable, args, runtimePaths)
}
return installSystemdDaemon(opts, executable, args)
}
Expand Down Expand Up @@ -367,7 +367,7 @@ func ensureServiceRunning(needsReload bool, executable string, args []string) er
"systemctl", "restart", pkgconfig.BinaryName,
).CombinedOutput(); err != nil {
log.Warnf("Error restarting service: %s: %v", string(out), err)
return startFallbackDaemon(executable, args, machinediagnostics.DefaultRuntimeLockPath)
return startFallbackDaemon(executable, args, machinediagnostics.SystemRuntimePaths())
}
log.Infof("restarted Devsy daemon with updated config")
} else if !isServiceRunning() {
Expand All @@ -376,25 +376,24 @@ func ensureServiceRunning(needsReload bool, executable string, args []string) er
"systemctl", "start", pkgconfig.BinaryName,
).CombinedOutput(); err != nil {
log.Warnf("Error starting service: %s: %v", string(out), err)
return startFallbackDaemon(executable, args, machinediagnostics.DefaultRuntimeLockPath)
return startFallbackDaemon(executable, args, machinediagnostics.SystemRuntimePaths())
}
log.Infof("installed Devsy daemon into server")
}

return nil
}

func startFallbackDaemon(executable string, args []string, runtimeLockPath string) error {
func startFallbackDaemon(
executable string,
args []string,
runtimePaths machinediagnostics.RuntimePaths,
) error {
daemonArgs := args[1:] // strip executable path
err := command.StartBackgroundOnce(pkgconfig.DaemonProcessName, func() (*exec.Cmd, error) {
//nolint:gosec // executable is from os.Executable()
cmd := exec.Command(executable, daemonArgs...)
if runtimeLockPath != machinediagnostics.DefaultRuntimeLockPath {
cmd.Env = append(
os.Environ(),
machinediagnostics.RuntimeLockPathEnv+"="+runtimeLockPath,
)
}
cmd.Env = fallbackDaemonEnv(runtimePaths)
return cmd, nil
})
if err != nil {
Expand All @@ -404,18 +403,34 @@ func startFallbackDaemon(executable string, args []string, runtimeLockPath strin
return nil
}

func fallbackRuntimeLockPath(
func fallbackDaemonEnv(runtimePaths machinediagnostics.RuntimePaths) []string {
baseEnv := os.Environ()
filteredEnv := baseEnv[:0]
for _, entry := range baseEnv {
if strings.HasPrefix(entry, machinediagnostics.RuntimeLockPathEnv+"=") ||
strings.HasPrefix(entry, machinediagnostics.RuntimeLocatorPathEnv+"=") {
continue
}
filteredEnv = append(filteredEnv, entry)
}
if runtimePaths.LockPath == machinediagnostics.DefaultRuntimeLockPath &&
runtimePaths.LocatorPath == machinediagnostics.DefaultLocatorPath {
return filteredEnv
}
return append(filteredEnv,
machinediagnostics.RuntimeLockPathEnv+"="+runtimePaths.LockPath,
machinediagnostics.RuntimeLocatorPathEnv+"="+runtimePaths.LocatorPath,
)
}

func fallbackRuntimePaths(
uid int,
userCacheDir func() (string, error),
) (string, error) {
) (machinediagnostics.RuntimePaths, error) {
if uid == 0 {
return machinediagnostics.DefaultRuntimeLockPath, nil
}
cacheDir, err := userCacheDir()
if err != nil {
return "", fmt.Errorf("get user cache directory for daemon lock: %w", err)
return machinediagnostics.SystemRuntimePaths(), nil
}
return filepath.Join(cacheDir, "devsy", "agent-daemon.lock"), nil
return machinediagnostics.UserRuntimePaths(userCacheDir)
}

func RemoveDaemon() error {
Expand Down
32 changes: 25 additions & 7 deletions pkg/daemon/agent/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/devsy-org/api/pkg/devsy"
"github.com/devsy-org/devsy/pkg/devcontainer/config"
"github.com/devsy-org/devsy/pkg/machinediagnostics"
provider2 "github.com/devsy-org/devsy/pkg/provider"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -28,21 +29,38 @@ func TestBuildDaemonArgs(t *testing.T) {
}, args)
}

func TestFallbackRuntimeLockPath(t *testing.T) {
func TestFallbackRuntimePaths(t *testing.T) {
cacheDir := func() (string, error) { return "/home/devsy/.cache", nil }

rootPath, err := fallbackRuntimeLockPath(0, cacheDir)
rootPaths, err := fallbackRuntimePaths(0, cacheDir)
require.NoError(t, err)
assert.Equal(t, "/run/devsy/agent-daemon.lock", rootPath)
assert.Equal(t, "/run/devsy/agent-daemon.lock", rootPaths.LockPath)
assert.Equal(t, "/run/devsy/agent-daemon.json", rootPaths.LocatorPath)

userPath, err := fallbackRuntimeLockPath(1000, cacheDir)
userPaths, err := fallbackRuntimePaths(1000, cacheDir)
require.NoError(t, err)
assert.Equal(t, "/home/devsy/.cache/devsy/agent-daemon.lock", userPath)
assert.Equal(t, "/home/devsy/.cache/devsy/agent-daemon.lock", userPaths.LockPath)
assert.Equal(t, "/home/devsy/.cache/devsy/agent-daemon.json", userPaths.LocatorPath)

_, err = fallbackRuntimeLockPath(1000, func() (string, error) {
_, err = fallbackRuntimePaths(1000, func() (string, error) {
return "", errors.New("unavailable")
})
require.ErrorContains(t, err, "get user cache directory for daemon lock")
require.ErrorContains(t, err, "get user cache directory for daemon runtime")
}

func TestFallbackDaemonEnvPropagatesBothRuntimePaths(t *testing.T) {
t.Setenv(machinediagnostics.RuntimeLockPathEnv, "/inherited/lock")
t.Setenv(machinediagnostics.RuntimeLocatorPathEnv, "/inherited/locator")
env := fallbackDaemonEnv(machinediagnostics.RuntimePaths{
LockPath: "/cache/devsy/agent-daemon.lock",
LocatorPath: "/cache/devsy/agent-daemon.json",
})
assert.Contains(t, env, "DEVSY_DAEMON_RUNTIME_LOCK_PATH=/cache/devsy/agent-daemon.lock")
assert.Contains(t, env, "DEVSY_DAEMON_LOCATOR_PATH=/cache/devsy/agent-daemon.json")

systemEnv := fallbackDaemonEnv(machinediagnostics.SystemRuntimePaths())
assert.NotContains(t, systemEnv, machinediagnostics.RuntimeLockPathEnv+"=/inherited/lock")
assert.NotContains(t, systemEnv, machinediagnostics.RuntimeLocatorPathEnv+"=/inherited/locator")
}

func TestDaemonUnitStateLocationMatchesExactArgumentValues(t *testing.T) {
Expand Down
136 changes: 134 additions & 2 deletions pkg/machinediagnostics/locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"time"
)

const DefaultLocatorPath = "/run/devsy/agent-daemon.json"
const DefaultLocatorPath = DefaultRuntimeDir + "/" + RuntimeLocatorFileName

type Locator struct {
SchemaVersion int `json:"schemaVersion"`
Expand All @@ -25,7 +25,7 @@ func WriteLocator(
if !filepath.IsAbs(path) || !filepath.IsAbs(locator.DiagnosticsDir) {
return fmt.Errorf("locator paths must be absolute")
}
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
if err := ensureRuntimeDirForPath(path); err != nil {
return err
}
locator.SchemaVersion = SchemaVersion
Expand Down Expand Up @@ -106,3 +106,135 @@ func ReadFromLocator(path string, options ReadOptions) ReadResponse {
}
return Read(locator.DiagnosticsDir, options)
}

// ReadActive reads the highest-priority active daemon locator, falling
// through to the per-user fallback unless the system locator is corrupt.
func ReadActive(options ReadOptions, userCacheDir func() (string, error)) (ReadResponse, error) {
paths, err := ActiveLocatorCandidates(userCacheDir)
if err != nil {
// Surface the discovery failure when no system locator exists.
response := readActiveFromCandidates([]string{DefaultLocatorPath}, options)
if response.Availability != AvailabilityNotInitialized {
return response, nil
}
return ReadResponse{}, err
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return readActiveFromCandidates(paths, options), nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func ActiveLocatorCandidates(userCacheDir func() (string, error)) ([]string, error) {
paths := []string{DefaultLocatorPath}
userPaths, err := UserRuntimePaths(userCacheDir)
if err != nil {
return nil, err
}
if userPaths.LocatorPath != DefaultLocatorPath {
paths = append(paths, userPaths.LocatorPath)
}
return paths, nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func readActiveFromCandidates(paths []string, options ReadOptions) ReadResponse {
var permissionResponse ReadResponse
var staleResponse ReadResponse
for i, path := range paths {
candidate := readLocatorCandidate(path, options)
response, done, updatedPermission := applyLocatorCandidate(
candidate,
i,
permissionResponse,
)
if done {
return response
}
permissionResponse = updatedPermission
if candidate.state == locatorCandidateStale && staleResponse.Availability == "" {
staleResponse = candidate.response
}
}
if permissionResponse.Availability != "" {
return permissionResponse
}
if staleResponse.Availability != "" {
return staleResponse
}
if len(paths) > 0 {
return ReadFromLocator(paths[0], options)
}
return ReadFromLocator(DefaultLocatorPath, options)
}

func applyLocatorCandidate(
candidate locatorCandidate,
index int,
permissionResponse ReadResponse,
) (ReadResponse, bool, ReadResponse) {
switch candidate.state {
case locatorCandidateSuccess:
return candidate.response, true, permissionResponse
case locatorCandidateCorrupt:
if index == 0 || permissionResponse.Availability == "" {
return candidate.response, true, permissionResponse
}
return permissionResponse, true, permissionResponse
case locatorCandidatePermission:
if permissionResponse.Availability == "" {
permissionResponse = candidate.response
}
case locatorCandidateStale:
return ReadResponse{}, false, permissionResponse
}
return ReadResponse{}, false, permissionResponse
}

type locatorCandidateState uint8

const (
locatorCandidateMissing locatorCandidateState = iota
locatorCandidatePermission
locatorCandidateCorrupt
locatorCandidateSuccess
locatorCandidateStale
)

type locatorCandidate struct {
response ReadResponse
state locatorCandidateState
}

func readLocatorCandidate(path string, options ReadOptions) locatorCandidate {
locator, err := ReadLocator(path)
if err != nil {
switch {
case os.IsNotExist(err):
return locatorCandidate{state: locatorCandidateMissing}
case os.IsPermission(err):
return locatorCandidate{
response: ReadFromLocator(path, options),
state: locatorCandidatePermission,
}
default:
return locatorCandidate{
response: ReadFromLocator(path, options),
state: locatorCandidateCorrupt,
}
}
}
return classifyLocatorResponse(Read(locator.DiagnosticsDir, options))
}

func classifyLocatorResponse(response ReadResponse) locatorCandidate {
if response.Availability == AvailabilityAvailable && response.Status != nil {
if response.Status.State == DaemonStopping || response.Freshness == FreshnessStale {
return locatorCandidate{response: response, state: locatorCandidateStale}
}
return locatorCandidate{response: response, state: locatorCandidateSuccess}
}
if response.Availability == AvailabilityNotInitialized {
return locatorCandidate{state: locatorCandidateMissing}
}
if response.Availability == AvailabilityPermissionDenied {
return locatorCandidate{response: response, state: locatorCandidatePermission}
}
return locatorCandidate{response: response, state: locatorCandidateCorrupt}
}
6 changes: 3 additions & 3 deletions pkg/machinediagnostics/runtime_lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package machinediagnostics

import (
"fmt"
"os"
"path/filepath"

"github.com/gofrs/flock"
)

const (
DefaultRuntimeLockPath = "/run/devsy/agent-daemon.lock"
DefaultRuntimeLockPath = DefaultRuntimeDir + "/" + RuntimeLockFileName
RuntimeLockPathEnv = "DEVSY_DAEMON_RUNTIME_LOCK_PATH"
RuntimeLocatorPathEnv = "DEVSY_DAEMON_LOCATOR_PATH"
)

// RuntimeLock prevents two singleton machine daemons from supervising the
Expand All @@ -21,7 +21,7 @@ func AcquireRuntimeLock(path string) (*RuntimeLock, error) {
if !filepath.IsAbs(path) {
return nil, fmt.Errorf("daemon runtime lock path must be absolute")
}
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
if err := ensureRuntimeDirForPath(path); err != nil {
return nil, fmt.Errorf("create daemon runtime directory: %w", err)
}
lock := flock.New(path)
Expand Down
Loading
Loading