Files
ctrld/discover_user_darwin.go
Codescribe 023969ff6d feat: robust username detection and CI updates
Add platform-specific username detection for Control D metadata:
- macOS: directory services (dscl) with console user fallback
- Linux: systemd loginctl, utmp, /etc/passwd traversal
- Windows: WTS session enumeration, registry, token lookup
2026-03-10 17:18:25 +07:00

135 lines
3.6 KiB
Go

//go:build darwin
package ctrld
import (
"context"
"os/exec"
"strconv"
"strings"
)
// DiscoverMainUser attempts to find the primary user on macOS systems.
// This is designed to work reliably under RMM deployments where traditional
// environment variables and session detection may not be available.
//
// Priority chain (deterministic, lowest UID wins among candidates):
// 1. Console user from stat -f %Su /dev/console
// 2. Active console session user via scutil
// 3. First user with UID >= 501 from dscl (standard macOS user range)
func DiscoverMainUser(ctx context.Context) string {
logger := LoggerFromCtx(ctx).Debug()
// Method 1: Check console owner via stat
logger.Msg("attempting to discover user via console stat")
if user := getConsoleUser(ctx); user != "" && user != "root" {
logger.Str("method", "stat").Str("user", user).Msg("found user via console stat")
return user
}
// Method 2: Check active console session via scutil
logger.Msg("attempting to discover user via scutil ConsoleUser")
if user := getScutilConsoleUser(ctx); user != "" && user != "root" {
logger.Str("method", "scutil").Str("user", user).Msg("found user via scutil ConsoleUser")
return user
}
// Method 3: Find lowest UID >= 501 from directory services
logger.Msg("attempting to discover user via dscl directory scan")
if user := getLowestRegularUser(ctx); user != "" {
logger.Str("method", "dscl").Str("user", user).Msg("found user via dscl scan")
return user
}
logger.Msg("all user discovery methods failed")
return "unknown"
}
// getConsoleUser uses stat to find the owner of /dev/console
func getConsoleUser(ctx context.Context) string {
cmd := exec.CommandContext(ctx, "stat", "-f", "%Su", "/dev/console")
out, err := cmd.Output()
if err != nil {
LoggerFromCtx(ctx).Debug().Err(err).Msg("failed to stat /dev/console")
return ""
}
return strings.TrimSpace(string(out))
}
// getScutilConsoleUser uses scutil to get the current console user
func getScutilConsoleUser(ctx context.Context) string {
cmd := exec.CommandContext(ctx, "scutil", "-r", "ConsoleUser")
out, err := cmd.Output()
if err != nil {
LoggerFromCtx(ctx).Debug().Err(err).Msg("failed to get ConsoleUser via scutil")
return ""
}
lines := strings.Split(string(out), "\n")
for _, line := range lines {
if strings.Contains(line, "Name :") {
parts := strings.Fields(line)
if len(parts) >= 3 {
return strings.TrimSpace(parts[2])
}
}
}
return ""
}
// getLowestRegularUser finds the user with the lowest UID >= 501
func getLowestRegularUser(ctx context.Context) string {
// Get list of all users with UID >= 501
cmd := exec.CommandContext(ctx, "dscl", ".", "list", "/Users", "UniqueID")
out, err := cmd.Output()
if err != nil {
LoggerFromCtx(ctx).Debug().Err(err).Msg("failed to list users via dscl")
return ""
}
var candidates []struct {
name string
uid int
}
lines := strings.Split(string(out), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) != 2 {
continue
}
username := fields[0]
uidStr := fields[1]
uid, err := strconv.Atoi(uidStr)
if err != nil {
continue
}
// Only consider regular users (UID >= 501 on macOS)
if uid >= 501 {
candidates = append(candidates, struct {
name string
uid int
}{username, uid})
}
}
if len(candidates) == 0 {
return ""
}
// Find the candidate with the lowest UID (deterministic choice)
lowestUID := candidates[0].uid
result := candidates[0].name
for _, candidate := range candidates[1:] {
if candidate.uid < lowestUID {
lowestUID = candidate.uid
result = candidate.name
}
}
return result
}