erm/internal/app/darktile/termutil/terminal.go
Ernest Romero Climent 93392fc149
Fix terminal state and key state data races (#335)
* Update gitignore to ignore .swp

* Fix terminal state data races

go run -race cmd/darktile/main.go

WARNING: DATA RACE
Write at 0x00c000864128 by goroutine 23:
	github.com/liamg/darktile/internal/app/darktile/termutil.(*Terminal).sgrSequenceHandler()
	/home/ernestrc/src/darktile/internal/app/darktile/termutil/csi.go:973 +0x12ca
	github.com/liamg/darktile/internal/app/darktile/termutil.(*Terminal).handleCSI()
	/home/ernestrc/src/darktile/internal/app/darktile/termutil/csi.go:62 +0x7e8
	github.com/liamg/darktile/internal/app/darktile/termutil.(*Terminal).handleANSI()
	/home/ernestrc/src/darktile/internal/app/darktile/termutil/ansi.go:11 +0xc86
	github.com/liamg/darktile/internal/app/darktile/termutil.(*Terminal).process()
	/home/ernestrc/src/darktile/internal/app/darktile/termutil/terminal.go:206 +0x126
	github.com/liamg/darktile/internal/app/darktile/termutil.(*Terminal).Run·dwrap·10()
	/home/ernestrc/src/darktile/internal/app/darktile/termutil/terminal.go:171 +0x39
Previous read at 0x00c000864128 by goroutine 22:
	[failed to restore the stack]
Goroutine 23 (running) created at:
	github.com/liamg/darktile/internal/app/darktile/termutil.(*Terminal).Run()
	/home/ernestrc/src/darktile/internal/app/darktile/termutil/terminal.go:171 +0x517
	github.com/liamg/darktile/internal/app/darktile/gui.(*GUI).Run.func1()
	/home/ernestrc/src/darktile/internal/app/darktile/gui/gui.go:80 +0xa
Goroutine 22 (running) created at:
	github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw.(*UserInterface).Run()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw/run_notsinglethread.go:37 +0x2c4
	github.com/hajimehoshi/ebiten/v2.RunGame()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/run.go:158 +0x1d4
	github.com/liamg/darktile/internal/app/darktile/gui.(*GUI).Run()
	/home/ernestrc/src/darktile/internal/app/darktile/gui/gui.go:99 +0x2de
	github.com/liamg/darktile/internal/app/darktile/cmd.glob..func2()
	/home/ernestrc/src/darktile/internal/app/darktile/cmd/root.go:130 +0x13a9
	github.com/spf13/cobra.(*Command).execute()
	/home/ernestrc/src/darktile/vendor/github.com/spf13/cobra/command.go:852 +0xa7d
	github.com/spf13/cobra.(*Command).ExecuteC()
	/home/ernestrc/src/darktile/vendor/github.com/spf13/cobra/command.go:960 +0x5da
	github.com/spf13/cobra.(*Command).Execute()
	/home/ernestrc/src/darktile/vendor/github.com/spf13/cobra/command.go:897 +0x366
	github.com/liamg/darktile/internal/app/darktile/cmd.Execute()
	/home/ernestrc/src/darktile/internal/app/darktile/cmd/root.go:153 +0x34f
	main.main()
	/home/ernestrc/src/darktile/cmd/darktile/main.go:75 +0x24
==================
Found 26 data race(s)

* Fix KeyPressed data race

go run -race cmd/darktile/main.go

WARNING: DATA RACE
Write at 0x00c00009fbc0 by goroutine 20:
	runtime.mapdelete_fast64()
	/usr/lib/go/src/runtime/map_fast64.go:272 +0x0
	github.com/liamg/darktile/internal/app/darktile/gui.(*keyState).RepeatPressed()
	/home/ernestrc/src/darktile/internal/app/darktile/gui/key_states.go:61 +0x428
	github.com/liamg/darktile/internal/app/darktile/gui.(*GUI).handleInput()
	/home/ernestrc/src/darktile/internal/app/darktile/gui/input.go:109 +0x224
	github.com/liamg/darktile/internal/app/darktile/gui.(*GUI).Update()
	/home/ernestrc/src/darktile/internal/app/darktile/gui/update.go:34 +0x2e
	github.com/hajimehoshi/ebiten/v2.(*imageDumper).update()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/imagedumper_desktop.go:111 +0x85
	github.com/hajimehoshi/ebiten/v2.(*imageDumperGame).Update()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/run.go:115 +0x172
	github.com/hajimehoshi/ebiten/v2.(*uiContext).updateImpl()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/uicontext.go:194 +0x12c
	github.com/hajimehoshi/ebiten/v2.(*uiContext).update()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/uicontext.go:166 +0x88
	github.com/hajimehoshi/ebiten/v2.(*uiContext).Update()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/uicontext.go:147 +0x3b
	github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw.(*UserInterface).loop()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw/ui.go:1036 +0x401
	github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw.(*UserInterface).Run.func1()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw/run_notsinglethread.go:53 +0x1b1
Previous read at 0x00c00009fbc0 by goroutine 19:
	github.com/liamg/darktile/internal/app/darktile/gui.(*keyState).AnythingPressed()
	/home/ernestrc/src/darktile/internal/app/darktile/gui/key_states.go:33 +0xbc
	github.com/liamg/darktile/internal/app/darktile/gui.(*GUI).watchForUpdate()
	/home/ernestrc/src/darktile/internal/app/darktile/gui/gui.go:105 +0x77
	github.com/liamg/darktile/internal/app/darktile/gui.(*GUI).Run·dwrap·2()
	/home/ernestrc/src/darktile/internal/app/darktile/gui/gui.go:97 +0x39
Goroutine 20 (running) created at:
	github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw.(*UserInterface).Run()
	/home/ernestrc/src/darktile/vendor/github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw/run_notsinglethread.go:37 +0x2c4
	github.com/hajimehoshi/ebiten/v2.RunGame()
Goroutine 19 (running) created at:
	github.com/liamg/darktile/internal/app/darktile/gui.(*GUI).Run()
	/home/ernestrc/src/darktile/internal/app/darktile/gui/gui.go:97 +0x2ca
	github.com/liamg/darktile/internal/app/darktile/cmd.glob..func2()
	/home/ernestrc/src/darktile/internal/app/darktile/cmd/root.go:130 +0x13a9
	github.com/spf13/cobra.(*Command).execute()
	/home/ernestrc/src/darktile/vendor/github.com/spf13/cobra/command.go:852 +0xa7d
	github.com/spf13/cobra.(*Command).ExecuteC()
	/home/ernestrc/src/darktile/vendor/github.com/spf13/cobra/command.go:960 +0x5da
	github.com/spf13/cobra.(*Command).Execute()
	/home/ernestrc/src/darktile/vendor/github.com/spf13/cobra/command.go:897 +0x366
	github.com/liamg/darktile/internal/app/darktile/cmd.Execute()
	/home/ernestrc/src/darktile/internal/app/darktile/cmd/root.go:153 +0x34f
	main.main()
	/home/ernestrc/src/darktile/cmd/darktile/main.go:75 +0x24
2022-05-10 09:18:01 +01:00

324 lines
6.3 KiB
Go

package termutil
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"sync"
"github.com/creack/pty"
"golang.org/x/term"
)
const (
MainBuffer uint8 = 0
AltBuffer uint8 = 1
InternalBuffer uint8 = 2
)
// Terminal communicates with the underlying terminal
type Terminal struct {
mu sync.Mutex
windowManipulator WindowManipulator
pty *os.File
updateChan chan struct{}
processChan chan MeasuredRune
closeChan chan struct{}
buffers []*Buffer
activeBuffer *Buffer
mouseMode MouseMode
mouseExtMode MouseExtMode
logFile *os.File
theme *Theme
running bool
shell string
initialCommand string
}
// NewTerminal creates a new terminal instance
func New(options ...Option) *Terminal {
term := &Terminal{
processChan: make(chan MeasuredRune, 0xffff),
closeChan: make(chan struct{}),
theme: &Theme{},
}
for _, opt := range options {
opt(term)
}
fg := term.theme.DefaultForeground()
bg := term.theme.DefaultBackground()
term.buffers = []*Buffer{
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
}
term.activeBuffer = term.buffers[0]
return term
}
func (t *Terminal) SetWindowManipulator(m WindowManipulator) {
t.windowManipulator = m
}
func (t *Terminal) log(line string, params ...interface{}) {
if t.logFile != nil {
_, _ = fmt.Fprintf(t.logFile, line+"\n", params...)
}
}
func (t *Terminal) reset() {
fg := t.theme.DefaultForeground()
bg := t.theme.DefaultBackground()
t.buffers = []*Buffer{
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
}
t.useMainBuffer()
}
// Pty exposes the underlying terminal pty, if it exists
func (t *Terminal) Pty() *os.File {
return t.pty
}
func (t *Terminal) WriteToPty(data []byte) error {
_, err := t.pty.Write(data)
return err
}
func (t *Terminal) GetTitle() string {
return t.windowManipulator.GetTitle()
}
func (t *Terminal) Theme() *Theme {
return t.theme
}
// write takes data from StdOut of the child shell and processes it
func (t *Terminal) Write(data []byte) (n int, err error) {
reader := bufio.NewReader(bytes.NewBuffer(data))
for {
r, size, err := reader.ReadRune()
if err == io.EOF {
break
}
t.processChan <- MeasuredRune{Rune: r, Width: size}
}
return len(data), nil
}
func (t *Terminal) SetSize(rows, cols uint16) error {
if t.pty == nil {
return fmt.Errorf("terminal is not running")
}
t.log("RESIZE %d, %d\n", cols, rows)
t.activeBuffer.resizeView(cols, rows)
if err := pty.Setsize(t.pty, &pty.Winsize{
Rows: rows,
Cols: cols,
}); err != nil {
return err
}
return nil
}
// Run starts the terminal/shell proxying process
func (t *Terminal) Run(updateChan chan struct{}, rows uint16, cols uint16) error {
os.Setenv("TERM", "xterm-256color")
t.updateChan = updateChan
if t.shell == "" {
t.shell = os.Getenv("SHELL")
if t.shell == "" {
t.shell = "/bin/sh"
}
}
// Create arbitrary command.
c := exec.Command(t.shell)
// Start the command with a pty.
var err error
t.pty, err = pty.Start(c)
if err != nil {
return err
}
// Make sure to close the pty at the end.
defer func() { _ = t.pty.Close() }() // Best effort.
if err := t.SetSize(rows, cols); err != nil {
return err
}
// Set stdin in raw mode.
if fd := int(os.Stdin.Fd()); term.IsTerminal(fd) {
oldState, err := term.MakeRaw(fd)
if err != nil {
t.windowManipulator.ReportError(err)
}
defer func() { _ = term.Restore(fd, oldState) }() // Best effort.
}
go t.process()
t.running = true
t.windowManipulator.SetTitle("darktile")
if t.initialCommand != "" {
if err := t.WriteToPty([]byte(t.initialCommand)); err != nil {
return err
}
}
_, _ = io.Copy(t, t.pty)
close(t.closeChan)
return nil
}
func (t *Terminal) IsRunning() bool {
return t.running
}
func (t *Terminal) requestRender() {
select {
case t.updateChan <- struct{}{}:
default:
}
}
func (t *Terminal) processSequence(mr MeasuredRune) (render bool) {
if mr.Rune == 0x1b {
return t.handleANSI(t.processChan)
}
return t.processRunes(mr)
}
func (t *Terminal) process() {
for {
select {
case <-t.closeChan:
return
case mr := <-t.processChan:
if t.processSequence(mr) {
t.requestRender()
}
}
}
}
func (t *Terminal) processRunes(runes ...MeasuredRune) (renderRequired bool) {
t.mu.Lock()
defer t.mu.Unlock()
for _, r := range runes {
t.log("%c 0x%X", r.Rune, r.Rune)
switch r.Rune {
case 0x05: //enq
continue
case 0x07: //bell
//DING DING DING
continue
case 0x8: //backspace
t.activeBuffer.backspace()
renderRequired = true
case 0x9: //tab
t.activeBuffer.tab()
renderRequired = true
case 0xa, 0xc: //newLine/form feed
t.activeBuffer.newLine()
renderRequired = true
case 0xb: //vertical tab
t.activeBuffer.verticalTab()
renderRequired = true
case 0xd: //carriageReturn
t.activeBuffer.carriageReturn()
renderRequired = true
case 0xe: //shiftOut
t.activeBuffer.currentCharset = 1
case 0xf: //shiftIn
t.activeBuffer.currentCharset = 0
default:
if r.Rune < 0x20 {
// handle any other control chars here?
continue
}
t.activeBuffer.write(t.translateRune(r))
renderRequired = true
}
}
return renderRequired
}
func (t *Terminal) translateRune(b MeasuredRune) MeasuredRune {
table := t.activeBuffer.charsets[t.activeBuffer.currentCharset]
if table == nil {
return b
}
chr, ok := (*table)[b.Rune]
if ok {
return MeasuredRune{Rune: chr, Width: 1}
}
return b
}
func (t *Terminal) setTitle(title string) {
t.windowManipulator.SetTitle(title)
}
func (t *Terminal) switchBuffer(index uint8) {
var carrySize bool
var w, h uint16
if t.activeBuffer != nil {
w, h = t.activeBuffer.viewWidth, t.activeBuffer.viewHeight
carrySize = true
}
t.activeBuffer = t.buffers[index]
if carrySize {
t.activeBuffer.resizeView(w, h)
}
}
func (t *Terminal) GetMouseMode() MouseMode {
return t.mouseMode
}
func (t *Terminal) GetMouseExtMode() MouseExtMode {
return t.mouseExtMode
}
func (t *Terminal) GetActiveBuffer() *Buffer {
return t.activeBuffer
}
func (t *Terminal) useMainBuffer() {
t.switchBuffer(MainBuffer)
}
func (t *Terminal) useAltBuffer() {
t.switchBuffer(AltBuffer)
}
func (t *Terminal) Lock() {
t.mu.Lock()
}
func (t *Terminal) Unlock() {
t.mu.Unlock()
}