mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-23 11:35:25 -04:00
fix(greeter): avoid pinning auto-login session commands
- Store a desktop session identity instead of relying on raw Exec commands - Resolve the current session desktop file when auto-login launches - Preserve legacy memory compatibility while ignoring stale lastSessionExec - Add regression coverage for stale /nix/store session paths - Autologin users should rerun the process
This commit is contained in:
@@ -233,24 +233,39 @@ func stripDesktopExecCodes(execLine string) string {
|
||||
return strings.Join(cleaned, " ")
|
||||
}
|
||||
|
||||
func formatInitialSessionCommand(sessionExec string) string {
|
||||
execLine := strings.TrimSpace(stripDesktopExecCodes(sessionExec))
|
||||
if execLine == "" {
|
||||
func shellQuote(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
func stableDMSCommand() string {
|
||||
for _, candidate := range []string{"/usr/bin/dms", "/usr/local/bin/dms"} {
|
||||
info, err := os.Stat(candidate)
|
||||
if err == nil && !info.IsDir() && info.Mode()&0o111 != 0 {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return "dms"
|
||||
}
|
||||
|
||||
func formatInitialSessionCommand(cacheDir string) string {
|
||||
cacheDir = strings.TrimSpace(cacheDir)
|
||||
if cacheDir == "" {
|
||||
return `command = ""`
|
||||
}
|
||||
escaped := strings.ReplaceAll(execLine, `'`, `'\''`)
|
||||
launcher := fmt.Sprintf("%s greeter launch-session --from-memory --cache-dir %s", stableDMSCommand(), shellQuote(cacheDir))
|
||||
escaped := strings.ReplaceAll(launcher, `'`, `'\''`)
|
||||
inner := fmt.Sprintf("env XDG_SESSION_TYPE=wayland sh -c 'exec %s'", escaped)
|
||||
tomlEscaped := strings.ReplaceAll(inner, `\`, `\\`)
|
||||
tomlEscaped = strings.ReplaceAll(tomlEscaped, `"`, `\"`)
|
||||
return fmt.Sprintf(`command = "%s"`, tomlEscaped)
|
||||
}
|
||||
|
||||
func upsertInitialSession(configContent, loginUser, sessionExec string, enabled bool) string {
|
||||
func upsertInitialSession(configContent, loginUser, cacheDir string, enabled bool) string {
|
||||
if !enabled {
|
||||
return removeTomlSection(configContent, "initial_session")
|
||||
}
|
||||
|
||||
commandLine := formatInitialSessionCommand(sessionExec)
|
||||
commandLine := formatInitialSessionCommand(cacheDir)
|
||||
lines := strings.Split(configContent, "\n")
|
||||
var out []string
|
||||
|
||||
@@ -328,10 +343,11 @@ type greeterAutoLoginConfig struct {
|
||||
}
|
||||
|
||||
type greeterAutoLoginMemory struct {
|
||||
LastSuccessfulUser string `json:"lastSuccessfulUser"`
|
||||
LastSessionID string `json:"lastSessionId"`
|
||||
LastSessionExec string `json:"lastSessionExec"`
|
||||
AutoLoginEnabled bool `json:"autoLoginEnabled"`
|
||||
LastSuccessfulUser string `json:"lastSuccessfulUser"`
|
||||
LastSessionID string `json:"lastSessionId"`
|
||||
LastSessionDesktopID string `json:"lastSessionDesktopId"`
|
||||
LastSessionExec string `json:"lastSessionExec"`
|
||||
AutoLoginEnabled bool `json:"autoLoginEnabled"`
|
||||
}
|
||||
|
||||
func readGreeterAutoLoginConfig(settingsPath string) (greeterAutoLoginConfig, error) {
|
||||
@@ -381,7 +397,7 @@ func execFromDesktopFile(path string) (string, error) {
|
||||
return "", fmt.Errorf("no Exec= line found in %s", path)
|
||||
}
|
||||
|
||||
func resolveGreeterAutoLoginState(cacheDir, homeDir string) (enabled bool, loginUser string, sessionExec string, err error) {
|
||||
func resolveGreeterAutoLoginState(cacheDir, homeDir string) (enabled bool, loginUser string, sessionID string, err error) {
|
||||
settingsPath := filepath.Join(cacheDir, "settings.json")
|
||||
if _, statErr := os.Stat(settingsPath); statErr != nil {
|
||||
settingsPath = filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||
@@ -416,15 +432,9 @@ func resolveGreeterAutoLoginState(cacheDir, homeDir string) (enabled bool, login
|
||||
loginUser = current.Username
|
||||
}
|
||||
|
||||
sessionExec = mem.LastSessionExec
|
||||
if sessionExec == "" && mem.LastSessionID != "" {
|
||||
sessionExec, err = execFromDesktopFile(mem.LastSessionID)
|
||||
if err != nil {
|
||||
sessionExec = ""
|
||||
}
|
||||
}
|
||||
sessionID = sessionDesktopIDFromMemory(mem)
|
||||
|
||||
return true, loginUser, sessionExec, nil
|
||||
return true, loginUser, sessionID, nil
|
||||
}
|
||||
|
||||
func writeGreetdConfig(configPath, content string, logFunc func(string), sudoPassword, successMsg string) error {
|
||||
@@ -540,7 +550,7 @@ func readGreeterMemoryFile(memoryPath, sudoPassword string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func SyncGreetdAutoLogin(cacheDir, homeDir string, logFunc func(string), sudoPassword string) error {
|
||||
enabled, loginUser, sessionExec, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
||||
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -568,7 +578,7 @@ func SyncGreetdAutoLogin(cacheDir, homeDir string, logFunc func(string), sudoPas
|
||||
return writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, "✓ Disabled greeter auto-login")
|
||||
}
|
||||
|
||||
if loginUser == "" || sessionExec == "" {
|
||||
if loginUser == "" || sessionID == "" {
|
||||
if logFunc != nil {
|
||||
logFunc("⚠ Greeter auto-login is enabled but user or session is not configured yet. Log in manually once, then run sync.")
|
||||
}
|
||||
@@ -579,7 +589,7 @@ func SyncGreetdAutoLogin(cacheDir, homeDir string, logFunc func(string), sudoPas
|
||||
return nil
|
||||
}
|
||||
|
||||
newConfig := upsertInitialSession(configContent, loginUser, sessionExec, true)
|
||||
newConfig := upsertInitialSession(configContent, loginUser, cacheDir, true)
|
||||
if newConfig == configContent {
|
||||
if logFunc != nil {
|
||||
logFunc(fmt.Sprintf("✓ Greeter auto-login already configured for %s", loginUser))
|
||||
|
||||
@@ -111,15 +111,18 @@ command = "/usr/bin/dms-greeter --command niri"
|
||||
|
||||
t.Run("inserts initial session", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := upsertInitialSession(baseConfig, "alice", "niri", true)
|
||||
got := upsertInitialSession(baseConfig, "alice", "/var/cache/dms-greeter", true)
|
||||
if !strings.Contains(got, "[initial_session]") {
|
||||
t.Fatalf("expected [initial_session] section, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, `user = "alice"`) {
|
||||
t.Fatalf("expected alice user in initial session, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, `env XDG_SESSION_TYPE=wayland sh -c 'exec niri'`) {
|
||||
t.Fatalf("expected wrapped session command, got:\n%s", got)
|
||||
if !strings.Contains(got, `dms greeter launch-session --from-memory --cache-dir`) {
|
||||
t.Fatalf("expected stable launch-session command, got:\n%s", got)
|
||||
}
|
||||
if strings.Contains(got, `exec niri`) {
|
||||
t.Fatalf("initial session must not bake the desktop Exec command, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -130,12 +133,12 @@ command = "/usr/bin/dms-greeter --command niri"
|
||||
user = "bob"
|
||||
command = "old-command"
|
||||
`
|
||||
got := upsertInitialSession(existing, "alice", "Hyprland", true)
|
||||
got := upsertInitialSession(existing, "alice", "/var/cache/dms-greeter", true)
|
||||
if strings.Contains(got, `user = "bob"`) {
|
||||
t.Fatalf("expected bob to be replaced, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, `exec Hyprland`) {
|
||||
t.Fatalf("expected Hyprland command, got:\n%s", got)
|
||||
if !strings.Contains(got, `dms greeter launch-session --from-memory`) {
|
||||
t.Fatalf("expected launch-session command, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -179,15 +182,46 @@ func TestResolveGreeterAutoLoginState(t *testing.T) {
|
||||
}`)
|
||||
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
|
||||
"lastSuccessfulUser": "alice",
|
||||
"lastSessionExec": "niri"
|
||||
"lastSessionDesktopId": "niri.desktop"
|
||||
}`)
|
||||
|
||||
enabled, loginUser, sessionExec, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
||||
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
|
||||
}
|
||||
if !enabled || loginUser != "alice" || sessionExec != "niri" {
|
||||
t.Fatalf("got enabled=%v user=%q exec=%q", enabled, loginUser, sessionExec)
|
||||
if !enabled || loginUser != "alice" || sessionID != "niri.desktop" {
|
||||
t.Fatalf("got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveGreeterAutoLoginStateIgnoresStaleSessionExec(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cacheDir := t.TempDir()
|
||||
homeDir := t.TempDir()
|
||||
|
||||
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
|
||||
"greeterAutoLogin": true,
|
||||
"greeterRememberLastUser": true,
|
||||
"greeterRememberLastSession": true
|
||||
}`)
|
||||
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
|
||||
"lastSuccessfulUser": "alice",
|
||||
"lastSessionId": "/nix/store/old-session/share/wayland-sessions/example.desktop",
|
||||
"lastSessionExec": "/nix/store/old-session/bin/start-example-session"
|
||||
}`)
|
||||
|
||||
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
|
||||
}
|
||||
if !enabled || loginUser != "alice" || sessionID != "example.desktop" {
|
||||
t.Fatalf("got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
|
||||
}
|
||||
|
||||
got := upsertInitialSession("", loginUser, cacheDir, true)
|
||||
if strings.Contains(got, "/nix/store/old-session") {
|
||||
t.Fatalf("initial session must not include stale store path, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,12 +242,35 @@ func TestResolveGreeterAutoLoginStateIgnoresMemoryFlag(t *testing.T) {
|
||||
"lastSessionExec": "niri"
|
||||
}`)
|
||||
|
||||
enabled, loginUser, sessionExec, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
||||
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
|
||||
}
|
||||
if enabled || loginUser != "" || sessionExec != "" {
|
||||
t.Fatalf("expected disabled with empty user/exec, got enabled=%v user=%q exec=%q", enabled, loginUser, sessionExec)
|
||||
if enabled || loginUser != "" || sessionID != "" {
|
||||
t.Fatalf("expected disabled with empty user/session, got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSessionExecInDirs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
oldDir := filepath.Join(t.TempDir(), "wayland-sessions")
|
||||
newDir := filepath.Join(t.TempDir(), "wayland-sessions")
|
||||
writeTestFile(t, filepath.Join(oldDir, "example.desktop"), `[Desktop Entry]
|
||||
Name=Example Session
|
||||
Exec=/nix/store/old-session/bin/start-example-session
|
||||
`)
|
||||
writeTestFile(t, filepath.Join(newDir, "example.desktop"), `[Desktop Entry]
|
||||
Name=Example Session
|
||||
Exec=/run/current-system/sw/bin/start-example-session
|
||||
`)
|
||||
|
||||
got, err := resolveSessionExecInDirs("example.desktop", []string{newDir, oldDir})
|
||||
if err != nil {
|
||||
t.Fatalf("resolveSessionExecInDirs returned error: %v", err)
|
||||
}
|
||||
if got != "/run/current-system/sw/bin/start-example-session" {
|
||||
t.Fatalf("resolveSessionExecInDirs = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package greeter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func sessionDesktopIDFromPath(path string) string {
|
||||
id := strings.TrimSpace(path)
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.ContainsAny(id, "/\\") {
|
||||
id = filepath.Base(id)
|
||||
}
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasSuffix(id, ".desktop") {
|
||||
id += ".desktop"
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func sessionDesktopIDFromMemory(mem greeterAutoLoginMemory) string {
|
||||
if id := sessionDesktopIDFromPath(mem.LastSessionDesktopID); id != "" {
|
||||
return id
|
||||
}
|
||||
return sessionDesktopIDFromPath(mem.LastSessionID)
|
||||
}
|
||||
|
||||
func sessionDesktopDirs() []string {
|
||||
seen := make(map[string]bool)
|
||||
dirs := make([]string, 0, 8)
|
||||
|
||||
addBase := func(base string) {
|
||||
base = strings.TrimSpace(base)
|
||||
if base == "" {
|
||||
return
|
||||
}
|
||||
for _, sub := range []string{"wayland-sessions", "xsessions"} {
|
||||
dir := filepath.Join(base, sub)
|
||||
if seen[dir] {
|
||||
continue
|
||||
}
|
||||
seen[dir] = true
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
|
||||
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
|
||||
addBase(dataHome)
|
||||
} else if home, err := os.UserHomeDir(); err == nil && home != "" {
|
||||
addBase(filepath.Join(home, ".local", "share"))
|
||||
}
|
||||
|
||||
if dataDirs := os.Getenv("XDG_DATA_DIRS"); dataDirs != "" {
|
||||
for _, dir := range strings.Split(dataDirs, ":") {
|
||||
addBase(dir)
|
||||
}
|
||||
} else {
|
||||
addBase("/usr/local/share")
|
||||
addBase("/usr/share")
|
||||
}
|
||||
|
||||
return dirs
|
||||
}
|
||||
|
||||
func ResolveSessionExec(sessionID string) (string, error) {
|
||||
return resolveSessionExecInDirs(sessionID, sessionDesktopDirs())
|
||||
}
|
||||
|
||||
func resolveSessionExecInDirs(sessionID string, dirs []string) (string, error) {
|
||||
id := sessionDesktopIDFromPath(sessionID)
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("session id is empty")
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
path := filepath.Join(dir, id)
|
||||
execLine, err := execFromDesktopFile(path)
|
||||
if err == nil {
|
||||
return execLine, nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("session desktop file %q was not found", id)
|
||||
}
|
||||
|
||||
func LaunchSessionByID(sessionID string) error {
|
||||
execLine, err := ResolveSessionExec(sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
execLine = strings.TrimSpace(stripDesktopExecCodes(execLine))
|
||||
if execLine == "" {
|
||||
return fmt.Errorf("session %q has an empty Exec command", sessionID)
|
||||
}
|
||||
|
||||
env := append(os.Environ(), "XDG_SESSION_TYPE=wayland")
|
||||
return syscall.Exec("/bin/sh", []string{"sh", "-c", "exec " + execLine}, env)
|
||||
}
|
||||
|
||||
func LaunchSessionFromMemory(cacheDir, homeDir string) error {
|
||||
enabled, _, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !enabled {
|
||||
return fmt.Errorf("greeter auto-login is disabled")
|
||||
}
|
||||
if sessionID == "" {
|
||||
return fmt.Errorf("greeter auto-login has no remembered session")
|
||||
}
|
||||
return LaunchSessionByID(sessionID)
|
||||
}
|
||||
Reference in New Issue
Block a user