plc4go/tools/plc4xpcapanalyzer/ui/commands.go (681 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package ui import ( "context" "fmt" "os" "path" "reflect" "runtime/debug" "strings" "time" plc4xconfig "github.com/apache/plc4x/plc4go/pkg/api/config" "github.com/apache/plc4x/plc4go/spi" "github.com/pkg/errors" "github.com/rivo/tview" "github.com/rs/zerolog" "github.com/rs/zerolog/log" cliConfig "github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/config" "github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/internal/analyzer" "github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/internal/extractor" ) const rootCommandIndicator = "rootCommand" var rootCommand = Command{ Name: rootCommandIndicator, subCommands: []Command{ { Name: "ls", Description: "list directories", action: func(_ context.Context, _ Command, dir string) error { if dir == "" { dir = currentDir } _, _ = fmt.Fprintf(commandOutput, "dir cotents of %s\n", dir) readDir, err := os.ReadDir(dir) if err != nil { return err } for _, dirEntry := range readDir { isDir := dirEntry.IsDir() name := dirEntry.Name() name = strings.TrimPrefix(name, dir) if isDir { name = fmt.Sprintf("[#0000ff]%s[white]", name) } else if strings.HasSuffix(name, ".pcap") || strings.HasSuffix(name, ".pcapng") { name = fmt.Sprintf("[#00ff00]%s[white]", name) } _, _ = fmt.Fprintf(commandOutput, "%s\n", name) } return nil }, // TODO: add parameter suggestions }, { Name: "cd", Description: "changes directory", action: func(_ context.Context, _ Command, newDir string) error { var proposedCurrentDir string if newDir == "" { var err error proposedCurrentDir, err = os.UserHomeDir() if err != nil { return err } } else if strings.HasPrefix(newDir, "."+string(os.PathSeparator)) { proposedCurrentDir = currentDir + strings.TrimPrefix(newDir, ".") } else if strings.HasPrefix(newDir, ""+string(os.PathSeparator)) { proposedCurrentDir = newDir } else { proposedCurrentDir = currentDir + string(os.PathSeparator) + newDir } stat, err := os.Stat(proposedCurrentDir) if err != nil { return err } if !stat.IsDir() { return errors.Errorf("%s is not a dir", newDir) } currentDir = proposedCurrentDir _, _ = fmt.Fprintf(commandOutput, "current directory: %s\n", currentDir) return nil }, parameterSuggestions: func(currentText string) (entries []string) { if strings.HasPrefix(currentText, string(os.PathSeparator)) { dirEntries, err := os.ReadDir(currentText) if err != nil { plc4xpcapanalyzerLog.Warn().Err(err).Msg("Error suggesting directories") return } for _, dirEntry := range dirEntries { entry := path.Join(currentText, dirEntry.Name()) entries = append(entries, entry) } } else { dirEntries, err := os.ReadDir(currentDir) if err != nil { plc4xpcapanalyzerLog.Warn().Err(err).Msg("Error suggesting directories") return } for _, dirEntry := range dirEntries { entry := path.Join(".", dirEntry.Name()) entries = append(entries, entry) } } return }, }, { Name: "pwd", Description: "shows current directory", action: func(_ context.Context, _ Command, _ string) error { _, _ = fmt.Fprintf(commandOutput, "current directory: %s\n", currentDir) return nil }, }, { Name: "open", Description: "open file", action: func(_ context.Context, _ Command, pcapFile string) error { return OpenFile(pcapFile) }, parameterSuggestions: func(currentText string) (entries []string) { entries = append(entries, config.History.Last10Files...) readDir, err := os.ReadDir(currentDir) if err != nil { return } for _, dirEntry := range readDir { name := dirEntry.Name() name = strings.TrimPrefix(name, currentDir) if strings.HasSuffix(dirEntry.Name(), ".cap") || strings.HasSuffix(dirEntry.Name(), ".pcap") || strings.HasSuffix(name, ".pcapng") { entries = append(entries, name) } } return }, }, { Name: "analyze", Description: "Analyzes a pcap file using a driver", action: func(ctx context.Context, _ Command, protocolTypeAndPcapFile string) error { split := strings.Split(protocolTypeAndPcapFile, " ") if len(split) != 2 { return errors.Errorf("expect protocol and pcapfile") } protocolType := split[0] pcapFile := strings.TrimPrefix(protocolTypeAndPcapFile, protocolType+" ") cliConfig.PcapConfigInstance.Client = config.HostIp cliConfig.RootConfigInstance.HideProgressBar = true // disabled as we get this output anyway with the message call back //cliConfig.RootConfigInstance.Verbosity = 4 return analyzer.AnalyzeWithOutputAndCallback(ctx, pcapFile, protocolType, tview.ANSIWriter(messageOutput), tview.ANSIWriter(messageOutput), func(parsed spi.Message) { spiNumberOfMessagesReceived++ spiMessageReceived(spiNumberOfMessagesReceived, time.Now(), parsed) }) }, parameterSuggestions: func(currentText string) (entries []string) { for _, file := range loadedPcapFiles { for _, protocol := range protocolList { entries = append(entries, protocol+" "+file.path) } } return }, }, { Name: "extract", Description: "Extract a pcap file using a driver", action: func(ctx context.Context, _ Command, protocolTypeAndPcapFile string) error { split := strings.Split(protocolTypeAndPcapFile, " ") if len(split) != 2 { return errors.Errorf("expect protocol and pcapfile") } protocolType := split[0] pcapFile := strings.TrimPrefix(protocolTypeAndPcapFile, protocolType+" ") cliConfig.PcapConfigInstance.Client = config.HostIp cliConfig.RootConfigInstance.HideProgressBar = true cliConfig.RootConfigInstance.Verbosity = 4 return extractor.ExtractWithOutput(ctx, pcapFile, protocolType, tview.ANSIWriter(messageOutput), tview.ANSIWriter(messageOutput)) }, parameterSuggestions: func(currentText string) (entries []string) { for _, file := range loadedPcapFiles { for _, protocol := range protocolList { entries = append(entries, protocol+" "+file.path) } } return }, }, { Name: "host", Description: "The host which is assumed to be the sender (important for protocols that are directional)", subCommands: []Command{ { Name: "set", action: func(_ context.Context, _ Command, host string) error { config.HostIp = host return nil }, }, { Name: "get", action: func(_ context.Context, _ Command, host string) error { _, _ = fmt.Fprintf(commandOutput, "current set host %s", config.HostIp) return nil }, }, }, }, { Name: "register", Description: "register a driver in the subsystem", action: func(_ context.Context, _ Command, driver string) error { return registerDriver(driver) }, parameterSuggestions: func(currentText string) (entries []string) { for _, protocol := range protocolList { if strings.HasPrefix(protocol, currentText) { entries = append(entries, protocol) } } return }, }, { Name: "quit", Description: "Quits the application", }, { Name: "log", Description: "Log related operations", subCommands: []Command{ { Name: "get", Description: "Get a log level", action: func(_ context.Context, _ Command, _ string) error { _, _ = fmt.Fprintf(commandOutput, "Current log level %s", log.Logger.GetLevel()) return nil }, }, { Name: "set", Description: "Sets a log level", action: func(_ context.Context, _ Command, level string) error { parseLevel, err := zerolog.ParseLevel(level) if err != nil { return errors.Wrapf(err, "Error setting log level") } setLevel(parseLevel) log.Logger = log.Logger.Level(parseLevel) return nil }, parameterSuggestions: func(currentText string) (entries []string) { levels := []string{ zerolog.LevelTraceValue, zerolog.LevelDebugValue, zerolog.LevelInfoValue, zerolog.LevelWarnValue, zerolog.LevelErrorValue, zerolog.LevelFatalValue, zerolog.LevelPanicValue, } for _, level := range levels { entries = append(entries, level) } return }, }, }, }, { Name: "conf", Description: "Various settings for plc4xpcapanalyzer", subCommands: []Command{ { Name: "list", Description: "list config values with their current settings", action: func(_ context.Context, _ Command, _ string) error { allCliConfigsValue := reflect.ValueOf(allCliConfigsInstances) for i := 0; i < allCliConfigsValue.NumField(); i++ { allConfigField := allCliConfigsValue.Field(i) allConfigFieldType := allCliConfigsValue.Type().Field(i) _, _ = fmt.Fprintf(commandOutput, "%s:\n", allConfigFieldType.Name) configInstanceReflectValue := reflect.ValueOf(allConfigField.Interface()) if configInstanceReflectValue.Kind() == reflect.Ptr { configInstanceReflectValue = configInstanceReflectValue.Elem() } for j := 0; j < configInstanceReflectValue.NumField(); j++ { configField := configInstanceReflectValue.Field(j) configFieldType := configInstanceReflectValue.Type().Field(j) if configFieldType.Tag.Get("json") == "-" { // Ignore those continue } _, _ = fmt.Fprintf(commandOutput, " %s: %s\t= %v\n", configFieldType.Name, configFieldType.Type, configField.Interface()) } } return nil }, }, { Name: "set", Description: "sets a config value", subCommands: func() []Command { var configCommand []Command allCliConfigsValue := reflect.ValueOf(allCliConfigsInstances) for i := 0; i < allCliConfigsValue.NumField(); i++ { allConfigField := allCliConfigsValue.Field(i) allConfigFieldType := allCliConfigsValue.Type().Field(i) configCommand = append(configCommand, Command{ Name: allConfigFieldType.Name, Description: fmt.Sprintf("Setting for %s", allConfigFieldType.Name), subCommands: func() []Command { var configElementCommands []Command configInstanceReflectValue := reflect.ValueOf(allConfigField.Interface()) if configInstanceReflectValue.Kind() == reflect.Ptr { configInstanceReflectValue = configInstanceReflectValue.Elem() } for i := 0; i < configInstanceReflectValue.NumField(); i++ { field := configInstanceReflectValue.Field(i) fieldOfType := configInstanceReflectValue.Type().Field(i) if fieldOfType.Tag.Get("json") == "-" { // Ignore those continue } configElementCommands = append(configElementCommands, Command{ Name: fieldOfType.Name, Description: fmt.Sprintf("Sets value for %s", fieldOfType.Name), action: func(_ context.Context, _ Command, argument string) error { field.SetString(argument) return nil }, }) } return configElementCommands }(), }) } return configCommand }(), }, { Name: "plc4xpcapanalyzer-debug", Description: "Prints out debug information of the pcap analyzer itself", subCommands: []Command{ { Name: "on", Description: "debug on", action: func(_ context.Context, _ Command, _ string) error { plc4xpcapanalyzerLog = zerolog.New(zerolog.ConsoleWriter{Out: tview.ANSIWriter(consoleOutput)}) return nil }, }, { Name: "off", Description: "debug off", action: func(_ context.Context, _ Command, _ string) error { plc4xpcapanalyzerLog = zerolog.Nop() return nil }, }, }, }, { Name: "auto-register", Description: "autoregister driver at startup", subCommands: []Command{ { Name: "list", action: func(_ context.Context, currentCommand Command, argument string) error { _, _ = fmt.Fprintf(commandOutput, "Auto-register enabled drivers:\n %s\n", strings.Join(config.AutoRegisterDrivers, "\n ")) return nil }, }, { Name: "enable", action: func(_ context.Context, _ Command, argument string) error { return enableAutoRegister(argument) }, parameterSuggestions: func(currentText string) (entries []string) { for _, protocol := range protocolList { if strings.HasPrefix(protocol, currentText) { entries = append(entries, protocol) } } return }, }, { Name: "disable", action: func(_ context.Context, _ Command, argument string) error { return disableAutoRegister(argument) }, parameterSuggestions: func(currentText string) (entries []string) { for _, protocol := range protocolList { if strings.HasPrefix(protocol, currentText) { entries = append(entries, protocol) } } return }, }, }, }, }, }, { Name: "plc4x-conf", Description: "plc4x related settings", subCommands: []Command{ { Name: "TraceTransactionManagerWorkers", Description: "print information about transaction manager workers", subCommands: []Command{ { Name: "on", Description: "trace on", action: func(_ context.Context, _ Command, _ string) error { plc4xconfig.TraceTransactionManagerWorkers = true return nil }, }, { Name: "off", Description: "trace off", action: func(_ context.Context, _ Command, _ string) error { plc4xconfig.TraceTransactionManagerWorkers = false return nil }, }, }, }, { Name: "TraceTransactionManagerTransactions", Description: "print information about transaction manager transactions", subCommands: []Command{ { Name: "on", Description: "trace on", action: func(_ context.Context, _ Command, _ string) error { plc4xconfig.TraceTransactionManagerTransactions = true return nil }, }, { Name: "off", Description: "trace off", action: func(_ context.Context, _ Command, _ string) error { plc4xconfig.TraceTransactionManagerTransactions = false return nil }, }, }, }, { Name: "TraceDefaultMessageCodecWorker", Description: "print information about message codec workers", subCommands: []Command{ { Name: "on", Description: "trace on", action: func(_ context.Context, _ Command, _ string) error { plc4xconfig.TraceDefaultMessageCodecWorker = true return nil }, }, { Name: "off", Description: "trace off", action: func(_ context.Context, _ Command, _ string) error { plc4xconfig.TraceDefaultMessageCodecWorker = false return nil }, }, }, }, }, }, { Name: "history", Description: "outputs the last commands", action: func(_ context.Context, _ Command, _ string) error { outputCommandHistory() return nil }, }, { Name: "clear", Description: "clear all outputs", action: func(_ context.Context, _ Command, _ string) error { messageOutputClear() consoleOutputClear() commandOutputClear() return nil }, subCommands: []Command{ { Name: "message", Description: "clears message output", action: func(_ context.Context, _ Command, _ string) error { messageOutputClear() return nil }, }, { Name: "console", Description: "clears console output", action: func(_ context.Context, _ Command, _ string) error { consoleOutputClear() return nil }, }, { Name: "command", Description: "clears command output", action: func(_ context.Context, _ Command, _ string) error { commandOutputClear() return nil }, }, }, }, { Name: "abort", Description: "abort currently running jobs", action: func(_ context.Context, _ Command, _ string) error { for _, cancelFunc := range cancelFunctions { cancelFunc() } return nil }, }, }, } func init() { // Because of the cycle we need to define the help command here as it needs access to the to command rootCommand.subCommands = append(rootCommand.subCommands, Command{ Name: "help", Description: "prints out this help", action: func(_ context.Context, _ Command, _ string) error { _, _ = fmt.Fprintf(commandOutput, "[#0000ff]Available commands[white]\n") rootCommand.visit(0, func(currentIndent int, command Command) { indentString := strings.Repeat(" ", currentIndent) description := command.Description if description == "" { description = command.Name + "s" } _, _ = fmt.Fprintf(commandOutput, "%s [#00ff00]%s[white]: %s\n", indentString, command.Name, description) }) return nil }, }) } var NotDirectlyExecutable = errors.New("Not directly executable") type Command struct { Name string Description string action func(ctx context.Context, currentCommand Command, argument string) error subCommands []Command parameterSuggestions func(currentText string) (entries []string) } func (c Command) Completions(currentCommandText string) (entries []string) { if c.Name == rootCommandIndicator && len(currentCommandText) == 0 { // We don't return anything here to not pollute the command text by default return } if c.acceptsCurrentText(currentCommandText) { currentCommandPrefix := c.currentCommandPrefix() doesCommandTextTargetSubCommand := c.doesCommandTextTargetSubCommand(currentCommandPrefix) if c.hasDirectExecution() && !doesCommandTextTargetSubCommand { if c.parameterSuggestions != nil { preparedForParameters := c.prepareForParameters(currentCommandText) for _, parameterSuggestion := range c.parameterSuggestions(preparedForParameters) { entries = append(entries, currentCommandPrefix+parameterSuggestion) } } else if currentCommandText == "" { entries = append(entries, c.Name) } } if doesCommandTextTargetSubCommand { remainder := c.prepareForSubCommand(currentCommandText) for _, command := range c.subCommands { for _, subCommandCompletions := range command.Completions(remainder) { entries = append(entries, currentCommandPrefix+subCommandCompletions) } } } } else if strings.HasPrefix(c.Name, currentCommandText) { // Suggest ourselves if we start with the current letter entries = append(entries, c.Name) } return } func (c Command) acceptsCurrentText(currentCommandText string) bool { if c.Name == rootCommandIndicator { return true } hasThePrefix := strings.HasPrefix(currentCommandText, c.Name) hasNoMatchingAlternative := !strings.HasPrefix(currentCommandText, c.Name+"-") accepts := hasThePrefix && hasNoMatchingAlternative plc4xpcapanalyzerLog.Debug(). Stringer("c", c). Bool("accepts", accepts). Msg("c accepts accepts") return accepts } func (c Command) doesCommandTextTargetSubCommand(currentCommandText string) bool { if c.Name == rootCommandIndicator { return true } if len(c.subCommands) == 0 { return false } return strings.HasPrefix(currentCommandText, c.currentCommandPrefix()) } func (c Command) prepareForParameters(currentCommandText string) string { if currentCommandText == c.Name { return "" } return strings.TrimPrefix(currentCommandText, c.currentCommandPrefix()) } func (c Command) prepareForSubCommand(currentCommandText string) string { return strings.TrimPrefix(currentCommandText, c.currentCommandPrefix()) } func (c Command) currentCommandPrefix() string { if c.Name == rootCommandIndicator { return "" } return c.Name + " " } func (c Command) hasDirectExecution() bool { return c.action != nil } func Execute(ctx context.Context, commandText string) error { err := rootCommand.Execute(ctx, commandText) if err == nil { addCommandHistoryEntry(commandText) } return err } func (c Command) Execute(ctx context.Context, commandText string) (err error) { defer func() { if recoveredErr := recover(); recoveredErr != nil { if log.Debug().Enabled() { log.Error(). Str("stack", string(debug.Stack())). Interface("err", err). Msg("panic-ed") } err = errors.Errorf("panic occurred: %v.", recoveredErr) } }() plc4xpcapanalyzerLog.Debug(). Stringer("c", c).Str("commandText", commandText). Msg("c executes commandText") if !c.acceptsCurrentText(commandText) { return errors.Errorf("%s doesn't understand %s", c.Name, commandText) } if c.doesCommandTextTargetSubCommand(commandText) { prepareForSubCommandForSubCommand := c.prepareForSubCommand(commandText) for _, command := range c.subCommands { if command.acceptsCurrentText(prepareForSubCommandForSubCommand) { plc4xpcapanalyzerLog.Debug(). Stringer("c", c). Str("commandText", commandText). Msg("c delegates to sub command") return command.Execute(ctx, prepareForSubCommandForSubCommand) } } return errors.Errorf("%s not accepted by any subcommands of %s", commandText, c.Name) } else { if c.action == nil { return NotDirectlyExecutable } plc4xpcapanalyzerLog.Debug(). Stringer("c", c). Str("commandText", commandText). Msg("c executes commandText directly") preparedForParameters := c.prepareForParameters(commandText) return c.action(ctx, c, preparedForParameters) } } func (c Command) visit(i int, f func(currentIndent int, command Command)) { f(i, c) for _, subCommand := range c.subCommands { subCommand.visit(i+1, f) } } func (c Command) String() string { return c.Name }