mirror of https://github.com/curusarn/resh
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
211 lines
6.7 KiB
211 lines
6.7 KiB
package histfile
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/curusarn/resh/pkg/histlist"
|
|
"github.com/curusarn/resh/pkg/records"
|
|
)
|
|
|
|
// Histfile writes records to histfile
|
|
type Histfile struct {
|
|
sessionsMutex sync.Mutex
|
|
sessions map[string]records.Record
|
|
historyPath string
|
|
|
|
recentMutex sync.Mutex
|
|
recentRecords []records.Record
|
|
|
|
// NOTE: we have separate histories which only differ if there was not enough resh_history
|
|
// resh_history itself is common for both bash and zsh
|
|
bashCmdLines histlist.Histlist
|
|
zshCmdLines histlist.Histlist
|
|
}
|
|
|
|
// New creates new histfile and runs its gorutines
|
|
func New(input chan records.Record, sessionsToDrop chan string,
|
|
reshHistoryPath string, bashHistoryPath string, zshHistoryPath string,
|
|
maxInitHistSize int, minInitHistSizeKB int,
|
|
signals chan os.Signal, shutdownDone chan string) *Histfile {
|
|
|
|
hf := Histfile{
|
|
sessions: map[string]records.Record{},
|
|
historyPath: reshHistoryPath,
|
|
bashCmdLines: histlist.New(),
|
|
zshCmdLines: histlist.New(),
|
|
}
|
|
go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB)
|
|
go hf.writer(input, signals, shutdownDone)
|
|
go hf.sessionGC(sessionsToDrop)
|
|
return &hf
|
|
}
|
|
|
|
// loadsHistory from resh_history and if there is not enough of it also load native shell histories
|
|
func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) {
|
|
h.recentMutex.Lock()
|
|
defer h.recentMutex.Unlock()
|
|
log.Println("histfile: Checking if resh_history is large enough ...")
|
|
fi, err := os.Stat(h.historyPath)
|
|
var size int
|
|
if err != nil {
|
|
log.Println("histfile ERROR: failed to stat resh_history file:", err)
|
|
} else {
|
|
size = int(fi.Size())
|
|
}
|
|
useNativeHistories := false
|
|
if size/1024 < minInitHistSizeKB {
|
|
useNativeHistories = true
|
|
log.Println("histfile WARN: resh_history is too small - loading native bash and zsh history ...")
|
|
h.bashCmdLines = records.LoadCmdLinesFromBashFile(bashHistoryPath)
|
|
log.Println("histfile: bash history loaded - cmdLine count:", len(h.bashCmdLines.List))
|
|
h.zshCmdLines = records.LoadCmdLinesFromZshFile(zshHistoryPath)
|
|
log.Println("histfile: zsh history loaded - cmdLine count:", len(h.zshCmdLines.List))
|
|
// no maxInitHistSize when using native histories
|
|
maxInitHistSize = math.MaxInt32
|
|
}
|
|
log.Println("histfile: Loading resh history from file ...")
|
|
reshCmdLines := histlist.New()
|
|
// NOTE: keeping this weird interface for now because we might use it in the future
|
|
// when we only load bash or zsh history
|
|
records.LoadCmdLinesFromFile(&reshCmdLines, h.historyPath, maxInitHistSize)
|
|
log.Println("histfile: resh history loaded - cmdLine count:", len(reshCmdLines.List))
|
|
if useNativeHistories == false {
|
|
h.bashCmdLines = reshCmdLines
|
|
h.zshCmdLines = histlist.Copy(reshCmdLines)
|
|
return
|
|
}
|
|
h.bashCmdLines.AddHistlist(reshCmdLines)
|
|
log.Println("histfile: bash history + resh history - cmdLine count:", len(h.bashCmdLines.List))
|
|
h.zshCmdLines.AddHistlist(reshCmdLines)
|
|
log.Println("histfile: zsh history + resh history - cmdLine count:", len(h.zshCmdLines.List))
|
|
}
|
|
|
|
// sessionGC reads sessionIDs from channel and deletes them from histfile struct
|
|
func (h *Histfile) sessionGC(sessionsToDrop chan string) {
|
|
for {
|
|
func() {
|
|
session := <-sessionsToDrop
|
|
log.Println("histfile: got session to drop", session)
|
|
h.sessionsMutex.Lock()
|
|
defer h.sessionsMutex.Unlock()
|
|
if part1, found := h.sessions[session]; found == true {
|
|
log.Println("histfile: Dropping session:", session)
|
|
delete(h.sessions, session)
|
|
go writeRecord(part1, h.historyPath)
|
|
} else {
|
|
log.Println("histfile: No hanging parts for session:", session)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// writer reads records from channel, merges them and writes them to file
|
|
func (h *Histfile) writer(input chan records.Record, signals chan os.Signal, shutdownDone chan string) {
|
|
for {
|
|
func() {
|
|
select {
|
|
case record := <-input:
|
|
h.sessionsMutex.Lock()
|
|
defer h.sessionsMutex.Unlock()
|
|
|
|
// allows nested sessions to merge records properly
|
|
mergeID := record.SessionID + "_" + strconv.Itoa(record.Shlvl)
|
|
if record.PartOne {
|
|
if _, found := h.sessions[mergeID]; found {
|
|
log.Println("histfile WARN: Got another first part of the records before merging the previous one - overwriting! " +
|
|
"(this happens in bash because bash-preexec runs when it's not supposed to)")
|
|
}
|
|
h.sessions[mergeID] = record
|
|
} else {
|
|
if part1, found := h.sessions[mergeID]; found == false {
|
|
log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring! (mergeID:", mergeID, ")")
|
|
} else {
|
|
delete(h.sessions, mergeID)
|
|
go h.mergeAndWriteRecord(part1, record)
|
|
}
|
|
}
|
|
case sig := <-signals:
|
|
log.Println("histfile: Got signal " + sig.String())
|
|
h.sessionsMutex.Lock()
|
|
defer h.sessionsMutex.Unlock()
|
|
log.Println("histfile DEBUG: Unlocked mutex")
|
|
|
|
for sessID, record := range h.sessions {
|
|
log.Panicln("histfile WARN: Writing incomplete record for session " + sessID)
|
|
h.writeRecord(record)
|
|
}
|
|
log.Println("histfile DEBUG: Shutdown success")
|
|
shutdownDone <- "histfile"
|
|
return
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (h *Histfile) writeRecord(part1 records.Record) {
|
|
writeRecord(part1, h.historyPath)
|
|
}
|
|
|
|
func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) {
|
|
err := part1.Merge(part2)
|
|
if err != nil {
|
|
log.Println("Error while merging", err)
|
|
return
|
|
}
|
|
|
|
func() {
|
|
h.recentMutex.Lock()
|
|
defer h.recentMutex.Unlock()
|
|
h.recentRecords = append(h.recentRecords, part1)
|
|
cmdLine := part1.CmdLine
|
|
h.bashCmdLines.AddCmdLine(cmdLine)
|
|
h.zshCmdLines.AddCmdLine(cmdLine)
|
|
}()
|
|
|
|
writeRecord(part1, h.historyPath)
|
|
}
|
|
|
|
func writeRecord(rec records.Record, outputPath string) {
|
|
recJSON, err := json.Marshal(rec)
|
|
if err != nil {
|
|
log.Println("Marshalling error", err)
|
|
return
|
|
}
|
|
f, err := os.OpenFile(outputPath,
|
|
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
log.Println("Could not open file", err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
_, err = f.Write(append(recJSON, []byte("\n")...))
|
|
if err != nil {
|
|
log.Printf("Error while writing: %v, %s\n", rec, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// GetRecentCmdLines returns recent cmdLines
|
|
func (h *Histfile) GetRecentCmdLines(shell string, limit int) histlist.Histlist {
|
|
// NOTE: limit does nothing atm
|
|
h.recentMutex.Lock()
|
|
defer h.recentMutex.Unlock()
|
|
log.Println("histfile: History requested ...")
|
|
var hl histlist.Histlist
|
|
if shell == "bash" {
|
|
hl = histlist.Copy(h.bashCmdLines)
|
|
log.Println("histfile: history copied (bash) - cmdLine count:", len(hl.List))
|
|
return hl
|
|
}
|
|
if shell != "zsh" {
|
|
log.Println("histfile ERROR: Unknown shell: ", shell)
|
|
}
|
|
hl = histlist.Copy(h.zshCmdLines)
|
|
log.Println("histfile: history copied (zsh) - cmdLine count:", len(hl.List))
|
|
return hl
|
|
}
|
|
|