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 }