github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/worker/exec/common.go (about) 1 package exec 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "encoding/json" 8 "math" 9 "os" 10 "runtime" 11 "strconv" 12 "time" 13 14 "github.com/cozy/cozy-stack/model/instance" 15 "github.com/cozy/cozy-stack/model/job" 16 "github.com/cozy/cozy-stack/pkg/logger" 17 "github.com/cozy/cozy-stack/pkg/metrics" 18 "github.com/cozy/cozy-stack/pkg/utils" 19 "github.com/prometheus/client_golang/prometheus" 20 "github.com/spf13/afero" 21 ) 22 23 var defaultTimeout = 300 * time.Second 24 25 func init() { 26 job.AddWorker(&job.WorkerConfig{ 27 WorkerType: "konnector", 28 WorkerStart: func(ctx *job.TaskContext) (*job.TaskContext, error) { 29 return ctx.WithCookie(&konnectorWorker{}), nil 30 }, 31 BeforeHook: beforeHookKonnector, 32 ErrorHook: jobHookErrorCheckerKonnector, 33 WorkerFunc: worker, 34 WorkerCommit: commit, 35 Concurrency: runtime.NumCPU() * 2, 36 MaxExecCount: 2, 37 Timeout: defaultTimeout, 38 }) 39 40 job.AddWorker(&job.WorkerConfig{ 41 WorkerType: "service", 42 WorkerStart: func(ctx *job.TaskContext) (*job.TaskContext, error) { 43 return ctx.WithCookie(&serviceWorker{}), nil 44 }, 45 WorkerFunc: worker, 46 WorkerCommit: commit, 47 Concurrency: runtime.NumCPU() * 2, 48 MaxExecCount: 2, 49 Timeout: defaultTimeout, 50 }) 51 } 52 53 type execWorker interface { 54 Slug() string 55 PrepareWorkDir(ctx *job.TaskContext, i *instance.Instance) (workDir string, cleanDir func(), err error) 56 PrepareCmdEnv(ctx *job.TaskContext, i *instance.Instance) (cmd string, env []string, err error) 57 ScanOutput(ctx *job.TaskContext, i *instance.Instance, line []byte) error 58 Error(i *instance.Instance, err error) error 59 Logger(ctx *job.TaskContext) logger.Logger 60 Commit(ctx *job.TaskContext, errjob error) error 61 } 62 63 func worker(ctx *job.TaskContext) (err error) { 64 worker := ctx.Cookie().(execWorker) 65 66 if ctx.Instance == nil { 67 return instance.ErrNotFound 68 } 69 70 workDir, cleanDir, err := worker.PrepareWorkDir(ctx, ctx.Instance) 71 defer cleanDir() 72 if err != nil { 73 worker.Logger(ctx).Errorf("PrepareWorkDir: %s", err) 74 return err 75 } 76 77 cmdStr, env, err := worker.PrepareCmdEnv(ctx, ctx.Instance) 78 if err != nil { 79 worker.Logger(ctx).Errorf("PrepareCmdEnv: %s", err) 80 return err 81 } 82 83 var stderrBuf bytes.Buffer 84 cmd := CreateCmd(cmdStr, workDir) 85 cmd.Env = env 86 87 // set stderr writable with a bytes.Buffer limited total size of 256Ko 88 cmd.Stderr = utils.LimitWriterDiscard(&stderrBuf, 256*1024) 89 90 // Log out all things printed in stderr, whatever the result of the 91 // konnector is. 92 log := worker.Logger(ctx) 93 defer func() { 94 if stderrBuf.Len() > 0 { 95 log.Errorf("Stderr: %s", stderrBuf.String()) 96 } 97 }() 98 99 cmdOut, err := cmd.StdoutPipe() 100 if err != nil { 101 return err 102 } 103 scanBuf := make([]byte, 16*1024) 104 scanOut := bufio.NewScanner(cmdOut) 105 scanOut.Buffer(scanBuf, 64*1024) 106 107 timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { 108 var result string 109 if err != nil { 110 result = metrics.WorkerExecResultErrored 111 } else { 112 result = metrics.WorkerExecResultSuccess 113 } 114 metrics.WorkersKonnectorsExecDurations. 115 WithLabelValues(worker.Slug(), result). 116 Observe(v) 117 })) 118 defer timer.ObserveDuration() 119 120 if err = cmd.Start(); err != nil { 121 return wrapErr(ctx, err) 122 } 123 124 waitDone := make(chan error) 125 go func() { 126 for scanOut.Scan() { 127 if errOut := worker.ScanOutput(ctx, ctx.Instance, scanOut.Bytes()); errOut != nil { 128 log.Debug(errOut.Error()) 129 } 130 } 131 if errs := scanOut.Err(); errs != nil { 132 log.Errorf("could not scan stdout: %s", errs) 133 } 134 waitDone <- cmd.Wait() 135 close(waitDone) 136 }() 137 138 select { 139 case err = <-waitDone: 140 case <-ctx.Done(): 141 err = ctx.Err() 142 _ = KillCmd(cmd) 143 <-waitDone 144 } 145 146 return worker.Error(ctx.Instance, err) 147 } 148 149 func commit(ctx *job.TaskContext, errjob error) error { 150 return ctx.Cookie().(execWorker).Commit(ctx, errjob) 151 } 152 153 func ctxToTimeLimit(ctx *job.TaskContext) string { 154 var limit float64 155 if deadline, ok := ctx.Deadline(); ok { 156 limit = time.Until(deadline).Seconds() 157 } 158 if limit <= 0 { 159 limit = defaultTimeout.Seconds() 160 } 161 // add a little gap of 5 seconds to prevent racing the two deadlines 162 return strconv.Itoa(int(math.Ceil(limit)) + 5) 163 } 164 165 func wrapErr(ctx context.Context, err error) error { 166 if ctx.Err() == context.DeadlineExceeded { 167 return context.DeadlineExceeded 168 } 169 return err 170 } 171 172 // MaxPayloadSizeInEnvVar is the maximal size that the COZY_PAYLOAD env 173 // variable can be. If the payload is larger, we can't put it in the env 174 // variable as the kernel as a limit for it. Instead, we put the payload in a 175 // temporary file and only gives the filename in the COZY_PAYLOAD variable. 176 const MaxPayloadSizeInEnvVar = 100000 177 178 const payloadFilename = "cozy_payload.json" 179 180 func preparePayload(ctx *job.TaskContext, workDir string) (string, error) { 181 var payload string 182 if p, err := ctx.UnmarshalPayload(); err == nil { 183 marshaled, err := json.Marshal(p) 184 if err != nil { 185 return "", err 186 } 187 payload = string(marshaled) 188 } 189 190 if len(payload) > MaxPayloadSizeInEnvVar { 191 workFS := afero.NewBasePathFs(afero.NewOsFs(), workDir) 192 f, err := workFS.OpenFile(payloadFilename, os.O_CREATE|os.O_WRONLY, 0640) 193 if err != nil { 194 return "", err 195 } 196 _, err = f.WriteString(payload) 197 errc := f.Close() 198 if err != nil { 199 return "", err 200 } 201 if errc != nil { 202 return "", errc 203 } 204 payload = "@" + payloadFilename 205 } 206 207 return payload, nil 208 }