mirror of https://github.com/curusarn/resh
parent
96cf2ae032
commit
79d7f1c45e
@ -1,24 +0,0 @@ |
||||
package main |
||||
|
||||
import "github.com/curusarn/resh/pkg/records" |
||||
|
||||
type strategyDummy struct { |
||||
history []string |
||||
} |
||||
|
||||
func (s *strategyDummy) GetTitleAndDescription() (string, string) { |
||||
return "dummy", "Return empty candidate list" |
||||
} |
||||
|
||||
func (s *strategyDummy) GetCandidates() []string { |
||||
return nil |
||||
} |
||||
|
||||
func (s *strategyDummy) AddHistoryRecord(record *records.EnrichedRecord) error { |
||||
s.history = append(s.history, record.CmdLine) |
||||
return nil |
||||
} |
||||
|
||||
func (s *strategyDummy) ResetHistory() error { |
||||
return nil |
||||
} |
||||
@ -0,0 +1,246 @@ |
||||
package histanal |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"log" |
||||
"math/rand" |
||||
"os" |
||||
"os/exec" |
||||
|
||||
"github.com/curusarn/resh/pkg/records" |
||||
"github.com/curusarn/resh/pkg/strat" |
||||
"github.com/jpillora/longestcommon" |
||||
|
||||
"github.com/schollz/progressbar" |
||||
) |
||||
|
||||
type matchJSON struct { |
||||
Match bool |
||||
Distance int |
||||
CharsRecalled int |
||||
} |
||||
|
||||
type multiMatchItemJSON struct { |
||||
Distance int |
||||
CharsRecalled int |
||||
} |
||||
|
||||
type multiMatchJSON struct { |
||||
Match bool |
||||
Entries []multiMatchItemJSON |
||||
} |
||||
|
||||
type strategyJSON struct { |
||||
Title string |
||||
Description string |
||||
Matches []matchJSON |
||||
PrefixMatches []multiMatchJSON |
||||
} |
||||
|
||||
// HistEval evaluates history
|
||||
type HistEval struct { |
||||
HistLoad |
||||
BatchMode bool |
||||
maxCandidates int |
||||
Strategies []strategyJSON |
||||
} |
||||
|
||||
// NewHistEval constructs new HistEval
|
||||
func NewHistEval(inputPath string, |
||||
maxCandidates int, skipFailedCmds bool, |
||||
debugRecords float64, sanitizedInput bool) HistEval { |
||||
|
||||
e := HistEval{ |
||||
HistLoad: HistLoad{ |
||||
skipFailedCmds: skipFailedCmds, |
||||
debugRecords: debugRecords, |
||||
sanitizedInput: sanitizedInput, |
||||
}, |
||||
maxCandidates: maxCandidates, |
||||
BatchMode: false, |
||||
} |
||||
records := e.loadHistoryRecords(inputPath) |
||||
device := deviceRecords{Records: records} |
||||
user := userRecords{} |
||||
user.Devices = append(user.Devices, device) |
||||
e.UsersRecords = append(e.UsersRecords, user) |
||||
e.preprocessRecords() |
||||
return e |
||||
} |
||||
|
||||
// NewHistEvalBatchMode constructs new HistEval in batch mode
|
||||
func NewHistEvalBatchMode(input string, inputDataRoot string, |
||||
maxCandidates int, skipFailedCmds bool, |
||||
debugRecords float64, sanitizedInput bool) HistEval { |
||||
|
||||
e := HistEval{ |
||||
HistLoad: HistLoad{ |
||||
skipFailedCmds: skipFailedCmds, |
||||
debugRecords: debugRecords, |
||||
sanitizedInput: sanitizedInput, |
||||
}, |
||||
maxCandidates: maxCandidates, |
||||
BatchMode: false, |
||||
} |
||||
e.UsersRecords = e.loadHistoryRecordsBatchMode(input, inputDataRoot) |
||||
e.preprocessRecords() |
||||
return e |
||||
} |
||||
|
||||
func (e *HistEval) preprocessDeviceRecords(device deviceRecords) deviceRecords { |
||||
sessionIDs := map[string]uint64{} |
||||
var nextID uint64 |
||||
nextID = 1 // start with 1 because 0 won't get saved to json
|
||||
for k, record := range device.Records { |
||||
id, found := sessionIDs[record.SessionID] |
||||
if found == false { |
||||
id = nextID |
||||
sessionIDs[record.SessionID] = id |
||||
nextID++ |
||||
} |
||||
device.Records[k].SeqSessionID = id |
||||
// assert
|
||||
if record.Sanitized != e.sanitizedInput { |
||||
if e.sanitizedInput { |
||||
log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") |
||||
} |
||||
log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") |
||||
} |
||||
device.Records[k].SeqSessionID = id |
||||
if e.debugRecords > 0 && rand.Float64() < e.debugRecords { |
||||
device.Records[k].DebugThisRecord = true |
||||
} |
||||
} |
||||
// sort.SliceStable(device.Records, func(x, y int) bool {
|
||||
// if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID {
|
||||
// return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal
|
||||
// }
|
||||
// return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID
|
||||
// })
|
||||
|
||||
// iterate from back and mark last record of each session
|
||||
sessionIDSet := map[string]bool{} |
||||
for i := len(device.Records) - 1; i >= 0; i-- { |
||||
var record *records.EnrichedRecord |
||||
record = &device.Records[i] |
||||
if sessionIDSet[record.SessionID] { |
||||
continue |
||||
} |
||||
sessionIDSet[record.SessionID] = true |
||||
record.LastRecordOfSession = true |
||||
} |
||||
return device |
||||
} |
||||
|
||||
// enrich records and add sequential session ID
|
||||
func (e *HistEval) preprocessRecords() { |
||||
for i := range e.UsersRecords { |
||||
for j := range e.UsersRecords[i].Devices { |
||||
e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Evaluate a given strategy
|
||||
func (e *HistEval) Evaluate(strategy strat.IStrategy) error { |
||||
title, description := strategy.GetTitleAndDescription() |
||||
log.Println("Evaluating strategy:", title, "-", description) |
||||
strategyData := strategyJSON{Title: title, Description: description} |
||||
for i := range e.UsersRecords { |
||||
for j := range e.UsersRecords[i].Devices { |
||||
bar := progressbar.New(len(e.UsersRecords[i].Devices[j].Records)) |
||||
var prevRecord records.EnrichedRecord |
||||
for _, record := range e.UsersRecords[i].Devices[j].Records { |
||||
if e.skipFailedCmds && record.ExitCode != 0 { |
||||
continue |
||||
} |
||||
candidates := strategy.GetCandidates(records.Stripped(record)) |
||||
if record.DebugThisRecord { |
||||
log.Println() |
||||
log.Println("===================================================") |
||||
log.Println("STRATEGY:", title, "-", description) |
||||
log.Println("===================================================") |
||||
log.Println("Previous record:") |
||||
if prevRecord.RealtimeBefore == 0 { |
||||
log.Println("== NIL") |
||||
} else { |
||||
rec, _ := prevRecord.ToString() |
||||
log.Println(rec) |
||||
} |
||||
log.Println("---------------------------------------------------") |
||||
log.Println("Recommendations for:") |
||||
rec, _ := record.ToString() |
||||
log.Println(rec) |
||||
log.Println("---------------------------------------------------") |
||||
for i, candidate := range candidates { |
||||
if i > 10 { |
||||
break |
||||
} |
||||
log.Println(string(candidate)) |
||||
} |
||||
log.Println("===================================================") |
||||
} |
||||
|
||||
matchFound := false |
||||
longestPrefixMatchLength := 0 |
||||
multiMatch := multiMatchJSON{} |
||||
for i, candidate := range candidates { |
||||
// make an option (--calculate-total) to turn this on/off ?
|
||||
// if i >= e.maxCandidates {
|
||||
// break
|
||||
// }
|
||||
commonPrefixLength := len(longestcommon.Prefix([]string{candidate, record.CmdLine})) |
||||
if commonPrefixLength > longestPrefixMatchLength { |
||||
longestPrefixMatchLength = commonPrefixLength |
||||
prefixMatch := multiMatchItemJSON{Distance: i + 1, CharsRecalled: commonPrefixLength} |
||||
multiMatch.Match = true |
||||
multiMatch.Entries = append(multiMatch.Entries, prefixMatch) |
||||
} |
||||
if candidate == record.CmdLine { |
||||
match := matchJSON{Match: true, Distance: i + 1, CharsRecalled: record.CmdLength} |
||||
matchFound = true |
||||
strategyData.Matches = append(strategyData.Matches, match) |
||||
strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) |
||||
break |
||||
} |
||||
} |
||||
if matchFound == false { |
||||
strategyData.Matches = append(strategyData.Matches, matchJSON{}) |
||||
strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) |
||||
} |
||||
err := strategy.AddHistoryRecord(&record) |
||||
if err != nil { |
||||
log.Println("Error while evauating", err) |
||||
return err |
||||
} |
||||
bar.Add(1) |
||||
prevRecord = record |
||||
} |
||||
strategy.ResetHistory() |
||||
fmt.Println() |
||||
} |
||||
} |
||||
e.Strategies = append(e.Strategies, strategyData) |
||||
return nil |
||||
} |
||||
|
||||
// CalculateStatsAndPlot results
|
||||
func (e *HistEval) CalculateStatsAndPlot(scriptName string) { |
||||
evalJSON, err := json.Marshal(e) |
||||
if err != nil { |
||||
log.Fatal("json marshal error", err) |
||||
} |
||||
buffer := bytes.Buffer{} |
||||
buffer.Write(evalJSON) |
||||
// run python script to stat and plot/
|
||||
cmd := exec.Command(scriptName) |
||||
cmd.Stdout = os.Stdout |
||||
cmd.Stderr = os.Stderr |
||||
cmd.Stdin = &buffer |
||||
err = cmd.Run() |
||||
if err != nil { |
||||
log.Printf("Command finished with error: %v", err) |
||||
} |
||||
} |
||||
@ -0,0 +1,179 @@ |
||||
package histanal |
||||
|
||||
import ( |
||||
"bufio" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"log" |
||||
"math/rand" |
||||
"os" |
||||
"path/filepath" |
||||
|
||||
"github.com/curusarn/resh/pkg/records" |
||||
) |
||||
|
||||
type deviceRecords struct { |
||||
Name string |
||||
Records []records.EnrichedRecord |
||||
} |
||||
|
||||
type userRecords struct { |
||||
Name string |
||||
Devices []deviceRecords |
||||
} |
||||
|
||||
// HistLoad loads history
|
||||
type HistLoad struct { |
||||
UsersRecords []userRecords |
||||
skipFailedCmds bool |
||||
sanitizedInput bool |
||||
debugRecords float64 |
||||
} |
||||
|
||||
func (e *HistLoad) preprocessDeviceRecords(device deviceRecords) deviceRecords { |
||||
sessionIDs := map[string]uint64{} |
||||
var nextID uint64 |
||||
nextID = 1 // start with 1 because 0 won't get saved to json
|
||||
for k, record := range device.Records { |
||||
id, found := sessionIDs[record.SessionID] |
||||
if found == false { |
||||
id = nextID |
||||
sessionIDs[record.SessionID] = id |
||||
nextID++ |
||||
} |
||||
device.Records[k].SeqSessionID = id |
||||
// assert
|
||||
if record.Sanitized != e.sanitizedInput { |
||||
if e.sanitizedInput { |
||||
log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") |
||||
} |
||||
log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") |
||||
} |
||||
device.Records[k].SeqSessionID = id |
||||
if e.debugRecords > 0 && rand.Float64() < e.debugRecords { |
||||
device.Records[k].DebugThisRecord = true |
||||
} |
||||
} |
||||
// sort.SliceStable(device.Records, func(x, y int) bool {
|
||||
// if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID {
|
||||
// return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal
|
||||
// }
|
||||
// return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID
|
||||
// })
|
||||
|
||||
// iterate from back and mark last record of each session
|
||||
sessionIDSet := map[string]bool{} |
||||
for i := len(device.Records) - 1; i >= 0; i-- { |
||||
var record *records.EnrichedRecord |
||||
record = &device.Records[i] |
||||
if sessionIDSet[record.SessionID] { |
||||
continue |
||||
} |
||||
sessionIDSet[record.SessionID] = true |
||||
record.LastRecordOfSession = true |
||||
} |
||||
return device |
||||
} |
||||
|
||||
// enrich records and add sequential session ID
|
||||
func (e *HistLoad) preprocessRecords() { |
||||
for i := range e.UsersRecords { |
||||
for j := range e.UsersRecords[i].Devices { |
||||
e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (e *HistLoad) loadHistoryRecordsBatchMode(fname string, dataRootPath string) []userRecords { |
||||
var records []userRecords |
||||
info, err := os.Stat(dataRootPath) |
||||
if err != nil { |
||||
log.Fatal("Error: Directory", dataRootPath, "does not exist - exiting! (", err, ")") |
||||
} |
||||
if info.IsDir() == false { |
||||
log.Fatal("Error:", dataRootPath, "is not a directory - exiting!") |
||||
} |
||||
users, err := ioutil.ReadDir(dataRootPath) |
||||
if err != nil { |
||||
log.Fatal("Could not read directory:", dataRootPath) |
||||
} |
||||
fmt.Println("Listing users in <", dataRootPath, ">...") |
||||
for _, user := range users { |
||||
userRecords := userRecords{Name: user.Name()} |
||||
userFullPath := filepath.Join(dataRootPath, user.Name()) |
||||
if user.IsDir() == false { |
||||
log.Println("Warn: Unexpected file (not a directory) <", userFullPath, "> - skipping.") |
||||
continue |
||||
} |
||||
fmt.Println() |
||||
fmt.Printf("*- %s\n", user.Name()) |
||||
devices, err := ioutil.ReadDir(userFullPath) |
||||
if err != nil { |
||||
log.Fatal("Could not read directory:", userFullPath) |
||||
} |
||||
for _, device := range devices { |
||||
deviceRecords := deviceRecords{Name: device.Name()} |
||||
deviceFullPath := filepath.Join(userFullPath, device.Name()) |
||||
if device.IsDir() == false { |
||||
log.Println("Warn: Unexpected file (not a directory) <", deviceFullPath, "> - skipping.") |
||||
continue |
||||
} |
||||
fmt.Printf(" \\- %s\n", device.Name()) |
||||
files, err := ioutil.ReadDir(deviceFullPath) |
||||
if err != nil { |
||||
log.Fatal("Could not read directory:", deviceFullPath) |
||||
} |
||||
for _, file := range files { |
||||
fileFullPath := filepath.Join(deviceFullPath, file.Name()) |
||||
if file.Name() == fname { |
||||
fmt.Printf(" \\- %s - loading ...", file.Name()) |
||||
// load the data
|
||||
deviceRecords.Records = e.loadHistoryRecords(fileFullPath) |
||||
fmt.Println(" OK ✓") |
||||
} else { |
||||
fmt.Printf(" \\- %s - skipped\n", file.Name()) |
||||
} |
||||
} |
||||
userRecords.Devices = append(userRecords.Devices, deviceRecords) |
||||
} |
||||
records = append(records, userRecords) |
||||
} |
||||
return records |
||||
} |
||||
|
||||
func (e *HistLoad) loadHistoryRecords(fname string) []records.EnrichedRecord { |
||||
file, err := os.Open(fname) |
||||
if err != nil { |
||||
log.Fatal("Open() resh history file error:", err) |
||||
} |
||||
defer file.Close() |
||||
|
||||
var recs []records.EnrichedRecord |
||||
scanner := bufio.NewScanner(file) |
||||
for scanner.Scan() { |
||||
record := records.Record{} |
||||
fallbackRecord := records.FallbackRecord{} |
||||
line := scanner.Text() |
||||
err = json.Unmarshal([]byte(line), &record) |
||||
if err != nil { |
||||
err = json.Unmarshal([]byte(line), &fallbackRecord) |
||||
if err != nil { |
||||
log.Println("Line:", line) |
||||
log.Fatal("Decoding error:", err) |
||||
} |
||||
record = records.ConvertRecord(&fallbackRecord) |
||||
} |
||||
if e.sanitizedInput == false { |
||||
if record.CmdLength != 0 { |
||||
log.Fatal("Assert failed - 'cmdLength' is set in raw data. Maybe you want to use '--sanitized-input' option?") |
||||
} |
||||
record.CmdLength = len(record.CmdLine) |
||||
} |
||||
if record.CmdLength == 0 { |
||||
log.Fatal("Assert failed - 'cmdLength' is unset in the data. This should not happen.") |
||||
} |
||||
recs = append(recs, record.Enrich()) |
||||
} |
||||
return recs |
||||
} |
||||
@ -0,0 +1,24 @@ |
||||
package strat |
||||
|
||||
import "github.com/curusarn/resh/pkg/records" |
||||
|
||||
type Dummy struct { |
||||
history []string |
||||
} |
||||
|
||||
func (s *Dummy) GetTitleAndDescription() (string, string) { |
||||
return "dummy", "Return empty candidate list" |
||||
} |
||||
|
||||
func (s *Dummy) GetCandidates() []string { |
||||
return nil |
||||
} |
||||
|
||||
func (s *Dummy) AddHistoryRecord(record *records.EnrichedRecord) error { |
||||
s.history = append(s.history, record.CmdLine) |
||||
return nil |
||||
} |
||||
|
||||
func (s *Dummy) ResetHistory() error { |
||||
return nil |
||||
} |
||||
@ -0,0 +1,44 @@ |
||||
package strat |
||||
|
||||
import ( |
||||
"github.com/curusarn/resh/pkg/records" |
||||
) |
||||
|
||||
type ISimpleStrategy interface { |
||||
GetTitleAndDescription() (string, string) |
||||
GetCandidates() []string |
||||
AddHistoryRecord(record *records.EnrichedRecord) error |
||||
ResetHistory() error |
||||
} |
||||
|
||||
type IStrategy interface { |
||||
GetTitleAndDescription() (string, string) |
||||
GetCandidates(r records.EnrichedRecord) []string |
||||
AddHistoryRecord(record *records.EnrichedRecord) error |
||||
ResetHistory() error |
||||
} |
||||
|
||||
type simpleStrategyWrapper struct { |
||||
strategy ISimpleStrategy |
||||
} |
||||
|
||||
// NewSimpleStrategyWrapper returns IStrategy created by wrapping given ISimpleStrategy
|
||||
func NewSimpleStrategyWrapper(strategy ISimpleStrategy) *simpleStrategyWrapper { |
||||
return &simpleStrategyWrapper{strategy: strategy} |
||||
} |
||||
|
||||
func (s *simpleStrategyWrapper) GetTitleAndDescription() (string, string) { |
||||
return s.strategy.GetTitleAndDescription() |
||||
} |
||||
|
||||
func (s *simpleStrategyWrapper) GetCandidates(r records.EnrichedRecord) []string { |
||||
return s.strategy.GetCandidates() |
||||
} |
||||
|
||||
func (s *simpleStrategyWrapper) AddHistoryRecord(r *records.EnrichedRecord) error { |
||||
return s.strategy.AddHistoryRecord(r) |
||||
} |
||||
|
||||
func (s *simpleStrategyWrapper) ResetHistory() error { |
||||
return s.strategy.ResetHistory() |
||||
} |
||||
Loading…
Reference in new issue