1
0
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:
Stefan Haller
2026-04-21 22:00:53 +02:00
parent e470d15bb7
commit ee011eff2a
6 changed files with 39 additions and 20 deletions
@@ -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)
}
+1 -2
View File
@@ -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
View File
@@ -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() {
+2 -2
View File
@@ -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
View File
@@ -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())
}
}
+6 -6
View File
@@ -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() {})