1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-18 05:17:55 +02:00
lazygit/pkg/commands/oscommands/cmd_obj_runner.go

352 lines
9.4 KiB
Go
Raw Normal View History

2021-12-29 14:33:38 +11:00
package oscommands
import (
"bufio"
2022-01-09 12:56:29 +11:00
"bytes"
"io"
"regexp"
"strings"
2021-12-29 14:33:38 +11:00
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type ICmdObjRunner interface {
Run(cmdObj ICmdObj) error
RunWithOutput(cmdObj ICmdObj) (string, error)
RunWithOutputs(cmdObj ICmdObj) (string, string, error)
2021-12-30 11:22:29 +11:00
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
2021-12-29 14:33:38 +11:00
}
2022-01-09 12:56:29 +11:00
type CredentialType int
const (
Password CredentialType = iota
Username
Passphrase
Add credential prompts for U2F-backed SSH keys The 8.2 release of OpenSSH added support for FIDO/U2F hardware authenticators, which manifests in being able to create new types of SSH key, named `ecdsa-sk` nad `ed25519-sk`. This is relevant to lazygit, as those SSH keys can be used to authorise git operations over SSH, as well as signing git commits. Actual code changes are required for correct support, as the authentication process for these types of keys is different than the process for types supported previously. When an operation requiring credentials is initialised with a U2F authenticator-backed key, the first prompt is: Enter PIN for ${key_type} key ${path_to_key}: at which point the user is supposed to enter a numeric (and secret) PIN, specific to the particular FIDO/U2F authenticator using which the SSH keypair was generated. Upon entering the correct key, the user is supposed to physically interact with the authenticator to confirm presence. Sometimes this is accompanied by the following text prompt: Confirm user presence for key ${key_type} ${key_fingerprint} This second prompt does not always occur and it is presumed that the user will know to perform this step even if not prompted specifically. At this stage some authenticator devices may also begin to blink a LED to indicate that they're waiting for input. To facilitate lazygit's interoperability with these types of keys, add support for the first PIN prompt, which allows "fetch", "pull", and "push" git operations to complete.
2022-10-31 22:12:47 +01:00
PIN
2022-01-09 12:56:29 +11:00
)
2021-12-30 13:11:58 +11:00
type cmdObjRunner struct {
2022-01-02 10:34:33 +11:00
log *logrus.Entry
guiIO *guiIO
2021-12-29 14:33:38 +11:00
}
2021-12-30 13:11:58 +11:00
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
2022-01-19 18:32:27 +11:00
if cmdObj.GetCredentialStrategy() != NONE {
2022-01-02 10:34:33 +11:00
return self.runWithCredentialHandling(cmdObj)
}
2022-01-19 18:32:27 +11:00
if cmdObj.ShouldStreamOutput() {
return self.runAndStream(cmdObj)
}
_, err := self.RunWithOutputAux(cmdObj)
2022-01-19 18:32:27 +11:00
return err
2022-01-02 10:34:33 +11:00
}
2021-12-30 13:11:58 +11:00
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE {
err := self.runWithCredentialHandling(cmdObj)
2022-01-19 18:32:27 +11:00
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
}
if cmdObj.ShouldStreamOutput() {
err := self.runAndStream(cmdObj)
2022-01-02 10:34:33 +11:00
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
}
return self.RunWithOutputAux(cmdObj)
}
func (self *cmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE {
err := self.runWithCredentialHandling(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutputs
// but ignore the output, hence why we've got this check here.
return "", "", err
}
if cmdObj.ShouldStreamOutput() {
err := self.runAndStream(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutputs
// but ignore the output, hence why we've got this check here.
return "", "", err
}
return self.RunWithOutputsAux(cmdObj)
}
func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
2022-01-15 12:04:00 +11:00
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
2022-01-05 11:57:32 +11:00
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
2021-12-29 14:33:38 +11:00
output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
if err != nil {
self.log.WithField("command", cmdObj.ToString()).Error(output)
}
return output, err
}
func (self *cmdObjRunner) RunWithOutputsAux(cmdObj ICmdObj) (string, string, error) {
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
var outBuffer, errBuffer bytes.Buffer
cmd := cmdObj.GetCmd()
cmd.Stdout = &outBuffer
cmd.Stderr = &errBuffer
err := cmd.Run()
stdout := outBuffer.String()
stderr, err := sanitisedCommandOutput(errBuffer.Bytes(), err)
if err != nil {
self.log.WithField("command", cmdObj.ToString()).Error(stderr)
}
return stdout, stderr, err
}
2021-12-30 13:11:58 +11:00
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
2022-01-02 10:34:33 +11:00
if cmdObj.GetCredentialStrategy() != NONE {
return errors.New("cannot call RunAndProcessLines with credential strategy. If you're seeing this then a contributor to Lazygit has accidentally called this method! Please raise an issue")
}
2022-01-05 11:57:32 +11:00
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
2021-12-29 14:33:38 +11:00
cmd := cmdObj.GetCmd()
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
2022-06-18 13:28:20 +10:00
_ = Kill(cmd)
2021-12-29 14:33:38 +11:00
break
}
}
_ = cmd.Wait()
return nil
}
2022-01-09 12:56:29 +11:00
// Whenever we're asked for a password we just enter a newline, which will
// eventually cause the command to fail.
var failPromptFn = func(CredentialType) string { return "\n" }
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
var promptFn func(CredentialType) string
switch cmdObj.GetCredentialStrategy() {
case PROMPT:
promptFn = self.guiIO.promptForCredentialFn
case FAIL:
promptFn = failPromptFn
case NONE:
// we should never land here
return errors.New("runWithCredentialHandling called but cmdObj does not have a credential strategy")
2022-01-09 12:56:29 +11:00
}
return self.runAndDetectCredentialRequest(cmdObj, promptFn)
}
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
self.guiIO.logCommandFn(cmdObj.ToString(), true)
}
2021-12-29 14:33:38 +11:00
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", utils.WrapError(err)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}
2022-01-09 12:56:29 +11:00
type cmdHandler struct {
stdoutPipe io.Reader
stdinPipe io.Writer
close func() error
}
2022-01-19 18:32:27 +11:00
func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error {
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
go func() {
_, _ = io.Copy(cmdWriter, handler.stdoutPipe)
}()
})
}
2022-01-09 12:56:29 +11:00
// runAndDetectCredentialRequest detect a username / password / passphrase question in a command
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (self *cmdObjRunner) runAndDetectCredentialRequest(
cmdObj ICmdObj,
promptUserForCredential func(CredentialType) string,
2022-01-19 18:32:27 +11:00
) error {
// setting the output to english so we can parse it for a username/password request
cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
go utils.Safe(func() {
self.processOutput(tr, handler.stdinPipe, promptUserForCredential)
})
})
}
func (self *cmdObjRunner) runAndStreamAux(
cmdObj ICmdObj,
onRun func(*cmdHandler, io.Writer),
2022-01-09 12:56:29 +11:00
) error {
2022-02-22 21:16:00 +11:00
// if we're streaming this we don't want any fancy terminal stuff
cmdObj.AddEnvVars("TERM=dumb")
2022-01-09 12:56:29 +11:00
cmdWriter := self.guiIO.newCmdWriterFn()
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
2022-01-19 18:32:27 +11:00
cmd := cmdObj.GetCmd()
2022-01-09 12:56:29 +11:00
var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
handler, err := self.getCmdHandler(cmd)
if err != nil {
return err
}
var stdout bytes.Buffer
handler.stdoutPipe = io.TeeReader(handler.stdoutPipe, &stdout)
2022-01-09 12:56:29 +11:00
defer func() {
if closeErr := handler.close(); closeErr != nil {
self.log.Error(closeErr)
}
}()
2022-01-19 18:32:27 +11:00
onRun(handler, cmdWriter)
2022-01-09 12:56:29 +11:00
err = cmd.Wait()
if err != nil {
2022-01-19 18:32:27 +11:00
errStr := stderr.String()
if errStr != "" {
return errors.New(errStr)
}
if cmdObj.ShouldIgnoreEmptyError() {
2022-01-19 18:32:27 +11:00
return nil
}
return errors.New(stdout.String())
2022-01-09 12:56:29 +11:00
}
return nil
}
func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, promptUserForCredential func(CredentialType) string) {
checkForCredentialRequest := self.getCheckForCredentialRequestFunc()
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanBytes)
for scanner.Scan() {
newBytes := scanner.Bytes()
askFor, ok := checkForCredentialRequest(newBytes)
if ok {
toInput := promptUserForCredential(askFor)
// If the return data is empty we don't write anything to stdin
if toInput != "" {
_, _ = writer.Write([]byte(toInput))
}
}
}
}
// having a function that returns a function because we need to maintain some state inbetween calls hence the closure
func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (CredentialType, bool) {
var ttyText strings.Builder
// this function takes each word of output from the command and builds up a string to see if we're being asked for a password
return func(newBytes []byte) (CredentialType, bool) {
_, err := ttyText.Write(newBytes)
if err != nil {
self.log.Error(err)
}
prompts := map[string]CredentialType{
`Password:`: Password,
`.+'s password:`: Password,
`Password\s*for\s*'.+':`: Password,
`Username\s*for\s*'.+':`: Username,
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
Add credential prompts for U2F-backed SSH keys The 8.2 release of OpenSSH added support for FIDO/U2F hardware authenticators, which manifests in being able to create new types of SSH key, named `ecdsa-sk` nad `ed25519-sk`. This is relevant to lazygit, as those SSH keys can be used to authorise git operations over SSH, as well as signing git commits. Actual code changes are required for correct support, as the authentication process for these types of keys is different than the process for types supported previously. When an operation requiring credentials is initialised with a U2F authenticator-backed key, the first prompt is: Enter PIN for ${key_type} key ${path_to_key}: at which point the user is supposed to enter a numeric (and secret) PIN, specific to the particular FIDO/U2F authenticator using which the SSH keypair was generated. Upon entering the correct key, the user is supposed to physically interact with the authenticator to confirm presence. Sometimes this is accompanied by the following text prompt: Confirm user presence for key ${key_type} ${key_fingerprint} This second prompt does not always occur and it is presumed that the user will know to perform this step even if not prompted specifically. At this stage some authenticator devices may also begin to blink a LED to indicate that they're waiting for input. To facilitate lazygit's interoperability with these types of keys, add support for the first PIN prompt, which allows "fetch", "pull", and "push" git operations to complete.
2022-10-31 22:12:47 +01:00
`Enter\s*PIN\s*for\s*.+\s*key\s*.+:`: PIN,
2022-01-09 12:56:29 +11:00
}
for pattern, askFor := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText.String()); match {
ttyText.Reset()
return askFor, true
}
}
return 0, false
}
}