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  }