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