package main import ( "bufio" "os" "path/filepath" "sort" "strconv" "strings" "syscall" ) // Autostart is one XDG autostart entry, and what the session does with it. type Autostart struct { Entry Scope string `json:"scope"` // user or system // Overrides names the system entry of the same name this user entry hides; OverriddenBy the // user entry hiding this system one. Overrides string `json:"overrides,omitempty"` OverriddenBy string `json:"overridden_by,omitempty"` DexStarts bool `json:"dex_starts"` Why string `json:"why"` AlsoStarted string `json:"also_started_by,omitempty"` Running int `json:"running"` } // dexDecides says whether `dex --autostart --environment ` starts an entry, by dex's own // rules (dex 0.10): an entry of the same name in an earlier directory wins; Hidden=true, an // OnlyShowIn without the environment, a NotShowIn with it, a TryExec that is not there, or a type // other than Application keep it from starting. X-GNOME-Autostart-enabled and AutostartCondition // are GNOME's, and dex does not read them. func (m *Machine) dexDecides(a *Autostart, env string) { switch { case a.OverriddenBy != "": a.Why = "hidden by the user entry " + a.OverriddenBy case a.Broken != "": a.Why = a.Broken case a.Type != "" && a.Type != "Application": a.Why = "type " + a.Type + ", not Application" case a.Hidden: a.Why = "Hidden=true" case len(a.OnlyShow) > 0 && !contains(a.OnlyShow, env): a.Why = "OnlyShowIn " + strings.Join(a.OnlyShow, ";") + " does not name " + env case contains(a.NotShow, env): a.Why = "NotShowIn names " + env case a.TryExec != "" && !m.tryExecFound(a.TryExec): a.Why = "TryExec " + a.TryExec + " is not installed" default: a.DexStarts = true a.Why = "started at every login" if !a.Found { a.Why = "started at every login, and fails: " + a.Program + " is not installed" } if strings.EqualFold(a.GnomeEnabled, "false") { a.Why += "; X-GNOME-Autostart-enabled=false does not stop dex (use Hidden=true)" } } } func (m *Machine) tryExecFound(p string) bool { _, ok := m.executable(p) return ok } func contains(list []string, s string) bool { for _, x := range list { if strings.EqualFold(x, s) { return true } } return false } // autostartDirs are the account's directory, then the system's, as dex reads them. func (m *Machine) autostartDirs() []struct{ dir, scope string } { out := []struct{ dir, scope string }{{filepath.Join(m.configHome(), "autostart"), "user"}} for _, d := range m.configDirs() { out = append(out, struct{ dir, scope string }{filepath.Join(d, "autostart"), "system"}) } return out } // Autostarts lists every entry, user and system, with dex's decision for env. func (m *Machine) Autostarts(env string) []Autostart { var out []Autostart first := map[string]int{} procs := m.processes() starts := m.windowManagerStarts() for _, d := range m.autostartDirs() { entries, _ := os.ReadDir(m.p(d.dir)) for _, de := range entries { if !strings.HasSuffix(de.Name(), ".desktop") { continue } a := Autostart{Entry: m.readEntry(filepath.Join(d.dir, de.Name())), Scope: d.scope} if i, seen := first[de.Name()]; seen { a.OverriddenBy = out[i].File out[i].Overrides = a.File } else { first[de.Name()] = len(out) } out = append(out, a) } } for i := range out { a := &out[i] m.dexDecides(a, env) a.Running = procs.count(a.Program) if by, ok := starts[filepath.Base(a.Program)]; ok && a.Program != "" { a.AlsoStarted = by } } return out } // procs are the account's processes: each one's first two words, base names only, and its name. type procs [][3]string // processes reads the account's own processes from /proc; nothing but the command line and the // process's name is read. func (m *Machine) processes() procs { var out procs entries, _ := os.ReadDir(m.p("/proc")) self := strconv.Itoa(os.Getpid()) for _, e := range entries { if _, err := strconv.Atoi(e.Name()); err != nil || e.Name() == self { continue } dir := m.p(filepath.Join("/proc", e.Name())) info, err := os.Stat(dir) if err != nil { continue } if st, ok := info.Sys().(*syscall.Stat_t); !ok || int(st.Uid) != m.UID { continue } raw, err := os.ReadFile(filepath.Join(dir, "cmdline")) if err != nil || len(raw) == 0 { continue } words := strings.Split(strings.TrimRight(string(raw), "\x00"), "\x00") if len(words) == 1 { // Chromium and Electron rewrite their command line as one string, words joined by spaces. words = strings.Fields(words[0]) } if len(words) == 0 { continue } var p [3]string if comm, err := os.ReadFile(filepath.Join(dir, "comm")); err == nil { p[2] = strings.TrimSpace(string(comm)) } p[0] = filepath.Base(words[0]) if len(words) > 1 { p[1] = filepath.Base(words[1]) } out = append(out, p) } return out } // count is how many processes run a program: as their command, or as the script an interpreter // runs (a Python applet is "python3 /usr/bin/applet"). func (ps procs) count(program string) int { if program == "" { return 0 } name := filepath.Base(program) n := 0 for _, p := range ps { // The kernel keeps a process's name to 15 bytes. if p[0] == name || p[1] == name && isInterpreter(p[0]) || p[2] != "" && p[2] == truncate(name, 15) { n++ } } return n } func truncate(s string, n int) string { if len(s) > n { return s[:n] } return s } func isInterpreter(cmd string) bool { for _, i := range []string{"python", "perl", "sh", "bash", "node", "ruby"} { if strings.HasPrefix(cmd, i) { return true } } return false } // windowManagerStarts are the programs the window manager's configuration starts itself, by base // name, with the line that does it. Read only: the configuration is the window manager's module's. func (m *Machine) windowManagerStarts() map[string]string { out := map[string]string{} files := []string{filepath.Join(m.configHome(), "i3", "config")} more, _ := filepath.Glob(m.p(filepath.Join(m.configHome(), "i3", "config.d", "*.conf"))) sort.Strings(more) for _, f := range more { files = append(files, strings.TrimPrefix(f, m.Root)) } for _, f := range files { h, err := os.Open(m.p(f)) if err != nil { continue } sc := bufio.NewScanner(h) n := 0 for sc.Scan() { n++ words := strings.Fields(sc.Text()) if len(words) < 2 || words[0] != "exec" && words[0] != "exec_always" { continue } rest := words[1:] for len(rest) > 0 && strings.HasPrefix(rest[0], "--") { rest = rest[1:] } if len(rest) == 0 { continue } prog := filepath.Base(Program(strings.Join(rest, " "))) if prog == "dex" { continue } out[prog] = m.tilde(f) + ":" + strconv.Itoa(n) } h.Close() } return out }