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 }