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