github.com/theclapp/sh@v2.6.4+incompatible/interp/module.go (about) 1 // Copyright (c) 2017, Daniel Martà <mvdan@mvdan.cc> 2 // See LICENSE for licensing information 3 4 package interp 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "os" 11 "os/exec" 12 "runtime" 13 "strings" 14 "syscall" 15 "time" 16 17 "mvdan.cc/sh/expand" 18 ) 19 20 // FromModuleContext returns the ModuleCtx value stored in ctx, if any. 21 func FromModuleContext(ctx context.Context) (ModuleCtx, bool) { 22 mc, ok := ctx.Value(moduleCtxKey{}).(ModuleCtx) 23 return mc, ok 24 } 25 26 type moduleCtxKey struct{} 27 28 // ModuleCtx is the data passed to all the module functions via a context value. 29 // It contains some of the current state of the Runner, as well as some fields 30 // necessary to implement some of the modules. 31 type ModuleCtx struct { 32 Env expand.Environ 33 Dir string 34 Stdin io.Reader 35 Stdout io.Writer 36 Stderr io.Writer 37 KillTimeout time.Duration 38 } 39 40 // UnixPath fixes absolute unix paths on Windows, for example converting 41 // "C:\\CurDir\\dev\\null" to "/dev/null". 42 func (mc ModuleCtx) UnixPath(path string) string { 43 if runtime.GOOS != "windows" { 44 return path 45 } 46 path = strings.TrimPrefix(path, mc.Dir) 47 return strings.Replace(path, `\`, `/`, -1) 48 } 49 50 // ModuleExec is the module responsible for executing a program. It is 51 // executed for all CallExpr nodes where the first argument is neither a 52 // declared function nor a builtin. 53 // 54 // Note that the name is included as the first argument. If path is an 55 // empty string, it means that the executable did not exist or was not 56 // found in $PATH. 57 // 58 // Use a return error of type ExitStatus to set the exit status. A nil error has 59 // the same effect as ExitStatus(0). If the error is of any other type, the 60 // interpreter will come to a stop. 61 type ModuleExec func(ctx context.Context, path string, args []string) error 62 63 func (ModuleExec) isModule() {} 64 65 var DefaultExec = ModuleExec(func(ctx context.Context, path string, args []string) error { 66 mc, _ := FromModuleContext(ctx) 67 if path == "" { 68 fmt.Fprintf(mc.Stderr, "%q: executable file not found in $PATH\n", args[0]) 69 return ExitStatus(127) 70 } 71 cmd := exec.Cmd{ 72 Path: path, 73 Args: args, 74 Env: execEnv(mc.Env), 75 Dir: mc.Dir, 76 Stdin: mc.Stdin, 77 Stdout: mc.Stdout, 78 Stderr: mc.Stderr, 79 } 80 81 err := cmd.Start() 82 if err == nil { 83 if done := ctx.Done(); done != nil { 84 go func() { 85 <-done 86 87 if mc.KillTimeout <= 0 || runtime.GOOS == "windows" { 88 _ = cmd.Process.Signal(os.Kill) 89 return 90 } 91 92 // TODO: don't temporarily leak this goroutine 93 // if the program stops itself with the 94 // interrupt. 95 go func() { 96 time.Sleep(mc.KillTimeout) 97 _ = cmd.Process.Signal(os.Kill) 98 }() 99 _ = cmd.Process.Signal(os.Interrupt) 100 }() 101 } 102 103 err = cmd.Wait() 104 } 105 106 switch x := err.(type) { 107 case *exec.ExitError: 108 // started, but errored - default to 1 if OS 109 // doesn't have exit statuses 110 if status, ok := x.Sys().(syscall.WaitStatus); ok { 111 if status.Signaled() && ctx.Err() != nil { 112 return ctx.Err() 113 } 114 return ExitStatus(status.ExitStatus()) 115 } 116 return ExitStatus(1) 117 case *exec.Error: 118 // did not start 119 fmt.Fprintf(mc.Stderr, "%v\n", err) 120 return ExitStatus(127) 121 default: 122 return err 123 } 124 }) 125 126 // ModuleOpen is the module responsible for opening a file. It is 127 // executed for all files that are opened directly by the shell, such as 128 // in redirects. Files opened by executed programs are not included. 129 // 130 // The path parameter is absolute and has been cleaned. 131 // 132 // Use a return error of type *os.PathError to have the error printed to 133 // stderr and the exit status set to 1. If the error is of any other type, the 134 // interpreter will come to a stop. 135 // 136 // TODO: What about stat calls? They are used heavily in the builtin 137 // test expressions, and also when doing a cd. Should they have a 138 // separate module? 139 type ModuleOpen func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) 140 141 func (ModuleOpen) isModule() {} 142 143 var DefaultOpen = ModuleOpen(func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { 144 return os.OpenFile(path, flag, perm) 145 }) 146 147 func OpenDevImpls(next ModuleOpen) ModuleOpen { 148 return func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { 149 mc, _ := FromModuleContext(ctx) 150 switch mc.UnixPath(path) { 151 case "/dev/null": 152 return devNull{}, nil 153 } 154 return next(ctx, path, flag, perm) 155 } 156 } 157 158 var _ io.ReadWriteCloser = devNull{} 159 160 type devNull struct{} 161 162 func (devNull) Read(p []byte) (int, error) { return 0, io.EOF } 163 func (devNull) Write(p []byte) (int, error) { return len(p), nil } 164 func (devNull) Close() error { return nil }