File: interpreter.go

package info (click to toggle)
singularity-container 4.0.3%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 21,672 kB
  • sloc: asm: 3,857; sh: 2,125; ansic: 1,677; awk: 414; makefile: 110; python: 99
file content (328 lines) | stat: -rw-r--r-- 9,635 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
// Copyright (c) 2020-2022, Sylabs, Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license.  Please
// consult LICENSE.md file distributed with the sources of this project regarding
// your rights to use or distribute this software.

package interpreter

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"syscall"
	"time"

	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/interp"
	"mvdan.cc/sh/v3/syntax"
)

// ShellBuiltin defines function prototype for shell interpreter builtin registration.
type ShellBuiltin func(ctx context.Context, args []string) error

// OpenHandler defines function prototype for shell interpreter file handler registration.
type OpenHandler func(path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)

// Shell defines the shell interpreter.
type Shell struct {
	shellBuiltins map[string]ShellBuiltin
	openHandlers  map[string]OpenHandler
	name          string
	status        uint8
	reader        io.Reader
	runner        *interp.Runner
}

// execTimeout defines the execution timeout for commands executed by the
// shell interpreter (default: 1 minute).
var execTimeout = time.Minute

// defaultExecHandler is the default command execution handler if there is
// no registered shell builtin.
func defaultExecHandler(ctx context.Context, args []string) error {
	hc := interp.HandlerCtx(ctx)
	path, err := interp.LookPath(hc.Env, args[0])
	if err != nil {
		fmt.Fprintln(hc.Stderr, err)
		return interp.NewExitStatus(127)
	}

	ectx, cancel := context.WithTimeout(ctx, execTimeout)
	defer cancel()

	cmd := exec.CommandContext(ectx, path, args[1:]...)
	cmd.Env = append([]string{"PWD=" + hc.Dir}, GetEnv(hc)...)
	cmd.Dir = hc.Dir
	cmd.Stdin = hc.Stdin
	cmd.Stdout = hc.Stdout
	cmd.Stderr = hc.Stderr

	err = cmd.Run()

	switch x := err.(type) {
	case *exec.ExitError:
		if status, ok := x.Sys().(syscall.WaitStatus); ok {
			if status.Signaled() && ectx.Err() != nil {
				c := strings.Join(args, " ")
				return fmt.Errorf("command %q was killed after %s timeout", c, execTimeout)
			}
			return interp.NewExitStatus(uint8(status.ExitStatus()))
		}
		return interp.NewExitStatus(1)
	case *exec.Error:
		c := strings.Join(args, " ")
		return fmt.Errorf("command %q execution failed: %s", c, err)
	}

	return err
}

// GetEnv returns an the list of all exported environment variables within
// the context of the shell interpreter.
func GetEnv(hc interp.HandlerContext) []string {
	envMap := make(map[string]expand.Variable)

	// use of a map to remove duplicated variable, the latest wins
	hc.Env.Each(func(name string, vr expand.Variable) bool {
		envMap[name] = vr
		return true
	})

	environ := make([]string, 0, len(envMap))
	for k, v := range envMap {
		if v.Exported && v.Kind == expand.String {
			environ = append(environ, k+"="+v.Str)
		}
	}

	sort.Strings(environ)

	return environ
}

// New returns a shell interpreter instance.
func New(r io.Reader, name string, args []string, envs []string, runnerOptions ...interp.RunnerOption) (s *Shell, err error) {
	if r == nil {
		return nil, fmt.Errorf("nil reader")
	}

	s = &Shell{
		reader: r,
		name:   name,
	}

	dir, err := os.Getwd()
	if err != nil {
		dir = "/"
	}

	// TODO - update to ExecHandlers, as ExecHandler is deprecated.
	opts := []interp.RunnerOption{
		interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
		interp.ExecHandler(s.internalExecHandler()), //nolint:staticcheck
		interp.OpenHandler(s.internalOpenHandler()),
		interp.Params("--"),
		interp.Env(expand.ListEnviron(envs...)),
		interp.Dir(dir),
	}
	opts = append(opts, runnerOptions...)
	s.runner, err = interp.New(opts...)

	if err != nil {
		return nil, fmt.Errorf("while creating shell interpreter: %s", err)
	}

	s.runner.Params = append(s.runner.Params, args...)

	return s, err
}

// internalExecHandler returns an ExecHandlerFunc used by default.
func (s *Shell) internalExecHandler() interp.ExecHandlerFunc {
	return func(ctx context.Context, args []string) error {
		if s.runner.Exited() {
			// special path for exec builtin keyword
			if builtin, ok := s.shellBuiltins["exec"]; ok {
				return builtin(ctx, args)
			}
		} else if builtin, ok := s.shellBuiltins[args[0]]; ok {
			return builtin(ctx, args[1:])
		} else {
			// declaration clause are normally handled by the interpreter
			// but when a builtin prefixed with a backslash is encountered
			// by example, the parser consider it as a call expression and
			// we get there, so basically what we do is to create a new parser
			// and evaluate it in the current shell interpreter
			switch args[0] {
			case "export", "local", "declare", "nameref", "readonly", "typeset":
				var b bytes.Buffer

				b.WriteString(strings.Join(args, " "))
				node, err := syntax.NewParser().Parse(&b, s.name)
				if err != nil {
					return err
				}

				// We run individual syntax.Stmt rather than the parsed syntax.File as the latter
				// implies an `exit`, and causes https://github.com/sylabs/singularity/issues/274
				// with the exit/trap changes in https://github.com/mvdan/sh/commit/fb5052e7a0109c9ef5553a310c05f3b8c04cca5f
				for _, stmt := range node.Stmts {
					if err := s.runner.Run(ctx, stmt); err != nil {
						return err
					}
				}
				return nil
			}
		}
		return defaultExecHandler(ctx, args)
	}
}

// internalOpenHandler returns an OpenHandlerFunc used by default.
func (s *Shell) internalOpenHandler() interp.OpenHandlerFunc {
	return func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
		mc := interp.HandlerCtx(ctx)
		if !filepath.IsAbs(path) {
			path = filepath.Join(mc.Dir, path)
		}
		if handler, ok := s.openHandlers[path]; ok {
			return handler(path, flag, perm)
		}
		return os.OpenFile(path, flag, perm)
	}
}

// RegisterShellBuiltin registers a shell interpreter builtin.
func (s *Shell) RegisterShellBuiltin(name string, builtin ShellBuiltin) {
	if s.shellBuiltins == nil {
		s.shellBuiltins = make(map[string]ShellBuiltin)
	}
	s.shellBuiltins[name] = builtin
}

// RegisterOpenHandler registers a shell interpreter file handler.
func (s *Shell) RegisterOpenHandler(path string, handler OpenHandler) {
	if s.openHandlers == nil {
		s.openHandlers = make(map[string]OpenHandler)
	}
	s.openHandlers[path] = handler
}

// LookPath returns the absolute path for the command passed in argument
// within the context of the shell interpreter.
func (s *Shell) LookPath(ctx context.Context, cmd string) (string, error) {
	hc := interp.HandlerCtx(ctx)
	return interp.LookPath(hc.Env, cmd)
}

// Run runs the shell interpreter.
func (s *Shell) Run(ctx context.Context) error {
	parser := syntax.NewParser()
	node, err := parser.Parse(s.reader, s.name)
	if err != nil {
		return fmt.Errorf("while parsing script: %s", err)
	}

	if err := s.runner.Run(ctx, node); err != nil {
		if status, ok := interp.IsExitStatus(err); ok {
			s.status = status
		}
		return err
	}

	return nil
}

// Status returns the exit code status of the shell interpreter.
func (s *Shell) Status() uint8 {
	return s.status
}

// nonExportedEnv allows to initialize shell interpreter with all environment
// variables set as non exported variables.
type nonExportedEnv struct {
	envs map[string]expand.Variable
}

// newNonExportedEnv returns a localEnv instance associated to environment
// passed in argument.
func newNonExportedEnv(env []string) nonExportedEnv {
	local := nonExportedEnv{
		envs: make(map[string]expand.Variable),
	}
	for _, e := range env {
		e := strings.SplitN(e, "=", 2)
		local.envs[e[0]] = expand.Variable{Str: e[1], Kind: expand.String}
	}
	return local
}

// Get returns the named shell environment variable.
func (e nonExportedEnv) Get(name string) expand.Variable {
	if vr, ok := e.envs[name]; ok {
		return vr
	}
	return expand.Variable{}
}

// Each iterates over all environment variables by calling the
// function passed in argument for each variables.
func (e nonExportedEnv) Each(fn func(name string, vr expand.Variable) bool) {
	for name, vr := range e.envs {
		if !fn(name, vr) {
			return
		}
	}
}

// EvaluateEnv evaluates the environment variable script and returns
// the list of variables set in the script. Command execution is disabled
// along with redirection.
func EvaluateEnv(ctx context.Context, script []byte, args []string, envs []string) ([]string, error) {
	const stopBuiltin = "__stop__"

	var env []string

	// disable command execution and just handle stop builtin
	execHandler := func(ctx context.Context, args []string) error {
		if args[0] == stopBuiltin {
			env = GetEnv(interp.HandlerCtx(ctx))
			return nil
		}
		c := strings.Join(args, " ")
		return fmt.Errorf("could not execute %q: execution is disabled", c)
	}
	openHandler := func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
		return nil, fmt.Errorf("could not open/create/modify %q: file feature is disabled", path)
	}

	// TODO - update to ExecHandlers, as ExecHandler is deprecated.
	opts := []interp.RunnerOption{
		interp.ExecHandler(execHandler), //nolint:staticcheck
		interp.OpenHandler(openHandler),
		interp.Env(newNonExportedEnv(envs)),
	}

	b := bytes.NewBuffer(script)
	// append stop builtin to the end of the script
	b.WriteString("\n" + stopBuiltin + "\n")

	shell, err := New(b, "singularity", args, nil, opts...)
	if err != nil {
		return nil, fmt.Errorf("while initializing shell interpreter: %s", err)
	}
	// set allexport option
	interp.Params("-a")(shell.runner)

	if err := shell.Run(ctx); err != nil {
		return nil, fmt.Errorf("while evaluating environment script: %s", err)
	}

	return env, nil
}