mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
fix(Greeter): Multi-distro reliability updates
- Merge duplicate niri input/output KDL nodes instead of appending. Allows more overrides - Guard AppArmor install/uninstall behind IsAppArmorEnabled() check
This commit is contained in:
@@ -227,9 +227,11 @@ func installGreeter(nonInteractive bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nConfiguring AppArmor profile...")
|
if greeter.IsAppArmorEnabled() {
|
||||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
fmt.Println("\nConfiguring AppArmor profile...")
|
||||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||||
|
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nConfiguring greetd...")
|
fmt.Println("\nConfiguring greetd...")
|
||||||
@@ -575,12 +577,13 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
||||||
|
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||||
|
}
|
||||||
|
|
||||||
cacheDir := greeter.GreeterCacheDir
|
cacheDir := greeter.GreeterCacheDir
|
||||||
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||||
logFunc("Cache directory not found — attempting to create it...")
|
logFunc("Cache directory not found — attempting to create it...")
|
||||||
if createErr := greeter.EnsureGreeterCacheDir(logFunc, ""); createErr != nil {
|
|
||||||
return fmt.Errorf("greeter cache directory not found at %s and could not be created: %w\nRun: sudo mkdir -p %s && sudo chown greeter:greeter %s", cacheDir, createErr, cacheDir, cacheDir)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
greeterGroup := greeter.DetectGreeterGroup()
|
greeterGroup := greeter.DetectGreeterGroup()
|
||||||
@@ -600,27 +603,28 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||||
if !inGreeterGroup {
|
if !inGreeterGroup {
|
||||||
if nonInteractive {
|
if nonInteractive {
|
||||||
return fmt.Errorf("user must be in the %s group; run 'dms greeter sync' from a terminal to add", greeterGroup)
|
logFunc(fmt.Sprintf("⚠ Not yet in %s group — will be added during sync (logout/login required to take effect).", greeterGroup))
|
||||||
}
|
|
||||||
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
|
|
||||||
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
|
|
||||||
|
|
||||||
var response string
|
|
||||||
fmt.Scanln(&response)
|
|
||||||
response = strings.ToLower(strings.TrimSpace(response))
|
|
||||||
|
|
||||||
if response != "n" && response != "no" {
|
|
||||||
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
|
||||||
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
|
|
||||||
addUserCmd.Stdout = os.Stdout
|
|
||||||
addUserCmd.Stderr = os.Stderr
|
|
||||||
if err := addUserCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
|
||||||
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
|
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("aborted: user must be in the greeter group before syncing")
|
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
|
||||||
|
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
|
||||||
|
if response != "n" && response != "no" {
|
||||||
|
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
||||||
|
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
|
||||||
|
addUserCmd.Stdout = os.Stdout
|
||||||
|
addUserCmd.Stderr = os.Stderr
|
||||||
|
if err := addUserCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
||||||
|
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("aborted: user must be in the greeter group before syncing")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -694,18 +698,25 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSetting up permissions and ACLs...")
|
fmt.Println("\nSetting up permissions and ACLs...")
|
||||||
|
greeter.RemediateStaleACLs(logFunc, "")
|
||||||
|
greeter.RemediateStaleAppArmor(logFunc, "")
|
||||||
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure greeter cache directory at %s: %w\nRun: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s", cacheDir, err, cacheDir, greeterGroup, cacheDir, cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nSynchronizing DMS configurations...")
|
fmt.Println("\nSynchronizing DMS configurations...")
|
||||||
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, "", forceAuth); err != nil {
|
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, "", forceAuth); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nConfiguring AppArmor profile...")
|
if greeter.IsAppArmorEnabled() {
|
||||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
fmt.Println("\nConfiguring AppArmor profile...")
|
||||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||||
|
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\n=== Sync Complete ===")
|
fmt.Println("\n=== Sync Complete ===")
|
||||||
@@ -1021,6 +1032,7 @@ func enableGreeter(nonInteractive bool) error {
|
|||||||
logFunc := func(msg string) {
|
logFunc := func(msg string) {
|
||||||
fmt.Println(msg)
|
fmt.Println(msg)
|
||||||
}
|
}
|
||||||
|
greeterGroup := greeter.DetectGreeterGroup()
|
||||||
|
|
||||||
if configAlreadyCorrect {
|
if configAlreadyCorrect {
|
||||||
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
||||||
@@ -1028,8 +1040,12 @@ func enableGreeter(nonInteractive bool) error {
|
|||||||
fmt.Printf("✓ Configured compositor: %s\n", configuredCompositor)
|
fmt.Printf("✓ Configured compositor: %s\n", configuredCompositor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nSetting up dms-greeter group and permissions...")
|
||||||
|
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||||
fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
fmt.Printf("⚠ Could not ensure cache directory: %v\n Run: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s\n", err, greeter.GreeterCacheDir, greeterGroup, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ensureGraphicalTarget(); err != nil {
|
if err := ensureGraphicalTarget(); err != nil {
|
||||||
@@ -1100,12 +1116,18 @@ func enableGreeter(nonInteractive bool) error {
|
|||||||
return fmt.Errorf("failed to configure greetd: %w", err)
|
return fmt.Errorf("failed to configure greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nSetting up dms-greeter group and permissions...")
|
||||||
|
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||||
fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
fmt.Printf("⚠ Could not ensure cache directory: %v\n Run: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s\n", err, greeter.GreeterCacheDir, greeterGroup, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
if greeter.IsAppArmorEnabled() {
|
||||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||||
|
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ensureGraphicalTarget(); err != nil {
|
if err := ensureGraphicalTarget(); err != nil {
|
||||||
@@ -1540,30 +1562,33 @@ func checkGreeterStatus() error {
|
|||||||
fmt.Println(" - security key (U2F): disabled")
|
fmt.Println(" - security key (U2F): disabled")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ℹ No managed auth block present (fingerprint/U2F disabled for greeter)")
|
fmt.Println(" ℹ No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)")
|
||||||
}
|
}
|
||||||
if legacyManaged {
|
if legacyManaged {
|
||||||
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.")
|
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.")
|
||||||
allGood = false
|
allGood = false
|
||||||
}
|
}
|
||||||
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
||||||
|
showIncludedFprintNotice := false
|
||||||
|
if includedFprintFile != "" {
|
||||||
|
if enableFprint, _, settingsErr := greeter.ReadGreeterAuthToggles(homeDir); settingsErr == nil && enableFprint {
|
||||||
|
showIncludedFprintNotice = greeter.FingerprintAuthAvailableForCurrentUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
if managedFprint {
|
if managedFprint {
|
||||||
if includedFprintFile != "" {
|
if includedFprintFile != "" {
|
||||||
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile)
|
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile)
|
||||||
fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.")
|
fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.")
|
||||||
allGood = false
|
allGood = false
|
||||||
}
|
}
|
||||||
} else if includedFprintFile != "" {
|
} else if includedFprintFile != "" && showIncludedFprintNotice {
|
||||||
fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile)
|
fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile)
|
||||||
fmt.Println(" The DMS toggle only controls the managed block; disable fingerprint in authselect/pam-auth-update for password-only greeter login.")
|
fmt.Println(" The DMS toggle only controls the managed block; disable fingerprint in authselect/pam-auth-update for password-only greeter login.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSecurity (AppArmor):")
|
fmt.Println("\nSecurity (AppArmor):")
|
||||||
appArmorEnabled, appArmorErr := isAppArmorEnabled()
|
if !greeter.IsAppArmorEnabled() {
|
||||||
if appArmorErr != nil {
|
|
||||||
fmt.Printf(" ℹ Could not determine AppArmor status: %v\n", appArmorErr)
|
|
||||||
} else if !appArmorEnabled {
|
|
||||||
fmt.Println(" ℹ AppArmor not enabled")
|
fmt.Println(" ℹ AppArmor not enabled")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ℹ AppArmor is enabled")
|
fmt.Println(" ℹ AppArmor is enabled")
|
||||||
@@ -1612,18 +1637,6 @@ func checkGreeterStatus() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAppArmorEnabled() (bool, error) {
|
|
||||||
data, err := os.ReadFile("/sys/module/apparmor/parameters/enabled")
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
value := strings.TrimSpace(strings.ToLower(string(data)))
|
|
||||||
return strings.HasPrefix(value, "y"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) {
|
func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) {
|
||||||
if sampleLimit <= 0 {
|
if sampleLimit <= 0 {
|
||||||
sampleLimit = 3
|
sampleLimit = 3
|
||||||
@@ -1712,8 +1725,7 @@ func isGreeterRelatedAppArmorDenial(line string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// appArmorProfileMode returns "complain", "enforce", or "" (unknown) for a named AppArmor
|
// appArmorProfileMode returns "complain", "enforce", or "" for a named AppArmor profile.
|
||||||
// profile by reading /sys/kernel/security/apparmor/profiles.
|
|
||||||
func appArmorProfileMode(profileName string) string {
|
func appArmorProfileMode(profileName string) string {
|
||||||
data, err := os.ReadFile("/sys/kernel/security/apparmor/profiles")
|
data, err := os.ReadFile("/sys/kernel/security/apparmor/profiles")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ func DetectGreeterUser() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if user, found := findPasswdUser(passwdContent, "greeter", "_greeter", "greetd"); found {
|
if user, found := findPasswdUser(passwdContent, "greeter", "greetd", "_greeter"); found {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -230,6 +230,16 @@ func resolveGreeterWrapperPath() string {
|
|||||||
return override
|
return override
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Packaged installs only use the official wrapper; never fall back to /usr/local/bin.
|
||||||
|
if IsGreeterPackaged() {
|
||||||
|
packagedWrapper := "/usr/bin/dms-greeter"
|
||||||
|
if info, err := os.Stat(packagedWrapper); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 {
|
||||||
|
return packagedWrapper
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "⚠ Warning: packaged dms-greeter detected, but /usr/bin/dms-greeter is missing or not executable")
|
||||||
|
return packagedWrapper
|
||||||
|
}
|
||||||
|
|
||||||
for _, candidate := range []string{"/usr/bin/dms-greeter", "/usr/local/bin/dms-greeter"} {
|
for _, candidate := range []string{"/usr/bin/dms-greeter", "/usr/local/bin/dms-greeter"} {
|
||||||
if info, err := os.Stat(candidate); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 {
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 {
|
||||||
return candidate
|
return candidate
|
||||||
@@ -558,54 +568,124 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EnsureGreeterCacheDir creates /var/cache/dms-greeter with correct ownership if it does not exist.
|
// EnsureGreeterCacheDir creates /var/cache/dms-greeter with correct ownership if it does not exist.
|
||||||
// It is safe to call multiple times (idempotent).
|
// It is safe to call multiple times (idempotent) and will repair ownership/mode
|
||||||
|
// when the directory already exists with stale permissions.
|
||||||
func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
||||||
cacheDir := GreeterCacheDir
|
cacheDir := GreeterCacheDir
|
||||||
if _, err := os.Stat(cacheDir); err == nil {
|
created := false
|
||||||
return nil
|
if info, err := os.Stat(cacheDir); err != nil {
|
||||||
}
|
if !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to stat cache directory: %w", err)
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
}
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
created = true
|
||||||
|
} else if !info.IsDir() {
|
||||||
|
return fmt.Errorf("cache path exists but is not a directory: %s", cacheDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
group := DetectGreeterGroup()
|
group := DetectGreeterGroup()
|
||||||
owner := fmt.Sprintf("%s:%s", group, group)
|
daemonUser := DetectGreeterUser()
|
||||||
|
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
|
||||||
|
owner := preferredOwner
|
||||||
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to set cache directory owner: %w", err)
|
// Some setups may not have a matching daemon user at this moment; fall back
|
||||||
|
// to root:<group> while still allowing group-writable greeter runtime access.
|
||||||
|
fallbackOwner := fmt.Sprintf("root:%s", group)
|
||||||
|
if fallbackErr := runSudoCmd(sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
||||||
|
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
|
||||||
|
}
|
||||||
|
owner = fallbackOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "750", cacheDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, mode: 750)", cacheDir, owner))
|
|
||||||
|
runtimeDirs := []string{
|
||||||
|
filepath.Join(cacheDir, ".local"),
|
||||||
|
filepath.Join(cacheDir, ".local", "state"),
|
||||||
|
filepath.Join(cacheDir, ".local", "share"),
|
||||||
|
filepath.Join(cacheDir, ".cache"),
|
||||||
|
}
|
||||||
|
for _, dir := range runtimeDirs {
|
||||||
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", dir); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
if err := runSudoCmd(sudoPassword, "chown", owner, dir); err != nil {
|
||||||
|
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
if err := runSudoCmd(sudoPassword, "chmod", "2770", dir); err != nil {
|
||||||
|
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyMemoryPath := filepath.Join(cacheDir, "memory.json")
|
||||||
|
stateMemoryPath := filepath.Join(cacheDir, ".local", "state", "memory.json")
|
||||||
|
if err := ensureGreeterMemoryCompatLink(logFunc, sudoPassword, legacyMemoryPath, stateMemoryPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
|
||||||
|
if err := runSudoCmd(sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
||||||
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if created {
|
||||||
|
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, mode: 2770)", cacheDir, owner))
|
||||||
|
} else {
|
||||||
|
logFunc(fmt.Sprintf("✓ Ensured cache directory %s permissions (owner: %s, mode: 2770)", cacheDir, owner))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallAppArmorProfile writes the bundled AppArmor profile for dms-greeter and reloads
|
func isSELinuxEnforcing() bool {
|
||||||
// it with apparmor_parser. It is safe to call multiple times (idempotent reload).
|
data, err := os.ReadFile("/sys/fs/selinux/enforce")
|
||||||
//
|
if err != nil {
|
||||||
// Skipped silently when:
|
return false
|
||||||
// - AppArmor kernel module is absent (/sys/module/apparmor does not exist)
|
}
|
||||||
// - Running on NixOS (profiles are managed via security.apparmor.policies)
|
return strings.TrimSpace(string(data)) == "1"
|
||||||
// - SELinux is active (/sys/fs/selinux/enforce exists and equals "1") — Fedora/RHEL
|
}
|
||||||
|
|
||||||
|
func ensureGreeterMemoryCompatLink(logFunc func(string), sudoPassword, legacyPath, statePath string) error {
|
||||||
|
info, err := os.Lstat(legacyPath)
|
||||||
|
if err == nil && info.Mode().IsRegular() {
|
||||||
|
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
|
||||||
|
if copyErr := runSudoCmd(sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
||||||
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runSudoCmd(sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAppArmorEnabled reports whether AppArmor is active on the running kernel.
|
||||||
|
func IsAppArmorEnabled() bool {
|
||||||
|
data, err := os.ReadFile("/sys/module/apparmor/parameters/enabled")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(strings.TrimSpace(strings.ToLower(string(data))), "y")
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallAppArmorProfile installs the bundled AppArmor profile and reloads it. No-op on NixOS or non-AppArmor systems.
|
||||||
func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
||||||
if IsNixOS() {
|
if IsNixOS() {
|
||||||
logFunc(" ℹ Skipping AppArmor profile on NixOS (manage via security.apparmor.policies)")
|
logFunc(" ℹ Skipping AppArmor profile on NixOS (manage via security.apparmor.policies)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat("/sys/module/apparmor"); os.IsNotExist(err) {
|
if !IsAppArmorEnabled() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if data, err := os.ReadFile("/sys/fs/selinux/enforce"); err == nil {
|
|
||||||
if strings.TrimSpace(string(data)) == "1" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
||||||
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
||||||
}
|
}
|
||||||
@@ -814,7 +894,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
|
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
|
||||||
}
|
}
|
||||||
|
|
||||||
owner := DetectGreeterGroup()
|
group := DetectGreeterGroup()
|
||||||
|
|
||||||
logFunc("\nSetting up parent directory ACLs for greeter user access...")
|
logFunc("\nSetting up parent directory ACLs for greeter user access...")
|
||||||
|
|
||||||
@@ -826,9 +906,10 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil {
|
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
|
||||||
|
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
||||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:%s:x %s", owner, dir.path))
|
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,6 +919,73 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemediateStaleACLs removes user-based ACLs left by older binary versions. Best-effort.
|
||||||
|
func RemediateStaleACLs(logFunc func(string), sudoPassword string) {
|
||||||
|
if !utils.CommandExists("setfacl") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
passwdData, err := os.ReadFile("/etc/passwd")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs := []string{
|
||||||
|
homeDir,
|
||||||
|
filepath.Join(homeDir, ".config"),
|
||||||
|
filepath.Join(homeDir, ".config", "DankMaterialShell"),
|
||||||
|
filepath.Join(homeDir, ".cache"),
|
||||||
|
filepath.Join(homeDir, ".cache", "DankMaterialShell"),
|
||||||
|
filepath.Join(homeDir, ".local"),
|
||||||
|
filepath.Join(homeDir, ".local", "state"),
|
||||||
|
filepath.Join(homeDir, ".local", "share"),
|
||||||
|
}
|
||||||
|
|
||||||
|
passwdContent := string(passwdData)
|
||||||
|
staleUsers := []string{"greeter", "greetd", "_greeter"}
|
||||||
|
existingUsers := make([]string, 0, len(staleUsers))
|
||||||
|
for _, user := range staleUsers {
|
||||||
|
if hasPasswdUser(passwdContent, user) {
|
||||||
|
existingUsers = append(existingUsers, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(existingUsers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaned := false
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if _, err := os.Stat(dir); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, user := range existingUsers {
|
||||||
|
_ = runSudoCmd(sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
||||||
|
cleaned = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cleaned {
|
||||||
|
logFunc("✓ Cleaned up stale user-based ACLs from previous versions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemediateStaleAppArmor removes any AppArmor profile installed by an older binary on
|
||||||
|
// systems where AppArmor is not active.
|
||||||
|
func RemediateStaleAppArmor(logFunc func(string), sudoPassword string) {
|
||||||
|
if IsAppArmorEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(appArmorProfileDest); os.IsNotExist(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logFunc("ℹ Removing stale AppArmor profile (AppArmor is not active on this system)")
|
||||||
|
_ = UninstallAppArmorProfile(logFunc, sudoPassword)
|
||||||
|
}
|
||||||
|
|
||||||
func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -854,6 +1002,14 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
group := DetectGreeterGroup()
|
group := DetectGreeterGroup()
|
||||||
|
|
||||||
|
// Create the group if it doesn't exist yet (e.g. before greetd package is installed).
|
||||||
|
if !utils.HasGroup(group) {
|
||||||
|
if err := runSudoCmd(sudoPassword, "groupadd", "-r", group); err != nil {
|
||||||
|
return fmt.Errorf("failed to create %s group: %w", group, err)
|
||||||
|
}
|
||||||
|
logFunc(fmt.Sprintf("✓ Created system group %s", group))
|
||||||
|
}
|
||||||
|
|
||||||
groupsCmd := exec.Command("groups", currentUser)
|
groupsCmd := exec.Command("groups", currentUser)
|
||||||
groupsOutput, err := groupsCmd.Output()
|
groupsOutput, err := groupsCmd.Output()
|
||||||
if err == nil && strings.Contains(string(groupsOutput), group) {
|
if err == nil && strings.Contains(string(groupsOutput), group) {
|
||||||
@@ -865,6 +1021,24 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also add the daemon user (e.g. greetd on Fedora) so group ACLs apply to the running process.
|
||||||
|
daemonUser := DetectGreeterUser()
|
||||||
|
if daemonUser != currentUser {
|
||||||
|
daemonGroupsCmd := exec.Command("groups", daemonUser)
|
||||||
|
daemonGroupsOutput, daemonGroupsErr := daemonGroupsCmd.Output()
|
||||||
|
if daemonGroupsErr == nil {
|
||||||
|
if strings.Contains(string(daemonGroupsOutput), group) {
|
||||||
|
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
|
||||||
|
} else {
|
||||||
|
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
||||||
|
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
|
||||||
|
} else {
|
||||||
|
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
configDirs := []struct {
|
configDirs := []struct {
|
||||||
path string
|
path string
|
||||||
desc string
|
desc string
|
||||||
@@ -1018,8 +1192,11 @@ func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string)
|
|||||||
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
greeterGroup := DetectGreeterGroup()
|
greeterGroup := DetectGreeterGroup()
|
||||||
if err := runSudoCmd(sudoPassword, "chown", "greeter:"+greeterGroup, destPath); err != nil {
|
daemonUser := DetectGreeterUser()
|
||||||
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
if err := runSudoCmd(sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
||||||
|
if fallbackErr := runSudoCmd(sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
||||||
|
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
||||||
@@ -1158,28 +1335,107 @@ func DetectIncludedPamModule(pamText, module string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type greeterAuthSettings struct {
|
||||||
|
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
||||||
|
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGreeterAuthSettings(homeDir string) (greeterAuthSettings, error) {
|
||||||
|
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||||
|
data, err := os.ReadFile(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return greeterAuthSettings{}, nil
|
||||||
|
}
|
||||||
|
return greeterAuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(data)) == "" {
|
||||||
|
return greeterAuthSettings{}, nil
|
||||||
|
}
|
||||||
|
var settings greeterAuthSettings
|
||||||
|
if err := json.Unmarshal(data, &settings); err != nil {
|
||||||
|
return greeterAuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||||
|
}
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
|
||||||
|
settings, err := readGreeterAuthSettings(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasEnrolledFingerprintOutput(output string) bool {
|
||||||
|
lower := strings.ToLower(output)
|
||||||
|
if strings.Contains(lower, "no fingers enrolled") ||
|
||||||
|
strings.Contains(lower, "no fingerprints enrolled") ||
|
||||||
|
strings.Contains(lower, "no prints enrolled") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.Contains(lower, "has fingers enrolled") ||
|
||||||
|
strings.Contains(lower, "has fingerprints enrolled") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(lower, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "finger:") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func FingerprintAuthAvailableForUser(username string) bool {
|
||||||
|
username = strings.TrimSpace(username)
|
||||||
|
if username == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !pamModuleExists("pam_fprintd.so") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("fprintd-list"); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasEnrolledFingerprintOutput(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FingerprintAuthAvailableForCurrentUser() bool {
|
||||||
|
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
|
||||||
|
if username == "" {
|
||||||
|
username = strings.TrimSpace(os.Getenv("USER"))
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
out, err := exec.Command("id", "-un").Output()
|
||||||
|
if err == nil {
|
||||||
|
username = strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FingerprintAuthAvailableForUser(username)
|
||||||
|
}
|
||||||
|
|
||||||
func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error {
|
func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error {
|
||||||
var wantFprint, wantU2f bool
|
var wantFprint, wantU2f bool
|
||||||
|
fprintToggleEnabled := forceAuth
|
||||||
if forceAuth {
|
if forceAuth {
|
||||||
wantFprint = pamModuleExists("pam_fprintd.so")
|
wantFprint = pamModuleExists("pam_fprintd.so")
|
||||||
wantU2f = pamModuleExists("pam_u2f.so")
|
wantU2f = pamModuleExists("pam_u2f.so")
|
||||||
} else {
|
} else {
|
||||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
settings, err := readGreeterAuthSettings(homeDir)
|
||||||
data, err := os.ReadFile(settingsPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
return err
|
||||||
data = []byte("{}")
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var settings struct {
|
|
||||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
|
||||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &settings); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
|
||||||
}
|
}
|
||||||
|
fprintToggleEnabled = settings.GreeterEnableFprint
|
||||||
fprintModule := pamModuleExists("pam_fprintd.so")
|
fprintModule := pamModuleExists("pam_fprintd.so")
|
||||||
u2fModule := pamModuleExists("pam_u2f.so")
|
u2fModule := pamModuleExists("pam_u2f.so")
|
||||||
wantFprint = settings.GreeterEnableFprint && fprintModule
|
wantFprint = settings.GreeterEnableFprint && fprintModule
|
||||||
@@ -1212,7 +1468,8 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str
|
|||||||
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
|
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
|
||||||
wantFprint = false
|
wantFprint = false
|
||||||
}
|
}
|
||||||
if !wantFprint && includedFprintFile != "" {
|
showIncludedFprintNotice := fprintToggleEnabled && FingerprintAuthAvailableForCurrentUser()
|
||||||
|
if !wantFprint && includedFprintFile != "" && showIncludedFprintNotice {
|
||||||
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
||||||
logFunc(" Disable fingerprint in your system PAM manager (authselect/pam-auth-update) to force password-only greeter login.")
|
logFunc(" Disable fingerprint in your system PAM manager (authselect/pam-auth-update) to force password-only greeter login.")
|
||||||
}
|
}
|
||||||
@@ -1272,6 +1529,8 @@ type niriGreeterSync struct {
|
|||||||
cursorCount int
|
cursorCount int
|
||||||
debugCount int
|
debugCount int
|
||||||
cursorNode *document.Node
|
cursorNode *document.Node
|
||||||
|
inputNode *document.Node
|
||||||
|
outputNodes map[string]*document.Node
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
||||||
@@ -1289,7 +1548,8 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extractor := &niriGreeterSync{
|
extractor := &niriGreeterSync{
|
||||||
processed: make(map[string]bool),
|
processed: make(map[string]bool),
|
||||||
|
outputNodes: make(map[string]*document.Node),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := extractor.processFile(configPath); err != nil {
|
if err := extractor.processFile(configPath); err != nil {
|
||||||
@@ -1488,10 +1748,22 @@ func (s *niriGreeterSync) processFile(filePath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "input":
|
case "input":
|
||||||
s.nodes = append(s.nodes, node)
|
if s.inputNode == nil {
|
||||||
|
s.inputNode = node
|
||||||
|
s.inputNode.Children = dedupeCursorChildren(s.inputNode.Children)
|
||||||
|
s.nodes = append(s.nodes, node)
|
||||||
|
} else if len(node.Children) > 0 {
|
||||||
|
s.inputNode.Children = mergeInputChildren(s.inputNode.Children, node.Children)
|
||||||
|
}
|
||||||
s.inputCount++
|
s.inputCount++
|
||||||
case "output":
|
case "output":
|
||||||
s.nodes = append(s.nodes, node)
|
key := outputNodeKey(node)
|
||||||
|
if existing, ok := s.outputNodes[key]; ok {
|
||||||
|
*existing = *node
|
||||||
|
} else {
|
||||||
|
s.outputNodes[key] = node
|
||||||
|
s.nodes = append(s.nodes, node)
|
||||||
|
}
|
||||||
s.outputCount++
|
s.outputCount++
|
||||||
case "cursor":
|
case "cursor":
|
||||||
if s.cursorNode == nil {
|
if s.cursorNode == nil {
|
||||||
@@ -1554,6 +1826,36 @@ func dedupeCursorChildren(children []*document.Node) []*document.Node {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mergeInputChildren(existing []*document.Node, incoming []*document.Node) []*document.Node {
|
||||||
|
if len(incoming) == 0 {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
indexByName := make(map[string]int, len(existing))
|
||||||
|
for i, child := range existing {
|
||||||
|
indexByName[child.Name.String()] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range incoming {
|
||||||
|
name := child.Name.String()
|
||||||
|
if idx, ok := indexByName[name]; ok {
|
||||||
|
existing[idx] = child
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
indexByName[name] = len(existing)
|
||||||
|
existing = append(existing, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
func outputNodeKey(node *document.Node) string {
|
||||||
|
if len(node.Arguments) > 0 {
|
||||||
|
return strings.Trim(node.Arguments[0].String(), "\"")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (s *niriGreeterSync) handleInclude(node *document.Node, baseDir string) error {
|
func (s *niriGreeterSync) handleInclude(node *document.Node, baseDir string) error {
|
||||||
if len(node.Arguments) == 0 {
|
if len(node.Arguments) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Singleton {
|
|||||||
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
||||||
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
||||||
readonly property string memoryFile: greetCfgDir + "/memory.json"
|
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json"
|
||||||
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
|
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
|
||||||
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ Item {
|
|||||||
property bool cancelingExternalAuthForPassword: false
|
property bool cancelingExternalAuthForPassword: false
|
||||||
property int defaultAuthTimeoutMs: 12000
|
property int defaultAuthTimeoutMs: 12000
|
||||||
property int externalAuthTimeoutMs: 45000
|
property int externalAuthTimeoutMs: 45000
|
||||||
|
property int memoryFlushDelayMs: 120
|
||||||
|
property string pendingLaunchCommand: ""
|
||||||
|
property var pendingLaunchEnv: []
|
||||||
property int passwordFailureCount: 0
|
property int passwordFailureCount: 0
|
||||||
property int passwordAttemptLimitHint: 0
|
property int passwordAttemptLimitHint: 0
|
||||||
property string authFeedbackMessage: ""
|
property string authFeedbackMessage: ""
|
||||||
@@ -49,7 +52,7 @@ Item {
|
|||||||
property string externalAuthAutoStartedForUser: ""
|
property string externalAuthAutoStartedForUser: ""
|
||||||
readonly property bool greeterPamHasFprint: pamModuleEnabled(greetdPamText, "pam_fprintd") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"))
|
readonly property bool greeterPamHasFprint: pamModuleEnabled(greetdPamText, "pam_fprintd") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"))
|
||||||
readonly property bool greeterPamHasU2f: pamModuleEnabled(greetdPamText, "pam_u2f") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_u2f")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_u2f")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_u2f"))
|
readonly property bool greeterPamHasU2f: pamModuleEnabled(greetdPamText, "pam_u2f") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_u2f")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_u2f")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_u2f"))
|
||||||
readonly property bool greeterExternalAuthAvailable: greeterPamHasFprint || greeterPamHasU2f
|
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
|
||||||
|
|
||||||
function initWeatherService() {
|
function initWeatherService() {
|
||||||
if (weatherInitialized)
|
if (weatherInitialized)
|
||||||
@@ -1618,7 +1621,9 @@ Item {
|
|||||||
} else if (GreetdMemory.lastSuccessfulUser) {
|
} else if (GreetdMemory.lastSuccessfulUser) {
|
||||||
GreetdMemory.setLastSuccessfulUser("");
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
}
|
}
|
||||||
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
|
pendingLaunchCommand = sessionCmd;
|
||||||
|
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
|
||||||
|
memoryFlushTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAuthFailure(message) {
|
function onAuthFailure(message) {
|
||||||
@@ -1661,6 +1666,20 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: memoryFlushTimer
|
||||||
|
interval: memoryFlushDelayMs
|
||||||
|
onTriggered: {
|
||||||
|
if (!pendingLaunchCommand)
|
||||||
|
return;
|
||||||
|
const sessionCommand = pendingLaunchCommand;
|
||||||
|
const launchEnv = pendingLaunchEnv;
|
||||||
|
pendingLaunchCommand = "";
|
||||||
|
pendingLaunchEnv = [];
|
||||||
|
Greetd.launch(sessionCommand.split(" "), launchEnv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: authTimeout
|
id: authTimeout
|
||||||
interval: defaultAuthTimeoutMs
|
interval: defaultAuthTimeoutMs
|
||||||
|
|||||||
@@ -179,6 +179,22 @@ export QT_QPA_PLATFORM=wayland
|
|||||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
||||||
export EGL_PLATFORM=gbm
|
export EGL_PLATFORM=gbm
|
||||||
export DMS_RUN_GREETER=1
|
export DMS_RUN_GREETER=1
|
||||||
|
|
||||||
|
ensure_cache_tree() {
|
||||||
|
local base="$1"
|
||||||
|
mkdir -p "$base/.local/state" "$base/.local/share" "$base/.cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! ensure_cache_tree "$CACHE_DIR" 2>/dev/null; then
|
||||||
|
FALLBACK_CACHE_DIR="/tmp/dms-greeter-${UID:-$(id -u)}"
|
||||||
|
echo "Warning: cache directory '$CACHE_DIR' is not writable; falling back to '$FALLBACK_CACHE_DIR'" >&2
|
||||||
|
CACHE_DIR="$FALLBACK_CACHE_DIR"
|
||||||
|
if ! ensure_cache_tree "$CACHE_DIR"; then
|
||||||
|
echo "Error: failed to initialize fallback cache directory '$CACHE_DIR'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
||||||
|
|
||||||
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
|
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
|
||||||
@@ -203,11 +219,6 @@ if [[ -n "$REMEMBER_LAST_USER" ]]; then
|
|||||||
export DMS_SAVE_USERNAME
|
export DMS_SAVE_USERNAME
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR"
|
|
||||||
mkdir -p "$CACHE_DIR/.local/state"
|
|
||||||
mkdir -p "$CACHE_DIR/.local/share"
|
|
||||||
mkdir -p "$CACHE_DIR/.cache"
|
|
||||||
|
|
||||||
export HOME="$CACHE_DIR"
|
export HOME="$CACHE_DIR"
|
||||||
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
|
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
|
||||||
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
|
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
|
||||||
|
|||||||
@@ -49,18 +49,24 @@ Item {
|
|||||||
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
|
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
|
||||||
|
|
||||||
readonly property string greeterActionLabel: {
|
readonly property string greeterActionLabel: {
|
||||||
if (!root.greeterInstalled) return I18n.tr("Install");
|
if (!root.greeterInstalled)
|
||||||
if (!root.greeterEnabled) return I18n.tr("Activate");
|
return I18n.tr("Install");
|
||||||
|
if (!root.greeterEnabled)
|
||||||
|
return I18n.tr("Activate");
|
||||||
return I18n.tr("Uninstall");
|
return I18n.tr("Uninstall");
|
||||||
}
|
}
|
||||||
readonly property string greeterActionIcon: {
|
readonly property string greeterActionIcon: {
|
||||||
if (!root.greeterInstalled) return "download";
|
if (!root.greeterInstalled)
|
||||||
if (!root.greeterEnabled) return "login";
|
return "download";
|
||||||
|
if (!root.greeterEnabled)
|
||||||
|
return "login";
|
||||||
return "delete";
|
return "delete";
|
||||||
}
|
}
|
||||||
readonly property var greeterActionCommand: {
|
readonly property var greeterActionCommand: {
|
||||||
if (!root.greeterInstalled) return ["dms", "greeter", "install", "--terminal"];
|
if (!root.greeterInstalled)
|
||||||
if (!root.greeterEnabled) return ["dms", "greeter", "enable", "--terminal"];
|
return ["dms", "greeter", "install", "--terminal"];
|
||||||
|
if (!root.greeterEnabled)
|
||||||
|
return ["dms", "greeter", "enable", "--terminal"];
|
||||||
return ["dms", "greeter", "uninstall", "--terminal", "--yes"];
|
return ["dms", "greeter", "uninstall", "--terminal", "--yes"];
|
||||||
}
|
}
|
||||||
property string greeterPendingAction: ""
|
property string greeterPendingAction: ""
|
||||||
@@ -79,9 +85,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runGreeterInstallAction() {
|
function runGreeterInstallAction() {
|
||||||
root.greeterPendingAction = !root.greeterInstalled ? "install"
|
root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall";
|
||||||
: !root.greeterEnabled ? "activate"
|
|
||||||
: "uninstall";
|
|
||||||
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "…";
|
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "…";
|
||||||
greeterInstallActionRunning = true;
|
greeterInstallActionRunning = true;
|
||||||
greeterInstallActionProcess.running = true;
|
greeterInstallActionProcess.running = true;
|
||||||
@@ -241,6 +245,7 @@ Item {
|
|||||||
root.greeterStatusText = failure;
|
root.greeterStatusText = failure;
|
||||||
root.launchGreeterSyncTerminalFallback(false, "");
|
root.launchGreeterSyncTerminalFallback(false, "");
|
||||||
}
|
}
|
||||||
|
root.checkGreeterInstallState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +411,10 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { width: 1; height: Theme.spacingM }
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -420,7 +428,9 @@ Item {
|
|||||||
enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning
|
enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
DankButton {
|
DankButton {
|
||||||
text: I18n.tr("Refresh")
|
text: I18n.tr("Refresh")
|
||||||
|
|||||||
Reference in New Issue
Block a user