summaryrefslogtreecommitdiff
path: root/src/proxy.go
blob: d53805b1ac94ffd13ac699d0dc81c518497e4017 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package fzf

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"strings"
	"time"

	"github.com/junegunn/fzf/src/tui"
	"github.com/junegunn/fzf/src/util"
)

const becomeSuffix = ".become"

func escapeSingleQuote(str string) string {
	return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
}

func fifo(name string) (string, error) {
	ns := time.Now().UnixNano()
	output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
	output, err := mkfifo(output, 0600)
	if err != nil {
		return output, err
	}
	return output, nil
}

func runProxy(commandPrefix string, cmdBuilder func(temp string) *exec.Cmd, opts *Options, withExports bool) (int, error) {
	output, err := fifo("proxy-output")
	if err != nil {
		return ExitError, err
	}
	defer os.Remove(output)

	// Take the output
	go func() {
		withOutputPipe(output, func(outputFile io.ReadCloser) {
			if opts.Output == nil {
				io.Copy(os.Stdout, outputFile)
			} else {
				reader := bufio.NewReader(outputFile)
				sep := opts.PrintSep[0]
				for {
					item, err := reader.ReadString(sep)
					if err != nil {
						break
					}
					opts.Output <- item
				}
			}
		})
	}()

	var command string
	commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
	if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
		command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
	} else {
		input, err := fifo("proxy-input")
		if err != nil {
			return ExitError, err
		}
		defer os.Remove(input)

		go func() {
			withInputPipe(input, func(inputFile io.WriteCloser) {
				if opts.Input == nil {
					io.Copy(inputFile, os.Stdin)
				} else {
					for item := range opts.Input {
						fmt.Fprint(inputFile, item+opts.PrintSep)
					}
				}
			})
		}()

		if withExports {
			command = fmt.Sprintf(`%s < %q > %q`, commandPrefix, input, output)
		} else {
			// For mintty: cannot directly read named pipe from Go code
			command = fmt.Sprintf(`command cat %q | %s > %q`, input, commandPrefix, output)
		}
	}

	// To ensure that the options are processed by a POSIX-compliant shell,
	// we need to write the command to a temporary file and execute it with sh.
	var exports []string
	if withExports {
		exports = os.Environ()
		for idx, pairStr := range exports {
			pair := strings.SplitN(pairStr, "=", 2)
			exports[idx] = fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))
		}
	}
	temp := WriteTemporaryFile(append(exports, command), "\n")
	defer os.Remove(temp)

	cmd := cmdBuilder(temp)
	cmd.Stderr = os.Stderr
	intChan := make(chan os.Signal, 1)
	defer close(intChan)
	go func() {
		if sig, valid := <-intChan; valid {
			cmd.Process.Signal(sig)
		}
	}()
	signal.Notify(intChan, os.Interrupt)
	if err := cmd.Run(); err != nil {
		if exitError, ok := err.(*exec.ExitError); ok {
			code := exitError.ExitCode()
			if code == ExitBecome {
				becomeFile := temp + becomeSuffix
				data, err := os.ReadFile(becomeFile)
				os.Remove(becomeFile)
				if err != nil {
					return ExitError, err
				}
				elems := strings.Split(string(data), "\x00")
				if len(elems) < 1 {
					return ExitError, errors.New("invalid become command")
				}
				command := elems[0]
				env := []string{}
				if len(elems) > 1 {
					env = elems[1:]
				}
				executor := util.NewExecutor(opts.WithShell)
				ttyin, err := tui.TtyIn()
				if err != nil {
					return ExitError, err
				}
				executor.Become(ttyin, env, command)
			}
			return code, err
		}
	}

	return ExitOk, nil
}