2021-12-29 14:33:38 +11:00
package oscommands
import (
"bufio"
2022-01-09 12:56:29 +11:00
"bytes"
"io"
"regexp"
"strings"
2023-07-29 09:31:42 +10:00
"time"
2021-12-29 14:33:38 +11:00
"github.com/go-errors/errors"
2023-07-09 11:32:27 +10:00
"github.com/jesseduffield/gocui"
2021-12-29 14:33:38 +11:00
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type ICmdObjRunner interface {
Run ( cmdObj ICmdObj ) error
RunWithOutput ( cmdObj ICmdObj ) ( string , error )
2022-08-02 08:32:28 +09:00
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
}
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 {
2022-01-16 14:46:53 +11:00
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 )
}
2022-01-16 14:46:53 +11:00
_ , 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 ) {
2022-01-16 14:46:53 +11:00
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
}
2022-01-16 14:46:53 +11:00
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
}
2022-01-16 14:46:53 +11:00
return self . RunWithOutputAux ( cmdObj )
}
2022-08-02 08:32:28 +09:00
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 )
}
2022-01-16 14:46:53 +11:00
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 )
}
2023-07-29 09:31:42 +10:00
t := time . Now ( )
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 )
}
2023-07-29 09:31:42 +10:00
self . log . Infof ( "%s (%s)" , cmdObj . ToString ( ) , time . Since ( t ) )
2021-12-29 14:33:38 +11:00
return output , err
}
2022-08-02 08:32:28 +09:00
func ( self * cmdObjRunner ) RunWithOutputsAux ( cmdObj ICmdObj ) ( string , string , error ) {
self . log . WithField ( "command" , cmdObj . ToString ( ) ) . Debug ( "RunCommand" )
if cmdObj . ShouldLog ( ) {
self . logCmdObj ( cmdObj )
}
2023-07-29 09:31:42 +10:00
t := time . Now ( )
2022-08-02 08:32:28 +09:00
var outBuffer , errBuffer bytes . Buffer
cmd := cmdObj . GetCmd ( )
cmd . Stdout = & outBuffer
cmd . Stderr = & errBuffer
err := cmd . Run ( )
2023-07-29 09:31:42 +10:00
self . log . Infof ( "%s (%s)" , cmdObj . ToString ( ) , time . Since ( t ) )
2022-08-02 08:32:28 +09:00
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 {
2022-01-16 14:46:53 +11:00
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 )
}
2023-07-29 09:31:42 +10:00
t := time . Now ( )
2022-01-05 11:57:32 +11:00
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 ( )
2023-07-29 09:31:42 +10:00
self . log . Infof ( "%s (%s)" , cmdObj . ToString ( ) , time . Since ( t ) )
2021-12-29 14:33:38 +11:00
return nil
}
2022-01-09 12:56:29 +11:00
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 )
} ( )
} )
}
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 )
}
2022-08-01 20:04:22 +10:00
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
}
2022-03-27 14:28:30 +11:00
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 )
}
} ( )
2023-07-29 09:31:42 +10:00
t := time . Now ( )
2022-01-19 18:32:27 +11:00
onRun ( handler , cmdWriter )
2022-01-09 12:56:29 +11:00
err = cmd . Wait ( )
2023-07-29 09:31:42 +10:00
self . log . Infof ( "%s (%s)" , cmdObj . ToString ( ) , time . Since ( t ) )
2022-01-09 12:56:29 +11:00
if err != nil {
2022-01-19 18:32:27 +11:00
errStr := stderr . String ( )
2022-03-27 14:28:30 +11:00
if errStr != "" {
return errors . New ( errStr )
}
if cmdObj . ShouldIgnoreEmptyError ( ) {
2022-01-19 18:32:27 +11:00
return nil
}
2023-07-10 17:11:22 +10:00
stdoutStr := stdout . String ( )
if stdoutStr != "" {
return errors . New ( stdoutStr )
}
return errors . New ( "Command exited with non-zero exit code, but no output" )
2022-01-09 12:56:29 +11:00
}
return nil
}
2023-07-08 14:17:54 +10:00
type CredentialType int
const (
Password CredentialType = iota
Username
Passphrase
PIN
)
// Whenever we're asked for a password we just enter a newline, which will
// eventually cause the command to fail.
var failPromptFn = func ( CredentialType ) <- chan string {
ch := make ( chan string )
go func ( ) {
ch <- "\n"
} ( )
return ch
}
func ( self * cmdObjRunner ) runWithCredentialHandling ( cmdObj ICmdObj ) error {
promptFn , err := self . getCredentialPromptFn ( cmdObj )
if err != nil {
return err
}
return self . runAndDetectCredentialRequest ( cmdObj , promptFn )
}
func ( self * cmdObjRunner ) getCredentialPromptFn ( cmdObj ICmdObj ) ( func ( CredentialType ) <- chan string , error ) {
switch cmdObj . GetCredentialStrategy ( ) {
case PROMPT :
return self . guiIO . promptForCredentialFn , nil
case FAIL :
return failPromptFn , nil
default :
// we should never land here
return nil , errors . New ( "runWithCredentialHandling called but cmdObj does not have a credential strategy" )
}
}
// 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 ) <- chan string ,
) 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 )
2023-07-24 21:09:59 +10:00
go utils . Safe ( func ( ) {
self . processOutput ( tr , handler . stdinPipe , promptUserForCredential , cmdObj . GetTask ( ) )
} )
2023-07-08 14:17:54 +10:00
} )
}
func ( self * cmdObjRunner ) processOutput (
reader io . Reader ,
writer io . Writer ,
promptUserForCredential func ( CredentialType ) <- chan string ,
2023-07-09 21:09:52 +10:00
task gocui . Task ,
2023-07-08 14:17:54 +10:00
) {
2022-01-09 12:56:29 +11:00
checkForCredentialRequest := self . getCheckForCredentialRequestFunc ( )
scanner := bufio . NewScanner ( reader )
scanner . Split ( bufio . ScanBytes )
for scanner . Scan ( ) {
newBytes := scanner . Bytes ( )
askFor , ok := checkForCredentialRequest ( newBytes )
if ok {
2023-07-08 14:17:54 +10:00
responseChan := promptUserForCredential ( askFor )
2023-07-18 18:53:35 +02:00
if task != nil {
task . Pause ( )
}
2023-07-08 14:17:54 +10:00
toInput := <- responseChan
2023-07-18 18:53:35 +02:00
if task != nil {
task . Continue ( )
}
2022-01-09 12:56:29 +11:00
// 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
2023-03-19 01:08:54 +01:00
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 ,
` Enter\s*PIN\s*for\s*.+\s*key\s*.+: ` : PIN ,
}
compiledPrompts := map [ * regexp . Regexp ] CredentialType { }
for pattern , askFor := range prompts {
compiledPattern := regexp . MustCompile ( pattern )
compiledPrompts [ compiledPattern ] = askFor
}
newlineRegex := regexp . MustCompile ( "\n" )
2022-01-09 12:56:29 +11:00
// 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 )
}
2023-03-19 01:08:54 +01:00
for pattern , askFor := range compiledPrompts {
if match := pattern . Match ( [ ] byte ( ttyText . String ( ) ) ) ; match {
2022-01-09 12:56:29 +11:00
ttyText . Reset ( )
return askFor , true
}
}
2023-03-19 01:08:54 +01:00
if indices := newlineRegex . FindIndex ( [ ] byte ( ttyText . String ( ) ) ) ; indices != nil {
newText := [ ] byte ( ttyText . String ( ) [ indices [ 1 ] : ] )
ttyText . Reset ( )
ttyText . Write ( newText )
}
2022-01-09 12:56:29 +11:00
return 0 , false
}
}