mirror of https://github.com/curusarn/resh
parent
00c1262a9b
commit
e620a4a477
@ -1,46 +0,0 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/internal/cfg" |
||||
"github.com/curusarn/resh/internal/logger" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
// info passed during build
|
||||
var version string |
||||
var commit string |
||||
var developement bool |
||||
|
||||
func main() { |
||||
errDo := doConfigSetup() |
||||
config, errCfg := cfg.New() |
||||
logger, _ := logger.New("config-setup", config.LogLevel, developement) |
||||
defer logger.Sync() // flushes buffer, if any
|
||||
|
||||
if errDo != nil { |
||||
logger.Error("Config setup failed", zap.Error(errDo)) |
||||
// TODO: better error message for people
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %v\n", errDo) |
||||
} |
||||
if errCfg != nil { |
||||
logger.Error("Error while getting configuration", zap.Error(errCfg)) |
||||
} |
||||
} |
||||
|
||||
func doConfigSetup() error { |
||||
err := cfg.Touch() |
||||
if err != nil { |
||||
return fmt.Errorf("could not touch config file: %w", err) |
||||
} |
||||
changes, err := cfg.Migrate() |
||||
if err != nil { |
||||
return fmt.Errorf("could not migrate config file version: %v", err) |
||||
} |
||||
if changes { |
||||
fmt.Printf("Config file format has changed - your config was updated to reflect the changes.\n") |
||||
} |
||||
return nil |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
package main |
||||
|
||||
func backup() { |
||||
|
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"os" |
||||
) |
||||
|
||||
// info passed during build
|
||||
var version string |
||||
var commit string |
||||
var developement bool |
||||
|
||||
func main() { |
||||
var command string |
||||
flag.StringVar(&command, "command", "", "Utility to run") |
||||
flag.Parse() |
||||
|
||||
switch command { |
||||
case "backup": |
||||
backup() |
||||
case "rollback": |
||||
rollback() |
||||
case "migrate-config": |
||||
migrateConfig() |
||||
case "migrate-history": |
||||
migrateHistory() |
||||
case "help": |
||||
printUsage(os.Stdout) |
||||
default: |
||||
fmt.Fprintf(os.Stderr, "ERROR: Unknown command") |
||||
printUsage(os.Stderr) |
||||
} |
||||
} |
||||
|
||||
func printUsage(f *os.File) { |
||||
usage := ` |
||||
Utils used during resh instalation
|
||||
|
||||
USAGE: ./install-utils COMMAND |
||||
COMMANDS: |
||||
backup backup resh installation and data |
||||
rollback restore resh installation and data from backup |
||||
migrate-config update config to reflect updates |
||||
migrate-history update history to reflect updates |
||||
help show this help |
||||
` |
||||
fmt.Fprintf(f, usage) |
||||
} |
||||
@ -0,0 +1,36 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/internal/cfg" |
||||
) |
||||
|
||||
func migrateConfig() { |
||||
err := cfg.Touch() |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "ERROR: Failed to touch config file: %v\n", err) |
||||
os.Exit(1) |
||||
} |
||||
changes, err := cfg.Migrate() |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "ERROR: Failed to update config file: %v\n", err) |
||||
os.Exit(1) |
||||
} |
||||
if changes { |
||||
fmt.Printf("Config file format has changed since last update - your config was updated to reflect the changes.\n") |
||||
} |
||||
} |
||||
|
||||
func migrateHistory() { |
||||
// homeDir, err := os.UserHomeDir()
|
||||
// if err != nil {
|
||||
|
||||
// }
|
||||
|
||||
// TODO: Find history in:
|
||||
// - xdg_data/resh/history.reshjson
|
||||
// - .resh_history.json
|
||||
// - .resh/history.json
|
||||
} |
||||
@ -0,0 +1,71 @@ |
||||
package datadir |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"path" |
||||
) |
||||
|
||||
// You should not need this caching
|
||||
// It messes with proper dependency injection
|
||||
// Find another way
|
||||
|
||||
// type dirCache struct {
|
||||
// dir string
|
||||
// err error
|
||||
//
|
||||
// cached bool
|
||||
// }
|
||||
//
|
||||
// var cache dirCache
|
||||
//
|
||||
// func getPathNoCache() (string, error) {
|
||||
// reshDir := "resh"
|
||||
// xdgDir, found := os.LookupEnv("XDG_DATA_HOME")
|
||||
// if found {
|
||||
// return path.Join(xdgDir, reshDir), nil
|
||||
// }
|
||||
// homeDir, err := os.UserHomeDir()
|
||||
// if err != nil {
|
||||
// return "", fmt.Errorf("error while getting home dir: %w", err)
|
||||
// }
|
||||
// return path.Join(homeDir, ".local/share/", reshDir), nil
|
||||
// }
|
||||
//
|
||||
// func GetPath() (string, error) {
|
||||
// if !cache.cached {
|
||||
// dir, err := getPathNoCache()
|
||||
// cache = dirCache{
|
||||
// dir: dir,
|
||||
// err: err,
|
||||
// cached: true,
|
||||
// }
|
||||
// }
|
||||
// return cache.dir, cache.err
|
||||
// }
|
||||
|
||||
func GetPath() (string, error) { |
||||
reshDir := "resh" |
||||
xdgDir, found := os.LookupEnv("XDG_DATA_HOME") |
||||
if found { |
||||
return path.Join(xdgDir, reshDir), nil |
||||
} |
||||
homeDir, err := os.UserHomeDir() |
||||
if err != nil { |
||||
return "", fmt.Errorf("error while getting home dir: %w", err) |
||||
} |
||||
return path.Join(homeDir, ".local/share/", reshDir), nil |
||||
} |
||||
|
||||
func MakePath() (string, error) { |
||||
path, err := GetPath() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
err = os.MkdirAll(path, 0755) |
||||
// skip "exists" error
|
||||
if err != nil && !os.IsExist(err) { |
||||
return "", fmt.Errorf("error while creating directories: %w", err) |
||||
} |
||||
return path, nil |
||||
} |
||||
@ -0,0 +1,18 @@ |
||||
package deviceid |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"path" |
||||
"strings" |
||||
) |
||||
|
||||
func Get(dataDir string) (string, error) { |
||||
fname := "device-id" |
||||
dat, err := os.ReadFile(path.Join(dataDir, fname)) |
||||
if err != nil { |
||||
return "", fmt.Errorf("could not read file with device-id: %w", err) |
||||
} |
||||
id := strings.TrimRight(string(dat), "\n") |
||||
return id, nil |
||||
} |
||||
@ -0,0 +1,56 @@ |
||||
package histio |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"sync" |
||||
|
||||
"github.com/curusarn/resh/internal/recio" |
||||
"github.com/curusarn/resh/internal/recordint" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
type histfile struct { |
||||
sugar *zap.SugaredLogger |
||||
// deviceID string
|
||||
path string |
||||
|
||||
mu sync.RWMutex |
||||
data []recordint.Indexed |
||||
fileinfo os.FileInfo |
||||
} |
||||
|
||||
func newHistfile(sugar *zap.SugaredLogger, path string) *histfile { |
||||
return &histfile{ |
||||
sugar: sugar.With( |
||||
// FIXME: drop V1 once original histfile is gone
|
||||
"component", "histfileV1", |
||||
"path", path, |
||||
), |
||||
// deviceID: deviceID,
|
||||
path: path, |
||||
} |
||||
} |
||||
|
||||
func (h *histfile) updateFromFile() error { |
||||
rio := recio.New(h.sugar) |
||||
// TODO: decide and handle errors
|
||||
newData, _, err := rio.ReadFile(h.path) |
||||
if err != nil { |
||||
return fmt.Errorf("could not read history file: %w", err) |
||||
} |
||||
h.mu.Lock() |
||||
defer h.mu.Unlock() |
||||
h.data = newData |
||||
h.updateFileInfo() |
||||
return nil |
||||
} |
||||
|
||||
func (h *histfile) updateFileInfo() error { |
||||
info, err := os.Stat(h.path) |
||||
if err != nil { |
||||
return fmt.Errorf("history file not found: %w", err) |
||||
} |
||||
h.fileinfo = info |
||||
return nil |
||||
} |
||||
@ -0,0 +1,44 @@ |
||||
package histio |
||||
|
||||
import ( |
||||
"path" |
||||
|
||||
"github.com/curusarn/resh/internal/record" |
||||
"github.com/curusarn/resh/internal/recordint" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
type Histio struct { |
||||
sugar *zap.SugaredLogger |
||||
histDir string |
||||
|
||||
thisDeviceID string |
||||
thisHistory *histfile |
||||
// TODO: remote histories
|
||||
// moreHistories map[string]*histfile
|
||||
|
||||
recordsToAppend chan record.V1 |
||||
recordsToFlag chan recordint.Flag |
||||
} |
||||
|
||||
func New(sugar *zap.SugaredLogger, dataDir, deviceID string) *Histio { |
||||
sugarHistio := sugar.With(zap.String("component", "histio")) |
||||
histDir := path.Join(dataDir, "history") |
||||
currPath := path.Join(histDir, deviceID) |
||||
// TODO: file extenstion for the history, yes or no? (<id>.reshjson vs. <id>)
|
||||
|
||||
// TODO: discover other history files, exclude current
|
||||
|
||||
return &Histio{ |
||||
sugar: sugarHistio, |
||||
histDir: histDir, |
||||
|
||||
thisDeviceID: deviceID, |
||||
thisHistory: newHistfile(sugar, currPath), |
||||
// moreHistories: ...
|
||||
} |
||||
} |
||||
|
||||
func (h *Histio) Append(r *record.V1) { |
||||
|
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
package recconv |
||||
|
||||
import "github.com/curusarn/resh/internal/record" |
||||
|
||||
func LegacyToV1(r *record.Legacy) *record.V1 { |
||||
return &record.V1{ |
||||
// FIXME: fill in all the fields
|
||||
} |
||||
} |
||||
@ -0,0 +1,158 @@ |
||||
package recio |
||||
|
||||
import ( |
||||
"bufio" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"os" |
||||
"strings" |
||||
|
||||
"github.com/curusarn/resh/internal/recconv" |
||||
"github.com/curusarn/resh/internal/record" |
||||
"github.com/curusarn/resh/internal/recordint" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
func (r *RecIO) ReadAndFixFile(fpath string, maxErrors int) ([]recordint.Indexed, error) { |
||||
recs, numErrs, err := r.ReadFile(fpath) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if numErrs > maxErrors { |
||||
return nil, fmt.Errorf("encountered too many decoding errors") |
||||
} |
||||
if numErrs == 0 { |
||||
return recs, nil |
||||
} |
||||
|
||||
// TODO: check there error messages
|
||||
r.sugar.Warnw("Some history records could not be decoded - fixing resh history file by dropping them", |
||||
"corruptedRecords", numErrs, |
||||
) |
||||
fpathBak := fpath + ".bak" |
||||
r.sugar.Infow("Backing up current corrupted history file", |
||||
"backupFilename", fpathBak, |
||||
) |
||||
// TODO: maybe use upstram copy function
|
||||
err = copyFile(fpath, fpathBak) |
||||
if err != nil { |
||||
r.sugar.Errorw("Failed to create a backup history file - aborting fixing history file", |
||||
"backupFilename", fpathBak, |
||||
zap.Error(err), |
||||
) |
||||
return recs, nil |
||||
} |
||||
r.sugar.Info("Writing resh history file without errors ...") |
||||
var recsV1 []record.V1 |
||||
for _, rec := range recs { |
||||
recsV1 = append(recsV1, rec.Rec) |
||||
} |
||||
err = r.WriteFile(fpath, recsV1) |
||||
if err != nil { |
||||
r.sugar.Errorw("Failed write fixed history file - aborting fixing history file", |
||||
"filename", fpath, |
||||
zap.Error(err), |
||||
) |
||||
} |
||||
return recs, nil |
||||
} |
||||
|
||||
func (r *RecIO) ReadFile(fpath string) ([]recordint.Indexed, int, error) { |
||||
var recs []recordint.Indexed |
||||
file, err := os.Open(fpath) |
||||
if err != nil { |
||||
return nil, 0, fmt.Errorf("failed to open history file: %w", err) |
||||
} |
||||
defer file.Close() |
||||
|
||||
reader := bufio.NewReader(file) |
||||
numErrs := 0 |
||||
var idx int |
||||
for { |
||||
var line string |
||||
line, err = reader.ReadString('\n') |
||||
if err != nil { |
||||
break |
||||
} |
||||
idx++ |
||||
rec, err := r.decodeLine(line) |
||||
if err != nil { |
||||
numErrs++ |
||||
continue |
||||
} |
||||
recidx := recordint.Indexed{ |
||||
Rec: *rec, |
||||
// TODO: Is line index actually enough?
|
||||
// Don't we want to count bytes because we will scan by number of bytes?
|
||||
// hint: https://benjamincongdon.me/blog/2018/04/10/Counting-Scanned-Bytes-in-Go/
|
||||
Idx: idx, |
||||
} |
||||
recs = append(recs, recidx) |
||||
} |
||||
if err != io.EOF { |
||||
r.sugar.Error("Error while loading file", zap.Error(err)) |
||||
} |
||||
r.sugar.Infow("Loaded resh history records", |
||||
"recordCount", len(recs), |
||||
) |
||||
return recs, numErrs, nil |
||||
} |
||||
|
||||
func copyFile(source, dest string) error { |
||||
from, err := os.Open(source) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer from.Close() |
||||
|
||||
// This is equivalnet to: os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666)
|
||||
to, err := os.Create(dest) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer to.Close() |
||||
|
||||
_, err = io.Copy(to, from) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (r *RecIO) decodeLine(line string) (*record.V1, error) { |
||||
idx := strings.Index(line, "{") |
||||
if idx == -1 { |
||||
return nil, fmt.Errorf("no openning brace found") |
||||
} |
||||
schema := line[:idx] |
||||
jsn := line[idx:] |
||||
switch schema { |
||||
case "v1": |
||||
var rec record.V1 |
||||
err := decodeAnyRecord(jsn, &rec) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &rec, nil |
||||
case "": |
||||
var rec record.Legacy |
||||
err := decodeAnyRecord(jsn, &rec) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return recconv.LegacyToV1(&rec), nil |
||||
default: |
||||
return nil, fmt.Errorf("unknown record schema/type '%s'", schema) |
||||
} |
||||
} |
||||
|
||||
// TODO: find out if we are loosing performance because of the use of interface{}
|
||||
|
||||
func decodeAnyRecord(jsn string, rec interface{}) error { |
||||
err := json.Unmarshal([]byte(jsn), &rec) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to decode json: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
package recio |
||||
|
||||
import ( |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
type RecIO struct { |
||||
sugar *zap.SugaredLogger |
||||
} |
||||
|
||||
func New(sugar *zap.SugaredLogger) RecIO { |
||||
return RecIO{sugar: sugar} |
||||
} |
||||
@ -0,0 +1,46 @@ |
||||
package recio |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/internal/record" |
||||
"github.com/curusarn/resh/internal/recordint" |
||||
) |
||||
|
||||
// TODO: better errors
|
||||
func (r *RecIO) WriteFile(fpath string, data []record.V1) error { |
||||
file, err := os.Create(fpath) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer file.Close() |
||||
for _, rec := range data { |
||||
jsn, err := encodeV1Record(rec) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
_, err = file.Write(jsn) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (r *RecIO) EditRecordFlagsInFile(fpath string, idx int, rec recordint.Flag) error { |
||||
// FIXME: implement
|
||||
// open file "not as append"
|
||||
// scan to the correct line
|
||||
|
||||
return nil |
||||
} |
||||
|
||||
func encodeV1Record(rec record.V1) ([]byte, error) { |
||||
jsn, err := json.Marshal(rec) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to encode json: %w", err) |
||||
} |
||||
return append(jsn, []byte("\n")...), nil |
||||
} |
||||
@ -0,0 +1 @@ |
||||
package recload |
||||
@ -0,0 +1,88 @@ |
||||
package record |
||||
|
||||
type Legacy struct { |
||||
// core
|
||||
CmdLine string `json:"cmdLine"` |
||||
ExitCode int `json:"exitCode"` |
||||
Shell string `json:"shell"` |
||||
Uname string `json:"uname"` |
||||
SessionID string `json:"sessionId"` |
||||
RecordID string `json:"recordId"` |
||||
|
||||
// posix
|
||||
Home string `json:"home"` |
||||
Lang string `json:"lang"` |
||||
LcAll string `json:"lcAll"` |
||||
Login string `json:"login"` |
||||
Pwd string `json:"pwd"` |
||||
PwdAfter string `json:"pwdAfter"` |
||||
ShellEnv string `json:"shellEnv"` |
||||
Term string `json:"term"` |
||||
|
||||
// non-posix"`
|
||||
RealPwd string `json:"realPwd"` |
||||
RealPwdAfter string `json:"realPwdAfter"` |
||||
Pid int `json:"pid"` |
||||
SessionPID int `json:"sessionPid"` |
||||
Host string `json:"host"` |
||||
Hosttype string `json:"hosttype"` |
||||
Ostype string `json:"ostype"` |
||||
Machtype string `json:"machtype"` |
||||
Shlvl int `json:"shlvl"` |
||||
|
||||
// before after
|
||||
TimezoneBefore string `json:"timezoneBefore"` |
||||
TimezoneAfter string `json:"timezoneAfter"` |
||||
|
||||
RealtimeBefore float64 `json:"realtimeBefore"` |
||||
RealtimeAfter float64 `json:"realtimeAfter"` |
||||
RealtimeBeforeLocal float64 `json:"realtimeBeforeLocal"` |
||||
RealtimeAfterLocal float64 `json:"realtimeAfterLocal"` |
||||
|
||||
RealtimeDuration float64 `json:"realtimeDuration"` |
||||
RealtimeSinceSessionStart float64 `json:"realtimeSinceSessionStart"` |
||||
RealtimeSinceBoot float64 `json:"realtimeSinceBoot"` |
||||
|
||||
GitDir string `json:"gitDir"` |
||||
GitRealDir string `json:"gitRealDir"` |
||||
GitOriginRemote string `json:"gitOriginRemote"` |
||||
GitDirAfter string `json:"gitDirAfter"` |
||||
GitRealDirAfter string `json:"gitRealDirAfter"` |
||||
GitOriginRemoteAfter string `json:"gitOriginRemoteAfter"` |
||||
MachineID string `json:"machineId"` |
||||
|
||||
OsReleaseID string `json:"osReleaseId"` |
||||
OsReleaseVersionID string `json:"osReleaseVersionId"` |
||||
OsReleaseIDLike string `json:"osReleaseIdLike"` |
||||
OsReleaseName string `json:"osReleaseName"` |
||||
OsReleasePrettyName string `json:"osReleasePrettyName"` |
||||
|
||||
ReshUUID string `json:"reshUuid"` |
||||
ReshVersion string `json:"reshVersion"` |
||||
ReshRevision string `json:"reshRevision"` |
||||
|
||||
// records come in two parts (collect and postcollect)
|
||||
PartOne bool `json:"partOne,omitempty"` // false => part two
|
||||
PartsMerged bool `json:"partsMerged"` |
||||
// special flag -> not an actual record but an session end
|
||||
SessionExit bool `json:"sessionExit,omitempty"` |
||||
|
||||
// recall metadata
|
||||
Recalled bool `json:"recalled"` |
||||
RecallHistno int `json:"recallHistno,omitempty"` |
||||
RecallStrategy string `json:"recallStrategy,omitempty"` |
||||
RecallActionsRaw string `json:"recallActionsRaw,omitempty"` |
||||
RecallActions []string `json:"recallActions,omitempty"` |
||||
RecallLastCmdLine string `json:"recallLastCmdLine"` |
||||
|
||||
// recall command
|
||||
RecallPrefix string `json:"recallPrefix,omitempty"` |
||||
|
||||
// added by sanitizatizer
|
||||
Sanitized bool `json:"sanitized,omitempty"` |
||||
CmdLength int `json:"cmdLength,omitempty"` |
||||
|
||||
// fields that are string here and int in older resh verisons
|
||||
Cols interface{} `json:"cols"` |
||||
Lines interface{} `json:"lines"` |
||||
} |
||||
@ -0,0 +1,2 @@ |
||||
// Package record provides record types that are used in resh history files
|
||||
package record |
||||
@ -0,0 +1,58 @@ |
||||
package record |
||||
|
||||
type V1 struct { |
||||
// flags
|
||||
// deleted, favorite
|
||||
// FIXME: is this the best way? .. what about string, separate fields, or something similar
|
||||
Flags int `json:"flags"` |
||||
|
||||
DeviceID string `json:"deviceID"` |
||||
SessionID string `json:"sessionID"` |
||||
// can we have a shorter uuid for record
|
||||
RecordID string `json:"recordID"` |
||||
|
||||
// cmdline, exitcode
|
||||
CmdLine string `json:"cmdLine"` |
||||
ExitCode int `json:"exitCode"` |
||||
|
||||
// paths
|
||||
Home string `json:"home"` |
||||
Pwd string `json:"pwd"` |
||||
RealPwd string `json:"realPwd"` |
||||
|
||||
// hostname + lognem (not sure if we actually need logname)
|
||||
Logname string `json:"logname"` |
||||
Hostname string `json:"hostname"` |
||||
|
||||
// git info
|
||||
// origin is the most important
|
||||
GitOriginRemote string `json:"gitOriginRemote"` |
||||
// maybe branch could be useful - e.g. in monorepo ??
|
||||
GitBranch string `json:"gitBranch"` |
||||
|
||||
// what is this for ??
|
||||
// session watching needs this
|
||||
// but I'm not sure if we need to save it
|
||||
// records belong to sessions
|
||||
// PID int `json:"pid"`
|
||||
// needed for tracking of sessions but I think it shouldn't be part of V1
|
||||
SessionPID int `json:"sessionPID"` |
||||
|
||||
// needed to because records are merged with parts with same "SessionID + Shlvl"
|
||||
// I don't think we need to save it
|
||||
Shlvl int `json:"shlvl"` |
||||
|
||||
// time (before), duration of command
|
||||
Time float64 `json:"time"` |
||||
Duration float64 `json:"duration"` |
||||
|
||||
// these look like internal stuff
|
||||
|
||||
// records come in two parts (collect and postcollect)
|
||||
PartOne bool `json:"partOne,omitempty"` // false => part two
|
||||
PartsNotMerged bool `json:"partsNotMerged,omitempty"` |
||||
|
||||
// special flag -> not an actual record but an session end
|
||||
// TODO: this shouldn't be part of serializable V1 record
|
||||
SessionExit bool `json:"sessionExit,omitempty"` |
||||
} |
||||
@ -0,0 +1,21 @@ |
||||
package recordint |
||||
|
||||
import "github.com/curusarn/resh/internal/record" |
||||
|
||||
type Collect struct { |
||||
// record merging
|
||||
SessionID string |
||||
Shlvl int |
||||
// session watching
|
||||
SessionPID int |
||||
|
||||
Rec record.V1 |
||||
} |
||||
|
||||
type Postcollect struct { |
||||
// record merging
|
||||
SessionID string |
||||
Shlvl int |
||||
// session watching
|
||||
SessionPID int |
||||
} |
||||
@ -0,0 +1,51 @@ |
||||
package recordint |
||||
|
||||
import ( |
||||
"github.com/curusarn/resh/internal/record" |
||||
"github.com/curusarn/resh/internal/recutil" |
||||
) |
||||
|
||||
// TODO: This all seems excessive
|
||||
// TODO: V1 should be converted directly to SearchApp record
|
||||
|
||||
// EnrichedRecord - record enriched with additional data
|
||||
type Enriched struct { |
||||
// TODO: think about if it really makes sense to have this based on V1
|
||||
record.V1 |
||||
|
||||
// TODO: drop some/all of this
|
||||
// enriching fields - added "later"
|
||||
Command string `json:"command"` |
||||
FirstWord string `json:"firstWord"` |
||||
Invalid bool `json:"invalid"` |
||||
SeqSessionID uint64 `json:"seqSessionId"` |
||||
LastRecordOfSession bool `json:"lastRecordOfSession"` |
||||
DebugThisRecord bool `json:"debugThisRecord"` |
||||
Errors []string `json:"errors"` |
||||
// SeqSessionID uint64 `json:"seqSessionId,omitempty"`
|
||||
} |
||||
|
||||
// Enriched - returns enriched record
|
||||
func NewEnrichedFromV1(r *record.V1) Enriched { |
||||
rec := Enriched{Record: r} |
||||
// normlize git remote
|
||||
rec.GitOriginRemote = NormalizeGitRemote(rec.GitOriginRemote) |
||||
rec.GitOriginRemoteAfter = NormalizeGitRemote(rec.GitOriginRemoteAfter) |
||||
// Get command/first word from commandline
|
||||
var err error |
||||
err = recutil.Validate(r) |
||||
if err != nil { |
||||
rec.Errors = append(rec.Errors, "Validate error:"+err.Error()) |
||||
// rec, _ := record.ToString()
|
||||
// sugar.Println("Invalid command:", rec)
|
||||
rec.Invalid = true |
||||
} |
||||
rec.Command, rec.FirstWord, err = GetCommandAndFirstWord(r.CmdLine) |
||||
if err != nil { |
||||
rec.Errors = append(rec.Errors, "GetCommandAndFirstWord error:"+err.Error()) |
||||
// rec, _ := record.ToString()
|
||||
// sugar.Println("Invalid command:", rec)
|
||||
rec.Invalid = true // should this be really invalid ?
|
||||
} |
||||
return rec |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
package recordint |
||||
|
||||
type Flag struct { |
||||
deviceID string |
||||
recordID string |
||||
|
||||
flagDeleted bool |
||||
flagFavourite bool |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
package recordint |
||||
|
||||
import "github.com/curusarn/resh/internal/record" |
||||
|
||||
// Indexed record allows us to find records in history file in order to edit them
|
||||
type Indexed struct { |
||||
Rec record.V1 |
||||
Idx int |
||||
} |
||||
@ -0,0 +1,2 @@ |
||||
// Package recordint provides internal record types that are passed between resh components
|
||||
package recordint |
||||
@ -0,0 +1,40 @@ |
||||
package recordint |
||||
|
||||
// SearchApp record used for sending records to RESH-CLI
|
||||
type SearchApp struct { |
||||
IsRaw bool |
||||
SessionID string |
||||
DeviceID string |
||||
|
||||
CmdLine string |
||||
Host string |
||||
Pwd string |
||||
Home string // helps us to collapse /home/user to tilde
|
||||
GitOriginRemote string |
||||
ExitCode int |
||||
|
||||
Time float64 |
||||
} |
||||
|
||||
// NewCliRecordFromCmdLine
|
||||
func NewSearchAppFromCmdLine(cmdLine string) SearchApp { |
||||
return SearchApp{ |
||||
IsRaw: true, |
||||
CmdLine: cmdLine, |
||||
} |
||||
} |
||||
|
||||
// NewCliRecord from EnrichedRecord
|
||||
func NewSearchApp(r *Enriched) SearchApp { |
||||
return SearchApp{ |
||||
IsRaw: false, |
||||
SessionID: r.SessionID, |
||||
CmdLine: r.CmdLine, |
||||
Host: r.Hostname, |
||||
Pwd: r.Pwd, |
||||
Home: r.Home, |
||||
GitOriginRemote: r.GitOriginRemote, |
||||
ExitCode: r.ExitCode, |
||||
Time: r.Time, |
||||
} |
||||
} |
||||
@ -0,0 +1,94 @@ |
||||
package recutil |
||||
|
||||
import ( |
||||
"errors" |
||||
"net/url" |
||||
"strings" |
||||
|
||||
"github.com/curusarn/resh/internal/record" |
||||
"github.com/mattn/go-shellwords" |
||||
giturls "github.com/whilp/git-urls" |
||||
) |
||||
|
||||
// NormalizeGitRemote helper
|
||||
func NormalizeGitRemote(gitRemote string) string { |
||||
if strings.HasSuffix(gitRemote, ".git") { |
||||
gitRemote = gitRemote[:len(gitRemote)-4] |
||||
} |
||||
parsedURL, err := giturls.Parse(gitRemote) |
||||
if err != nil { |
||||
// TODO: log this error
|
||||
return gitRemote |
||||
} |
||||
if parsedURL.User == nil || parsedURL.User.Username() == "" { |
||||
parsedURL.User = url.User("git") |
||||
} |
||||
// TODO: figure out what scheme we want
|
||||
parsedURL.Scheme = "git+ssh" |
||||
return parsedURL.String() |
||||
} |
||||
|
||||
// Validate returns error if the record is invalid
|
||||
func Validate(r *record.V1) error { |
||||
if r.CmdLine == "" { |
||||
return errors.New("There is no CmdLine") |
||||
} |
||||
if r.RealtimeBefore == 0 || r.RealtimeAfter == 0 { |
||||
return errors.New("There is no Time") |
||||
} |
||||
if r.RealtimeBeforeLocal == 0 || r.RealtimeAfterLocal == 0 { |
||||
return errors.New("There is no Local Time") |
||||
} |
||||
if r.RealPwd == "" || r.RealPwdAfter == "" { |
||||
return errors.New("There is no Real Pwd") |
||||
} |
||||
if r.Pwd == "" || r.PwdAfter == "" { |
||||
return errors.New("There is no Pwd") |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Merge two records (part1 - collect + part2 - postcollect)
|
||||
func Merge(r1 *record.V1, r2 *record.V1) error { |
||||
if r1.PartOne == false || r2.PartOne { |
||||
return errors.New("Expected part1 and part2 of the same record - usage: Merge(part1, part2)") |
||||
} |
||||
if r1.SessionID != r2.SessionID { |
||||
return errors.New("Records to merge are not from the same sesion - r1:" + r1.SessionID + " r2:" + r2.SessionID) |
||||
} |
||||
if r1.CmdLine != r2.CmdLine { |
||||
return errors.New("Records to merge are not parts of the same records - r1:" + r1.CmdLine + " r2:" + r2.CmdLine) |
||||
} |
||||
if r1.RecordID != r2.RecordID { |
||||
return errors.New("Records to merge do not have the same ID - r1:" + r1.RecordID + " r2:" + r2.RecordID) |
||||
} |
||||
r1.ExitCode = r2.ExitCode |
||||
r1.Duration = r2.Duration |
||||
|
||||
r1.PartsMerged = true |
||||
r1.PartOne = false |
||||
return nil |
||||
} |
||||
|
||||
// GetCommandAndFirstWord func
|
||||
func GetCommandAndFirstWord(cmdLine string) (string, string, error) { |
||||
args, err := shellwords.Parse(cmdLine) |
||||
if err != nil { |
||||
// Println("shellwords Error:", err, " (cmdLine: <", cmdLine, "> )")
|
||||
return "", "", err |
||||
} |
||||
if len(args) == 0 { |
||||
return "", "", nil |
||||
} |
||||
i := 0 |
||||
for true { |
||||
// commands in shell sometimes look like this `variable=something command argument otherArgument --option`
|
||||
// to get the command we skip over tokens that contain '='
|
||||
if strings.ContainsRune(args[i], '=') && len(args) > i+1 { |
||||
i++ |
||||
continue |
||||
} |
||||
return args[i], args[0], nil |
||||
} |
||||
return "ERROR", "ERROR", errors.New("failed to retrieve first word of command") |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue