/ components / execd / pkg / runtime / command_windows.go
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  }