command_windows.go
1 // Copyright 2025 Alibaba Group Holding Ltd. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build windows 16 // +build windows 17 18 package runtime 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 "os" 25 "os/exec" 26 "strconv" 27 "time" 28 29 "github.com/alibaba/opensandbox/execd/pkg/jupyter/execute" 30 "github.com/alibaba/opensandbox/execd/pkg/log" 31 "github.com/alibaba/opensandbox/execd/pkg/util/pathutil" 32 "github.com/alibaba/opensandbox/internal/safego" 33 ) 34 35 // runCommand executes shell commands and streams their output on Windows. 36 func (c *Controller) runCommand(ctx context.Context, request *ExecuteCodeRequest) error { 37 session := c.newContextID() 38 request.Hooks.OnExecuteInit(session) 39 40 stdout, stderr, err := c.stdLogDescriptor(session) 41 if err != nil { 42 return fmt.Errorf("failed to get stdlog descriptor: %w", err) 43 } 44 45 startAt := time.Now() 46 log.Info("received command: %v", request.Code) 47 cmd := exec.CommandContext(ctx, "cmd", "/C", request.Code) 48 extraEnv := mergeExtraEnvs(loadExtraEnvFromFile(), request.Envs) 49 cwd, err := pathutil.ExpandPathWithEnv(request.Cwd, extraEnv) 50 if err != nil { 51 return fmt.Errorf("resolve cwd: %w", err) 52 } 53 54 cmd.Stdout = stdout 55 cmd.Stderr = stderr 56 cmd.Dir = cwd 57 cmd.Env = mergeEnvs(os.Environ(), extraEnv) 58 59 done := make(chan struct{}, 1) 60 safego.Go(func() { 61 c.tailStdPipe(c.stdoutFileName(session), request.Hooks.OnExecuteStdout, done) 62 }) 63 safego.Go(func() { 64 c.tailStdPipe(c.stderrFileName(session), request.Hooks.OnExecuteStderr, done) 65 }) 66 67 err = cmd.Start() 68 if err != nil { 69 request.Hooks.OnExecuteError(&execute.ErrorOutput{EName: "CommandExecError", EValue: err.Error()}) 70 log.Error("CommandExecError: error starting commands: %v", err) 71 return nil 72 } 73 74 kernel := &commandKernel{ 75 pid: cmd.Process.Pid, 76 content: request.Code, 77 isBackground: false, 78 } 79 c.storeCommandKernel(session, kernel) 80 81 err = cmd.Wait() 82 close(done) 83 if err != nil { 84 var eName, eValue string 85 var traceback []string 86 87 var exitError *exec.ExitError 88 if errors.As(err, &exitError) { 89 exitCode := exitError.ExitCode() 90 eName = "CommandExecError" 91 eValue = strconv.Itoa(exitCode) 92 } else { 93 eName = "CommandExecError" 94 eValue = err.Error() 95 } 96 traceback = []string{err.Error()} 97 98 request.Hooks.OnExecuteError(&execute.ErrorOutput{ 99 EName: eName, 100 EValue: eValue, 101 Traceback: traceback, 102 }) 103 104 log.Error("CommandExecError: error running commands: %v", err) 105 return nil 106 } 107 request.Hooks.OnExecuteComplete(time.Since(startAt)) 108 return nil 109 } 110 111 // runBackgroundCommand executes shell commands in detached mode on Windows. 112 func (c *Controller) runBackgroundCommand(ctx context.Context, cancel context.CancelFunc, request *ExecuteCodeRequest) error { 113 session := c.newContextID() 114 request.Hooks.OnExecuteInit(session) 115 116 pipe, err := c.combinedOutputDescriptor(session) 117 if err != nil { 118 return fmt.Errorf("failed to get combined output descriptor: %w", err) 119 } 120 stdoutPath := c.combinedOutputFileName(session) 121 stderrPath := c.combinedOutputFileName(session) 122 123 startAt := time.Now() 124 log.Info("received command: %v", request.Code) 125 cmd := exec.CommandContext(ctx, "cmd", "/C", request.Code) 126 extraEnv := mergeExtraEnvs(loadExtraEnvFromFile(), request.Envs) 127 cwd, err := pathutil.ExpandPathWithEnv(request.Cwd, extraEnv) 128 if err != nil { 129 return fmt.Errorf("resolve cwd: %w", err) 130 } 131 132 cmd.Dir = cwd 133 cmd.Stdout = pipe 134 cmd.Stderr = pipe 135 cmd.Env = mergeEnvs(os.Environ(), extraEnv) 136 137 devNull, _ := os.OpenFile(os.DevNull, os.O_RDWR, 0) // best-effort, ignore error 138 cmd.Stdin = devNull 139 140 safego.Go(func() { 141 err := cmd.Start() 142 if err != nil { 143 log.Error("CommandExecError: error starting commands: %v", err) 144 pipe.Close() // best-effort 145 cancel() 146 return 147 } 148 149 kernel := &commandKernel{ 150 pid: cmd.Process.Pid, 151 content: request.Code, 152 stdoutPath: stdoutPath, 153 stderrPath: stderrPath, 154 startedAt: startAt, 155 running: true, 156 isBackground: true, 157 } 158 c.storeCommandKernel(session, kernel) 159 160 safego.Go(func() { 161 <-ctx.Done() 162 if cmd.Process != nil { 163 _ = cmd.Process.Kill() // best-effort 164 } 165 }) 166 167 err = cmd.Wait() 168 cancel() 169 pipe.Close() // best-effort 170 devNull.Close() // best-effort 171 172 if err != nil { 173 log.Error("CommandExecError: error running commands: %v", err) 174 exitCode := 1 175 var exitError *exec.ExitError 176 if errors.As(err, &exitError) { 177 exitCode = exitError.ExitCode() 178 } 179 c.markCommandFinished(session, exitCode, err.Error()) 180 return 181 } 182 c.markCommandFinished(session, 0, "") 183 }) 184 185 request.Hooks.OnExecuteComplete(time.Since(startAt)) 186 return nil 187 }