mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-06-05 19:43:36 +02:00
Abstract task command over *exec.Cmd
Windows ConPTY can't attach a child process to a pseudoconsole via os/exec — Go's stdlib doesn't expose PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE (golang/go#62708). The ConPTY path has to call CreateProcess directly, so it can't hand an *exec.Cmd back to the task runner. Widen NewCmdTask to accept a small Cmd interface satisfied by both *exec.Cmd (via the ExecCmd adapter) and the Windows ConPTY command type we're about to add. Change TerminateProcessGracefully to take *os.Process, which both cmd shapes can provide. Behavior is unchanged on every platform. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,6 @@ package oscommands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -40,10 +39,10 @@ func (c *OSCommand) UpdateWindowTitle() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TerminateProcessGracefully(cmd *exec.Cmd) error {
|
||||
if cmd.Process == nil {
|
||||
func TerminateProcessGracefully(proc *os.Process) error {
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd.Process.Signal(syscall.SIGTERM)
|
||||
return proc.Signal(syscall.SIGTERM)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package oscommands
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
@@ -24,7 +23,7 @@ func (c *OSCommand) UpdateWindowTitle() error {
|
||||
return c.Cmd.NewShell(argString, c.UserConfig().OS.ShellFunctionsFile).Run()
|
||||
}
|
||||
|
||||
func TerminateProcessGracefully(cmd *exec.Cmd) error {
|
||||
func TerminateProcessGracefully(proc *os.Process) error {
|
||||
// Signals other than SIGKILL are not supported on Windows
|
||||
return nil
|
||||
}
|
||||
|
||||
+3
-2
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/tasks"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
@@ -75,7 +76,7 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
|
||||
manager := gui.getManager(view)
|
||||
|
||||
var ptmx *os.File
|
||||
start := func() (*exec.Cmd, io.Reader) {
|
||||
start := func() (tasks.Cmd, io.Reader) {
|
||||
var err error
|
||||
ptmx, err = pty.StartWithSize(cmd, gui.desiredPtySize(view))
|
||||
if err != nil {
|
||||
@@ -86,7 +87,7 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
|
||||
gui.viewPtmxMap[view.Name()] = ptmx
|
||||
gui.Mutexes.PtyMutex.Unlock()
|
||||
|
||||
return cmd, ptmx
|
||||
return tasks.ExecCmd{Cmd: cmd}, ptmx
|
||||
}
|
||||
|
||||
onClose := func() {
|
||||
|
||||
@@ -19,7 +19,7 @@ func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
|
||||
manager := gui.getManager(view)
|
||||
|
||||
var r io.ReadCloser
|
||||
start := func() (*exec.Cmd, io.Reader) {
|
||||
start := func() (tasks.Cmd, io.Reader) {
|
||||
var err error
|
||||
r, err = cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
@@ -32,7 +32,7 @@ func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
|
||||
gui.c.Log.Error(err)
|
||||
}
|
||||
|
||||
return cmd, r
|
||||
return tasks.ExecCmd{Cmd: cmd}, r
|
||||
}
|
||||
|
||||
onClose := func() {
|
||||
|
||||
+24
-4
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -15,6 +16,25 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Cmd abstracts over a started external process. *exec.Cmd satisfies the bulk
|
||||
// of it via ExecCmd, but pty implementations can supply their own types — on
|
||||
// Windows, ConPTY has to spawn via CreateProcess directly and can't use
|
||||
// *exec.Cmd (see golang/go#62708).
|
||||
type Cmd interface {
|
||||
Wait() error
|
||||
String() string
|
||||
GetProcess() *os.Process
|
||||
}
|
||||
|
||||
// ExecCmd adapts *exec.Cmd to Cmd.
|
||||
type ExecCmd struct {
|
||||
*exec.Cmd
|
||||
}
|
||||
|
||||
func (c ExecCmd) GetProcess() *os.Process {
|
||||
return c.Process
|
||||
}
|
||||
|
||||
// This file revolves around running commands that will be output to the main panel
|
||||
// in the gui. If we're flicking through the commits panel, we want to invoke a
|
||||
// `git show` command for each commit, but we don't want to read the entire output
|
||||
@@ -117,7 +137,7 @@ func (self *ViewBufferManager) ReadToEnd(then func()) {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), prefix string, linesToRead LinesToRead, onDoneFn func()) func(TaskOpts) error {
|
||||
func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix string, linesToRead LinesToRead, onDoneFn func()) func(TaskOpts) error {
|
||||
return func(opts TaskOpts) error {
|
||||
var onDoneOnce sync.Once
|
||||
var onFirstPageShownOnce sync.Once
|
||||
@@ -173,8 +193,8 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
|
||||
//
|
||||
// Unfortunately this will do nothing on Windows, so Windows users will have to live
|
||||
// with the higher CPU usage.
|
||||
if err := oscommands.TerminateProcessGracefully(cmd); err != nil {
|
||||
self.Log.Errorf("error when trying to terminate cmd task: %v; Command: %v %v", err, cmd.Path, cmd.Args)
|
||||
if err := oscommands.TerminateProcessGracefully(cmd.GetProcess()); err != nil {
|
||||
self.Log.Errorf("error when trying to terminate cmd task: %v; Command: %v", err, cmd.String())
|
||||
}
|
||||
|
||||
// close the task's stdout pipe (or the pty if we're using one) to make the command terminate
|
||||
@@ -316,7 +336,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
|
||||
go func() { _ = cmd.Wait() }()
|
||||
default:
|
||||
if err := cmd.Wait(); err != nil {
|
||||
self.Log.Errorf("Unexpected error when running cmd task: %v; Failed command: %v %v", err, cmd.Path, cmd.Args)
|
||||
self.Log.Errorf("Unexpected error when running cmd task: %v; Failed command: %v", err, cmd.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,13 +43,13 @@ func TestNewCmdTaskInstantStop(t *testing.T) {
|
||||
|
||||
stop := make(chan struct{})
|
||||
reader := bytes.NewBufferString("test")
|
||||
start := func() (*exec.Cmd, io.Reader) {
|
||||
start := func() (Cmd, io.Reader) {
|
||||
// not actually starting this because it's not necessary
|
||||
cmd := exec.Command("blah")
|
||||
|
||||
close(stop)
|
||||
|
||||
return cmd, reader
|
||||
return ExecCmd{Cmd: cmd}, reader
|
||||
}
|
||||
|
||||
fn := manager.NewCmdTask(start, "prefix\n", LinesToRead{20, -1, nil}, onDone)
|
||||
@@ -108,11 +108,11 @@ func TestNewCmdTask(t *testing.T) {
|
||||
|
||||
stop := make(chan struct{})
|
||||
reader := bytes.NewBufferString("test")
|
||||
start := func() (*exec.Cmd, io.Reader) {
|
||||
start := func() (Cmd, io.Reader) {
|
||||
// not actually starting this because it's not necessary
|
||||
cmd := exec.Command("blah")
|
||||
|
||||
return cmd, reader
|
||||
return ExecCmd{Cmd: cmd}, reader
|
||||
}
|
||||
|
||||
fn := manager.NewCmdTask(start, "prefix\n", LinesToRead{20, -1, nil}, onDone)
|
||||
@@ -241,11 +241,11 @@ func TestNewCmdTaskRefresh(t *testing.T) {
|
||||
|
||||
stop := make(chan struct{})
|
||||
reader := BlankLineReader{totalLinesToYield: s.totalTaskLines}
|
||||
start := func() (*exec.Cmd, io.Reader) {
|
||||
start := func() (Cmd, io.Reader) {
|
||||
// not actually starting this because it's not necessary
|
||||
cmd := exec.Command("blah")
|
||||
|
||||
return cmd, &reader
|
||||
return ExecCmd{Cmd: cmd}, &reader
|
||||
}
|
||||
|
||||
fn := manager.NewCmdTask(start, "", s.linesToRead, func() {})
|
||||
|
||||
Reference in New Issue
Block a user