github.com/rigado/snapd@v2.42.5-go-mod+incompatible/cmd/snap-exec/main.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2015 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 main
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"syscall"
    28  
    29  	"github.com/jessevdk/go-flags"
    30  
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/osutil"
    33  	"github.com/snapcore/snapd/snap"
    34  	"github.com/snapcore/snapd/snap/snapenv"
    35  )
    36  
    37  // for the tests
    38  var syscallExec = syscall.Exec
    39  var osReadlink = os.Readlink
    40  
    41  // commandline args
    42  var opts struct {
    43  	Command string `long:"command" description:"use a different command like {stop,post-stop} from the app"`
    44  	Hook    string `long:"hook" description:"hook to run" hidden:"yes"`
    45  }
    46  
    47  func init() {
    48  	// plug/slot sanitization not used nor possible from snap-exec, make it no-op
    49  	snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {}
    50  }
    51  
    52  func main() {
    53  	if err := run(); err != nil {
    54  		fmt.Fprintf(os.Stderr, "cannot snap-exec: %s\n", err)
    55  		os.Exit(1)
    56  	}
    57  }
    58  
    59  func parseArgs(args []string) (app string, appArgs []string, err error) {
    60  	parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption)
    61  	rest, err := parser.ParseArgs(args)
    62  	if err != nil {
    63  		return "", nil, err
    64  	}
    65  	if len(rest) == 0 {
    66  		return "", nil, fmt.Errorf("need the application to run as argument")
    67  	}
    68  
    69  	// Catch some invalid parameter combinations, provide helpful errors
    70  	if opts.Hook != "" && opts.Command != "" {
    71  		return "", nil, fmt.Errorf("cannot use --hook and --command together")
    72  	}
    73  	if opts.Hook != "" && len(rest) > 1 {
    74  		return "", nil, fmt.Errorf("too many arguments for hook %q: %s", opts.Hook, strings.Join(rest, " "))
    75  	}
    76  
    77  	return rest[0], rest[1:], nil
    78  }
    79  
    80  func run() error {
    81  	snapApp, extraArgs, err := parseArgs(os.Args[1:])
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	// the SNAP_REVISION is set by `snap run` - we can not (easily)
    87  	// find it in `snap-exec` because `snap-exec` is run inside the
    88  	// confinement and (generally) can not talk to snapd
    89  	revision := os.Getenv("SNAP_REVISION")
    90  
    91  	// Now actually handle the dispatching
    92  	if opts.Hook != "" {
    93  		return execHook(snapApp, revision, opts.Hook)
    94  	}
    95  
    96  	return execApp(snapApp, revision, opts.Command, extraArgs)
    97  }
    98  
    99  const defaultShell = "/bin/bash"
   100  
   101  func findCommand(app *snap.AppInfo, command string) (string, error) {
   102  	var cmd string
   103  	switch command {
   104  	case "shell":
   105  		cmd = defaultShell
   106  	case "complete":
   107  		if app.Completer != "" {
   108  			cmd = defaultShell
   109  		}
   110  	case "stop":
   111  		cmd = app.StopCommand
   112  	case "reload":
   113  		cmd = app.ReloadCommand
   114  	case "post-stop":
   115  		cmd = app.PostStopCommand
   116  	case "", "gdb":
   117  		cmd = app.Command
   118  	default:
   119  		return "", fmt.Errorf("cannot use %q command", command)
   120  	}
   121  
   122  	if cmd == "" {
   123  		return "", fmt.Errorf("no %q command found for %q", command, app.Name)
   124  	}
   125  	return cmd, nil
   126  }
   127  
   128  func absoluteCommandChain(snapInfo *snap.Info, commandChain []string) []string {
   129  	chain := make([]string, 0, len(commandChain))
   130  	snapMountDir := snapInfo.MountDir()
   131  
   132  	for _, element := range commandChain {
   133  		chain = append(chain, filepath.Join(snapMountDir, element))
   134  	}
   135  
   136  	return chain
   137  }
   138  
   139  // expandEnvCmdArgs takes the string list of commandline arguments
   140  // and expands any $VAR with the given var from the env argument.
   141  func expandEnvCmdArgs(args []string, env map[string]string) []string {
   142  	cmdArgs := make([]string, 0, len(args))
   143  	for _, arg := range args {
   144  		maybeExpanded := os.Expand(arg, func(k string) string {
   145  			return env[k]
   146  		})
   147  		if maybeExpanded != "" {
   148  			cmdArgs = append(cmdArgs, maybeExpanded)
   149  		}
   150  	}
   151  	return cmdArgs
   152  }
   153  
   154  func completionHelper() (string, error) {
   155  	exe, err := osReadlink("/proc/self/exe")
   156  	if err != nil {
   157  		return "", err
   158  	}
   159  	return filepath.Join(filepath.Dir(exe), "etelpmoc.sh"), nil
   160  }
   161  
   162  func execApp(snapApp, revision, command string, args []string) error {
   163  	rev, err := snap.ParseRevision(revision)
   164  	if err != nil {
   165  		return fmt.Errorf("cannot parse revision %q: %s", revision, err)
   166  	}
   167  
   168  	snapName, appName := snap.SplitSnapApp(snapApp)
   169  	info, err := snap.ReadInfo(snapName, &snap.SideInfo{
   170  		Revision: rev,
   171  	})
   172  	if err != nil {
   173  		return fmt.Errorf("cannot read info for %q: %s", snapName, err)
   174  	}
   175  
   176  	app := info.Apps[appName]
   177  	if app == nil {
   178  		return fmt.Errorf("cannot find app %q in %q", appName, snapName)
   179  	}
   180  
   181  	cmdAndArgs, err := findCommand(app, command)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	// build the environment from the yaml, translating TMPDIR and
   187  	// similar variables back from where they were hidden when
   188  	// invoking the setuid snap-confine.
   189  	env := []string{}
   190  	for _, kv := range os.Environ() {
   191  		if strings.HasPrefix(kv, snapenv.PreservedUnsafePrefix) {
   192  			kv = kv[len(snapenv.PreservedUnsafePrefix):]
   193  		}
   194  		env = append(env, kv)
   195  	}
   196  	env = append(env, osutil.SubstituteEnv(app.Env())...)
   197  
   198  	// strings.Split() is ok here because we validate all app fields and the
   199  	// whitelist is pretty strict (see snap/validate.go:appContentWhitelist)
   200  	// (see also overlord/snapstate/check_snap.go's normPath)
   201  	tmpArgv := strings.Split(cmdAndArgs, " ")
   202  	cmd := tmpArgv[0]
   203  	cmdArgs := expandEnvCmdArgs(tmpArgv[1:], osutil.EnvMap(env))
   204  
   205  	// run the command
   206  	fullCmd := []string{filepath.Join(app.Snap.MountDir(), cmd)}
   207  	switch command {
   208  	case "shell":
   209  		fullCmd[0] = defaultShell
   210  		cmdArgs = nil
   211  	case "complete":
   212  		fullCmd[0] = defaultShell
   213  		helper, err := completionHelper()
   214  		if err != nil {
   215  			return fmt.Errorf("cannot find completion helper: %v", err)
   216  		}
   217  		cmdArgs = []string{
   218  			helper,
   219  			filepath.Join(app.Snap.MountDir(), app.Completer),
   220  		}
   221  	case "gdb":
   222  		fullCmd = append(fullCmd, fullCmd[0])
   223  		fullCmd[0] = filepath.Join(dirs.CoreLibExecDir, "snap-gdb-shim")
   224  	}
   225  	fullCmd = append(fullCmd, cmdArgs...)
   226  	fullCmd = append(fullCmd, args...)
   227  
   228  	fullCmd = append(absoluteCommandChain(app.Snap, app.CommandChain), fullCmd...)
   229  
   230  	if err := syscallExec(fullCmd[0], fullCmd, env); err != nil {
   231  		return fmt.Errorf("cannot exec %q: %s", fullCmd[0], err)
   232  	}
   233  	// this is never reached except in tests
   234  	return nil
   235  }
   236  
   237  func execHook(snapName, revision, hookName string) error {
   238  	rev, err := snap.ParseRevision(revision)
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	info, err := snap.ReadInfo(snapName, &snap.SideInfo{
   244  		Revision: rev,
   245  	})
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	hook := info.Hooks[hookName]
   251  	if hook == nil {
   252  		return fmt.Errorf("cannot find hook %q in %q", hookName, snapName)
   253  	}
   254  
   255  	// build the environment
   256  	env := append(os.Environ(), osutil.SubstituteEnv(hook.Env())...)
   257  
   258  	// run the hook
   259  	cmd := append(absoluteCommandChain(hook.Snap, hook.CommandChain), filepath.Join(hook.Snap.HooksDir(), hook.Name))
   260  	return syscallExec(cmd[0], cmd, env)
   261  }