github.com/dolfly/pty@v1.2.1/run_windows.go (about)

     1  //go:build windows
     2  // +build windows
     3  
     4  package pty
     5  
     6  import (
     7  	"errors"
     8  	"os"
     9  	"os/exec"
    10  	"runtime"
    11  	"syscall"
    12  	"unsafe"
    13  )
    14  
    15  type startupInfoEx struct {
    16  	startupInfo syscall.StartupInfo
    17  	lpAttrList  syscall.Handle
    18  }
    19  
    20  const (
    21  	_EXTENDED_STARTUPINFO_PRESENT = 0x00080000
    22  
    23  	_PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016
    24  )
    25  
    26  // StartWithSize assigns a pseudo-terminal Tty to c.Stdin, c.Stdout,
    27  // and c.Stderr, calls c.Start, and returns the File of the tty's
    28  // corresponding Pty.
    29  //
    30  // This will resize the Pty to the specified size before starting the command.
    31  // Starts the process in a new session and sets the controlling terminal.
    32  func StartWithSize(c *exec.Cmd, sz *Winsize) (Pty, error) {
    33  	return StartWithAttrs(c, sz, c.SysProcAttr)
    34  }
    35  
    36  // StartWithAttrs assigns a pseudo-terminal Tty to c.Stdin, c.Stdout,
    37  // and c.Stderr, calls c.Start, and returns the File of the tty's
    38  // corresponding Pty.
    39  //
    40  // This will resize the Pty to the specified size before starting the command if a size is provided.
    41  // The `attrs` parameter overrides the one set in c.SysProcAttr.
    42  //
    43  // This should generally not be needed. Used in some edge cases where it is needed to create a pty
    44  // without a controlling terminal.
    45  func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (_ Pty, err error) {
    46  	pty, tty, err := open()
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  
    51  	defer func() {
    52  		// unlike unix command exec, do not close tty unless error happened
    53  		if err != nil {
    54  			_ = tty.Close()
    55  			_ = pty.Close()
    56  		}
    57  	}()
    58  
    59  	if sz != nil {
    60  		if err = Setsize(pty, sz); err != nil {
    61  			return nil, err
    62  		}
    63  	}
    64  
    65  	// unlike unix command exec, do not set stdin/stdout/stderr
    66  
    67  	c.SysProcAttr = attrs
    68  
    69  	// do not use os/exec.Start since we need to append console handler to startup info
    70  
    71  	err = start((*cmd)(unsafe.Pointer(c)), syscall.Handle(tty.Fd()))
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	return pty, err
    77  }
    78  
    79  func createExtendedStartupInfo(consoleHandle syscall.Handle) (_ *startupInfoEx, err error) {
    80  	// append console handler to new process
    81  	var (
    82  		attrBufSize uint64
    83  		si          startupInfoEx
    84  	)
    85  
    86  	si.startupInfo.Cb = uint32(unsafe.Sizeof(si))
    87  
    88  	// get size of attr list
    89  	err = initializeProcThreadAttributeList.Find()
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	r1, _, err := initializeProcThreadAttributeList.Call(
    95  		0, // list ptr
    96  		1, // list item count
    97  		0, // dwFlags: reserved, MUST be 0
    98  		uintptr(unsafe.Pointer(&attrBufSize)),
    99  	)
   100  	if r1 == 0 {
   101  		// according to
   102  		// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-initializeprocthreadattributelist
   103  		// which says: This initial call will return an error by design. This is expected behavior.
   104  		//
   105  		// so here we check the returned value of the attr buf size, if it's zero, we cannot update attribute list
   106  		if attrBufSize == 0 {
   107  			return nil, os.NewSyscallError("InitializeProcThreadAttributeList (size)", err)
   108  		}
   109  	}
   110  
   111  	attrListBuf := make([]byte, attrBufSize)
   112  	si.lpAttrList = syscall.Handle(unsafe.Pointer(&attrListBuf[0]))
   113  	// create attr list with console handler
   114  	r1, _, err = initializeProcThreadAttributeList.Call(
   115  		uintptr(si.lpAttrList),                // attr list buf
   116  		1,                                     // list item count
   117  		0,                                     // dwFlags: reserved, MUST be 0
   118  		uintptr(unsafe.Pointer(&attrBufSize)), // size of the list
   119  	)
   120  	if r1 == 0 {
   121  		// false
   122  		return nil, os.NewSyscallError("InitializeProcThreadAttributeList (create)", err)
   123  	}
   124  
   125  	err = updateProcThreadAttribute.Find()
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	r1, _, err = updateProcThreadAttribute.Call(
   131  		uintptr(si.lpAttrList), // buf list
   132  		0,                      // dwFlags: reserved, MUST be 0
   133  		_PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
   134  		uintptr(consoleHandle),
   135  		unsafe.Sizeof(consoleHandle),
   136  		0,
   137  		0,
   138  	)
   139  	if r1 == 0 {
   140  		// false
   141  		if deleteProcThreadAttributeList.Find() == nil {
   142  			_, _, _ = deleteProcThreadAttributeList.Call(uintptr(si.lpAttrList))
   143  		}
   144  		return nil, os.NewSyscallError("UpdateProcThreadAttribute", err)
   145  	}
   146  
   147  	return &si, nil
   148  }
   149  
   150  // copied from os/exec.(*Cmd).Start
   151  // start starts the specified command but does not wait for it to complete.
   152  //
   153  // If Start returns successfully, the c.Process field will be set.
   154  //
   155  // The Wait method will return the exit code and release associated resources
   156  // once the command exits.
   157  func start(c *cmd, consoleHandle syscall.Handle) error {
   158  	if c.lookPathErr != nil {
   159  		_cmd_closeDescriptors(c, c.closeAfterStart)
   160  		_cmd_closeDescriptors(c, c.closeAfterWait)
   161  		return c.lookPathErr
   162  	}
   163  	if runtime.GOOS == "windows" {
   164  		lp, err := lookExtensions(c.Path, c.Dir)
   165  		if err != nil {
   166  			_cmd_closeDescriptors(c, c.closeAfterStart)
   167  			_cmd_closeDescriptors(c, c.closeAfterWait)
   168  			return err
   169  		}
   170  		c.Path = lp
   171  	}
   172  	if c.Process != nil {
   173  		return errors.New("exec: already started")
   174  	}
   175  	if c.ctx != nil {
   176  		select {
   177  		case <-c.ctx.Done():
   178  			_cmd_closeDescriptors(c, c.closeAfterStart)
   179  			_cmd_closeDescriptors(c, c.closeAfterWait)
   180  			return c.ctx.Err()
   181  		default:
   182  		}
   183  	}
   184  
   185  	//c.childFiles = make([]*os.File, 0, 3+len(c.ExtraFiles))
   186  	//type F func() (*os.File, error)
   187  	//for _, setupFd := range []F{c.stdin, c.stdout, c.stderr} {
   188  	//	fd, err := setupFd()
   189  	//	if err != nil {
   190  	//		closeDescriptors(c, c.closeAfterStart)
   191  	//		closeDescriptors(c, c.closeAfterWait)
   192  	//		return err
   193  	//	}
   194  	//	c.childFiles = append(c.childFiles, fd)
   195  	//}
   196  	//c.childFiles = append(c.childFiles, c.ExtraFiles...)
   197  
   198  	envv, err := _cmd_envv(c)
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	c.Process, err = startProcess(c.Path, _cmd_argv(c), &os.ProcAttr{
   204  		Dir:   c.Dir,
   205  		Files: c.childFiles,
   206  		Env:   addCriticalEnv(dedupEnv(envv)),
   207  		Sys:   c.SysProcAttr,
   208  	}, consoleHandle)
   209  	if err != nil {
   210  		_cmd_closeDescriptors(c, c.closeAfterStart)
   211  		_cmd_closeDescriptors(c, c.closeAfterWait)
   212  		return err
   213  	}
   214  
   215  	_cmd_closeDescriptors(c, c.closeAfterStart)
   216  
   217  	// Don't allocate the channel unless there are goroutines to fire.
   218  	if len(c.goroutine) > 0 {
   219  		c.errch = make(chan error, len(c.goroutine))
   220  		for _, fn := range c.goroutine {
   221  			go func(fn func() error) {
   222  				c.errch <- fn()
   223  			}(fn)
   224  		}
   225  	}
   226  
   227  	if c.ctx != nil {
   228  		c.waitDone = make(chan struct{})
   229  		go func() {
   230  			select {
   231  			case <-c.ctx.Done():
   232  				_ = c.Process.Kill()
   233  			case <-c.waitDone:
   234  			}
   235  		}()
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  // copied from os.startProcess, add consoleHandle arg
   242  func startProcess(name string, argv []string, attr *os.ProcAttr, consoleHandle syscall.Handle) (p *os.Process, err error) {
   243  	// If there is no SysProcAttr (ie. no Chroot or changed
   244  	// UID/GID), double-check existence of the directory we want
   245  	// to chdir into. We can make the error clearer this way.
   246  	if attr != nil && attr.Sys == nil && attr.Dir != "" {
   247  		if _, err := os.Stat(attr.Dir); err != nil {
   248  			pe := err.(*os.PathError)
   249  			pe.Op = "chdir"
   250  			return nil, pe
   251  		}
   252  	}
   253  
   254  	sysattr := &syscall.ProcAttr{
   255  		Dir: attr.Dir,
   256  		Env: attr.Env,
   257  		Sys: attr.Sys,
   258  	}
   259  	if sysattr.Env == nil {
   260  		sysattr.Env, err = execEnvDefault(sysattr.Sys)
   261  		if err != nil {
   262  			return nil, err
   263  		}
   264  	}
   265  	sysattr.Files = make([]uintptr, 0, len(attr.Files))
   266  	for _, f := range attr.Files {
   267  		sysattr.Files = append(sysattr.Files, f.Fd())
   268  	}
   269  
   270  	pid, h, e := syscallStartProcess(name, argv, sysattr, consoleHandle)
   271  
   272  	// Make sure we don't run the finalizers of attr.Files.
   273  	runtime.KeepAlive(attr)
   274  
   275  	if e != nil {
   276  		return nil, &os.PathError{Op: "fork/exec", Path: name, Err: e}
   277  	}
   278  
   279  	return newProcess(pid, h), nil
   280  }
   281  
   282  //go:linkname zeroProcAttr syscall.zeroProcAttr
   283  var zeroProcAttr syscall.ProcAttr
   284  
   285  //go:linkname zeroSysProcAttr syscall.zeroSysProcAttr
   286  var zeroSysProcAttr syscall.SysProcAttr
   287  
   288  // copied from syscall.StartProcess, add consoleHandle arg
   289  func syscallStartProcess(argv0 string, argv []string, attr *syscall.ProcAttr, consoleHandle syscall.Handle) (pid int, handle uintptr, err error) {
   290  	if len(argv0) == 0 {
   291  		return 0, 0, syscall.EWINDOWS
   292  	}
   293  	if attr == nil {
   294  		attr = &zeroProcAttr
   295  	}
   296  	sys := attr.Sys
   297  	if sys == nil {
   298  		sys = &zeroSysProcAttr
   299  	}
   300  
   301  	//if len(attr.Files) > 3 {
   302  	//	return 0, 0, syscall.EWINDOWS
   303  	//}
   304  	//if len(attr.Files) < 3 {
   305  	//	return 0, 0, syscall.EINVAL
   306  	//}
   307  
   308  	if len(attr.Dir) != 0 {
   309  		// StartProcess assumes that argv0 is relative to attr.Dir,
   310  		// because it implies Chdir(attr.Dir) before executing argv0.
   311  		// Windows CreateProcess assumes the opposite: it looks for
   312  		// argv0 relative to the current directory, and, only once the new
   313  		// process is started, it does Chdir(attr.Dir). We are adjusting
   314  		// for that difference here by making argv0 absolute.
   315  		var err error
   316  		argv0, err = joinExeDirAndFName(attr.Dir, argv0)
   317  		if err != nil {
   318  			return 0, 0, err
   319  		}
   320  	}
   321  	argv0p, err := syscall.UTF16PtrFromString(argv0)
   322  	if err != nil {
   323  		return 0, 0, err
   324  	}
   325  
   326  	var cmdline string
   327  	// Windows CreateProcess takes the command line as a single string:
   328  	// use attr.CmdLine if set, else build the command line by escaping
   329  	// and joining each argument with spaces
   330  	if sys.CmdLine != "" {
   331  		cmdline = sys.CmdLine
   332  	} else {
   333  		cmdline = makeCmdLine(argv)
   334  	}
   335  
   336  	var argvp *uint16
   337  	if len(cmdline) != 0 {
   338  		argvp, err = syscall.UTF16PtrFromString(cmdline)
   339  		if err != nil {
   340  			return 0, 0, err
   341  		}
   342  	}
   343  
   344  	var dirp *uint16
   345  	if len(attr.Dir) != 0 {
   346  		dirp, err = syscall.UTF16PtrFromString(attr.Dir)
   347  		if err != nil {
   348  			return 0, 0, err
   349  		}
   350  	}
   351  
   352  	// Acquire the fork lock so that no other threads
   353  	// create new fds that are not yet close-on-exec
   354  	// before we fork.
   355  	syscall.ForkLock.Lock()
   356  	defer syscall.ForkLock.Unlock()
   357  
   358  	//p, _ := syscall.GetCurrentProcess()
   359  	//fd := make([]syscall.Handle, len(attr.Files))
   360  	//for i := range attr.Files {
   361  	//	if attr.Files[i] > 0 {
   362  	//		err := syscall.DuplicateHandle(p, syscall.Handle(attr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS)
   363  	//		if err != nil {
   364  	//			return 0, 0, err
   365  	//		}
   366  	//		defer syscall.CloseHandle(syscall.Handle(fd[i]))
   367  	//	}
   368  	//}
   369  
   370  	// replaced default syscall.StartupInfo with custom startupInfEx for console handle
   371  	//si := new(syscall.StartupInfo)
   372  	//si.Cb = uint32(unsafe.Sizeof(*si))
   373  	si, err := createExtendedStartupInfo(consoleHandle)
   374  	if err != nil {
   375  		return 0, 0, err
   376  	}
   377  	// add finalizer for attribute list cleanup, best effort
   378  	runtime.SetFinalizer(si, func(si *startupInfoEx) {
   379  		if deleteProcThreadAttributeList.Find() == nil {
   380  			_, _, _ = deleteProcThreadAttributeList.Call(uintptr(si.lpAttrList))
   381  		}
   382  	})
   383  
   384  	si.startupInfo.Flags = syscall.STARTF_USESTDHANDLES
   385  	if sys.HideWindow {
   386  		si.startupInfo.Flags |= syscall.STARTF_USESHOWWINDOW
   387  		si.startupInfo.ShowWindow = syscall.SW_HIDE
   388  	}
   389  	//si.StdInput = fd[0]
   390  	//si.StdOutput = fd[1]
   391  	//si.StdErr = fd[2]
   392  
   393  	pi := new(syscall.ProcessInformation)
   394  
   395  	flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT
   396  
   397  	// add startupInfoEx flag
   398  	flags = flags | _EXTENDED_STARTUPINFO_PRESENT
   399  
   400  	// ignore security attrs since both Process and Thread handles are not inheritable for conPty
   401  	if sys.Token != 0 {
   402  		err = syscall.CreateProcessAsUser(sys.Token, argv0p, argvp, nil, nil, false, flags, createEnvBlock(attr.Env), dirp, &si.startupInfo, pi)
   403  	} else {
   404  		err = syscall.CreateProcess(argv0p, argvp, nil, nil, false, flags, createEnvBlock(attr.Env), dirp, &si.startupInfo, pi)
   405  	}
   406  	if err != nil {
   407  		return 0, 0, err
   408  	}
   409  	defer syscall.CloseHandle(syscall.Handle(pi.Thread))
   410  
   411  	return int(pi.ProcessId), uintptr(pi.Process), nil
   412  }