github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/usersession/autostart/autostart.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package autostart 21 22 import ( 23 "bufio" 24 "bytes" 25 "fmt" 26 "log/syslog" 27 "os" 28 "os/exec" 29 "os/user" 30 "path/filepath" 31 "sort" 32 "strings" 33 34 "github.com/snapcore/snapd/dirs" 35 "github.com/snapcore/snapd/logger" 36 "github.com/snapcore/snapd/osutil" 37 "github.com/snapcore/snapd/snap" 38 "github.com/snapcore/snapd/strutil" 39 "github.com/snapcore/snapd/strutil/shlex" 40 "github.com/snapcore/snapd/systemd" 41 ) 42 43 var ( 44 currentDesktop = splitSkippingEmpty(os.Getenv("XDG_CURRENT_DESKTOP"), ':') 45 ) 46 47 func splitSkippingEmpty(s string, sep rune) []string { 48 return strings.FieldsFunc(s, func(r rune) bool { return r == sep }) 49 } 50 51 // expandDesktopFields processes the input string and expands any %<char> 52 // patterns. '%%' expands to '%', all other patterns expand to empty strings. 53 func expandDesktopFields(in string) string { 54 raw := []rune(in) 55 out := make([]rune, 0, len(raw)) 56 57 var hasKey bool 58 for _, r := range raw { 59 if hasKey { 60 hasKey = false 61 // only allow %% -> % expansion, drop other keys 62 if r == '%' { 63 out = append(out, r) 64 } 65 continue 66 } else if r == '%' { 67 hasKey = true 68 continue 69 } 70 out = append(out, r) 71 } 72 return string(out) 73 } 74 75 type skipDesktopFileError struct { 76 reason string 77 } 78 79 func (s *skipDesktopFileError) Error() string { 80 return s.reason 81 } 82 83 func isOneOfIn(of []string, other []string) bool { 84 for _, one := range of { 85 if strutil.ListContains(other, one) { 86 return true 87 } 88 } 89 return false 90 } 91 92 func loadAutostartDesktopFile(path string) (command string, err error) { 93 f, err := os.Open(path) 94 if err != nil { 95 return "", err 96 } 97 defer f.Close() 98 99 scanner := bufio.NewScanner(f) 100 for scanner.Scan() { 101 bline := scanner.Bytes() 102 if bytes.HasPrefix(bline, []byte("#")) { 103 continue 104 } 105 split := bytes.SplitN(bline, []byte("="), 2) 106 if len(split) != 2 { 107 continue 108 } 109 // See https://standards.freedesktop.org/autostart-spec/autostart-spec-latest.html 110 // for details on how Hidden, OnlyShowIn, NotShownIn are handled. 111 switch string(split[0]) { 112 case "Exec": 113 command = strings.TrimSpace(expandDesktopFields(string(split[1]))) 114 case "Hidden": 115 if bytes.Equal(split[1], []byte("true")) { 116 return "", &skipDesktopFileError{"desktop file is hidden"} 117 } 118 case "OnlyShowIn": 119 onlyIn := splitSkippingEmpty(string(split[1]), ';') 120 if !isOneOfIn(currentDesktop, onlyIn) { 121 return "", &skipDesktopFileError{fmt.Sprintf("current desktop %q not included in %q", currentDesktop, onlyIn)} 122 } 123 case "NotShownIn": 124 notIn := splitSkippingEmpty(string(split[1]), ';') 125 if isOneOfIn(currentDesktop, notIn) { 126 return "", &skipDesktopFileError{fmt.Sprintf("current desktop %q excluded by %q", currentDesktop, notIn)} 127 } 128 case "X-GNOME-Autostart-enabled": 129 // GNOME specific extension, see gnome-session: 130 // https://github.com/GNOME/gnome-session/blob/c449df5269e02c59ae83021a3110ec1b338a2bba/gnome-session/gsm-autostart-app.c#L110..L145 131 if !strutil.ListContains(currentDesktop, "GNOME") { 132 // not GNOME 133 continue 134 } 135 if !bytes.Equal(split[1], []byte("true")) { 136 return "", &skipDesktopFileError{"desktop file is hidden by X-GNOME-Autostart-enabled extension"} 137 } 138 } 139 } 140 if err := scanner.Err(); err != nil { 141 return "", err 142 } 143 144 command = strings.TrimSpace(command) 145 if command == "" { 146 return "", fmt.Errorf("Exec not found or invalid") 147 } 148 return command, nil 149 150 } 151 152 func autostartCmd(snapName, desktopFilePath string) (*exec.Cmd, error) { 153 desktopFile := filepath.Base(desktopFilePath) 154 155 info, err := snap.ReadCurrentInfo(snapName) 156 if err != nil { 157 return nil, err 158 } 159 160 var app *snap.AppInfo 161 for _, candidate := range info.Apps { 162 if candidate.Autostart == desktopFile { 163 app = candidate 164 break 165 } 166 } 167 if app == nil { 168 return nil, fmt.Errorf("cannot match desktop file with snap %s applications", snapName) 169 } 170 171 command, err := loadAutostartDesktopFile(desktopFilePath) 172 if err != nil { 173 if _, ok := err.(*skipDesktopFileError); ok { 174 return nil, fmt.Errorf("skipped: %v", err) 175 } 176 return nil, fmt.Errorf("cannot determine startup command for application %s in snap %s: %v", app.Name, snapName, err) 177 } 178 logger.Debugf("exec line: %v", command) 179 180 split, err := shlex.Split(command) 181 if err != nil { 182 return nil, fmt.Errorf("invalid application startup command: %v", err) 183 } 184 185 // NOTE: Ignore the actual argv[0] in Exec=.. line and replace it with a 186 // command of the snap application. Any arguments passed in the Exec=.. 187 // line to the original command are preserved. 188 cmd := exec.Command(app.WrapperPath(), split[1:]...) 189 return cmd, nil 190 } 191 192 // failedAutostartError keeps track of errors that occurred when starting an 193 // application for a specific desktop file, desktop file name is as a key 194 type failedAutostartError map[string]error 195 196 func (f failedAutostartError) Error() string { 197 var out bytes.Buffer 198 199 dfiles := make([]string, 0, len(f)) 200 for desktopFile := range f { 201 dfiles = append(dfiles, desktopFile) 202 } 203 sort.Strings(dfiles) 204 for _, desktopFile := range dfiles { 205 fmt.Fprintf(&out, "- %q: %v\n", desktopFile, f[desktopFile]) 206 } 207 return out.String() 208 } 209 210 func makeStdStreams(identifier string) (stdout *os.File, stderr *os.File) { 211 var err error 212 213 stdout, err = systemd.NewJournalStreamFile(identifier, syslog.LOG_INFO, false) 214 if err != nil { 215 logger.Noticef("failed to set up stdout journal stream for %q: %v", identifier, err) 216 stdout = os.Stdout 217 } 218 219 stderr, err = systemd.NewJournalStreamFile(identifier, syslog.LOG_WARNING, false) 220 if err != nil { 221 logger.Noticef("failed to set up stderr journal stream for %q: %v", identifier, err) 222 stderr = os.Stderr 223 } 224 225 return stdout, stderr 226 } 227 228 var userCurrent = user.Current 229 230 func MockUserCurrent(f func() (*user.User, error)) (restore func()) { 231 osutil.MustBeTestBinary("mocking can only be done in tests") 232 old := userCurrent 233 userCurrent = f 234 return func() { 235 userCurrent = old 236 } 237 } 238 239 // AutostartSessionApps starts applications which have placed their desktop 240 // files in $SNAP_USER_DATA/.config/autostart 241 // 242 // NOTE: By the spec, the actual path is $SNAP_USER_DATA/${XDG_CONFIG_DIR}/autostart 243 func AutostartSessionApps() error { 244 usr, err := userCurrent() 245 if err != nil { 246 return err 247 } 248 249 usrSnapDir := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) 250 251 glob := filepath.Join(usrSnapDir, "*/current/.config/autostart/*.desktop") 252 matches, err := filepath.Glob(glob) 253 if err != nil { 254 return err 255 } 256 257 failedApps := make(failedAutostartError) 258 for _, desktopFilePath := range matches { 259 desktopFile := filepath.Base(desktopFilePath) 260 logger.Debugf("autostart desktop file %v", desktopFile) 261 262 // /home/foo/snap/some-snap/current/.config/autostart/some-app.desktop -> 263 // some-snap/current/.config/autostart/some-app.desktop 264 noHomePrefix := strings.TrimPrefix(desktopFilePath, usrSnapDir+"/") 265 // some-snap/current/.config/autostart/some-app.desktop -> some-snap 266 snapName := noHomePrefix[0:strings.IndexByte(noHomePrefix, '/')] 267 268 logger.Debugf("snap name: %q", snapName) 269 270 cmd, err := autostartCmd(snapName, desktopFilePath) 271 if err != nil { 272 failedApps[desktopFile] = err 273 continue 274 } 275 276 // similarly to gnome-session, use the desktop file name as 277 // identifier, see: 278 // https://github.com/GNOME/gnome-session/blob/099c19099de8e351f6cc0f2110ad27648780a0fe/gnome-session/gsm-autostart-app.c#L948 279 cmd.Stdout, cmd.Stderr = makeStdStreams(desktopFile) 280 if err := cmd.Start(); err != nil { 281 failedApps[desktopFile] = fmt.Errorf("cannot autostart %q: %v", desktopFile, err) 282 } 283 } 284 if len(failedApps) > 0 { 285 return failedApps 286 } 287 return nil 288 }