You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by sr...@apache.org on 2022/08/09 14:29:01 UTC
[plc4x] 03/03: feat(plc4xpcapanalyzer): added frontend for interactive work with the cli
This is an automated email from the ASF dual-hosted git repository.
sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
commit decf45ef9ffaf6276db4f1abfbe42c841fdd2456
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Tue Aug 9 16:28:51 2022 +0200
feat(plc4xpcapanalyzer): added frontend for interactive work with the cli
---
plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go | 4 +-
plc4go/tools/plc4xpcapanalyzer/cmd/cbus.go | 4 +-
plc4go/tools/plc4xpcapanalyzer/cmd/extract.go | 6 +-
plc4go/tools/plc4xpcapanalyzer/cmd/root.go | 7 +-
.../plc4xpcapanalyzer/cmd/{bacnet.go => ui.go} | 45 +-
.../internal/analyzer/analyzer.go | 36 +-
.../internal/bacnetanalyzer/analyzer.go | 5 +-
.../internal/cbusanalyzer/analyzer.go | 5 +-
.../internal/extractor/extractor.go | 20 +-
.../internal/pcaphandler/pcaphandler.go | 42 +-
plc4go/tools/plc4xpcapanalyzer/ui/actions.go | 130 +++++
plc4go/tools/plc4xpcapanalyzer/ui/commands.go | 613 +++++++++++++++++++++
plc4go/tools/plc4xpcapanalyzer/ui/common.go | 82 +++
plc4go/tools/plc4xpcapanalyzer/ui/config.go | 179 ++++++
plc4go/tools/plc4xpcapanalyzer/ui/ui.go | 310 +++++++++++
15 files changed, 1420 insertions(+), 68 deletions(-)
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go b/plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go
index 31dbc33f0..143051fc5 100644
--- a/plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go
@@ -56,7 +56,9 @@ TODO: document me
} else {
log.Info().Msg("All filtering disabled")
}
- analyzer.Analyze(pcapFile, "bacnet")
+ if err := analyzer.Analyze(pcapFile, "bacnet"); err != nil {
+ panic(err)
+ }
println("Done")
},
}
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/cbus.go b/plc4go/tools/plc4xpcapanalyzer/cmd/cbus.go
index 8ae7f0f8b..0b997047c 100644
--- a/plc4go/tools/plc4xpcapanalyzer/cmd/cbus.go
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/cbus.go
@@ -56,7 +56,9 @@ TODO: document me
} else {
log.Info().Msg("All filtering disabled")
}
- analyzer.Analyze(pcapFile, "c-bus")
+ if err := analyzer.Analyze(pcapFile, "c-bus"); err != nil {
+ panic(err)
+ }
println("Done")
},
}
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/extract.go b/plc4go/tools/plc4xpcapanalyzer/cmd/extract.go
index 55b21ede1..441d1a9aa 100644
--- a/plc4go/tools/plc4xpcapanalyzer/cmd/extract.go
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/extract.go
@@ -32,7 +32,7 @@ import (
var extractCmd = &cobra.Command{
Use: "extract [protocolType] [pcapfile]",
Short: "extract a pcap file using a driver supplied driver",
- Long: `Analyzes a pcap file using a driver
+ Long: `Extract a pcap file using a driver
TODO: document me
`,
Args: func(cmd *cobra.Command, args []string) error {
@@ -51,7 +51,9 @@ TODO: document me
Run: func(cmd *cobra.Command, args []string) {
protocolType := args[0]
pcapFile := args[1]
- extractor.Extract(pcapFile, protocolType)
+ if err := extractor.Extract(pcapFile, protocolType); err != nil {
+ panic(err)
+ }
println("Done")
},
}
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/root.go b/plc4go/tools/plc4xpcapanalyzer/cmd/root.go
index b367fa622..205a6e6f5 100644
--- a/plc4go/tools/plc4xpcapanalyzer/cmd/root.go
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/root.go
@@ -68,14 +68,13 @@ func initConfig() {
// Use config file from the flag.
viper.SetConfigFile(config.RootConfigInstance.CfgFile)
} else {
- // Find home directory.
- home, err := os.UserHomeDir()
+ // Find user config directory.
+ home, err := os.UserConfigDir()
cobra.CheckErr(err)
- // Search config in home directory with name ".plc4xpcapanalyzer" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
- viper.SetConfigName(".plc4xpcapanalyzer")
+ viper.SetConfigName("plc4xpcapanalyzer-viper")
}
viper.AutomaticEnv() // read in environment variables that match
diff --git a/plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go b/plc4go/tools/plc4xpcapanalyzer/cmd/ui.go
similarity index 55%
copy from plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go
copy to plc4go/tools/plc4xpcapanalyzer/cmd/ui.go
index 31dbc33f0..0a259a15d 100644
--- a/plc4go/tools/plc4xpcapanalyzer/cmd/bacnet.go
+++ b/plc4go/tools/plc4xpcapanalyzer/cmd/ui.go
@@ -20,8 +20,7 @@
package cmd
import (
- "github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
- "github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/analyzer"
+ "github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/ui"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"os"
@@ -29,16 +28,16 @@ import (
"github.com/spf13/cobra"
)
-// bacnetCmd represents the bacnet command
-var bacnetCmd = &cobra.Command{
- Use: "bacnet [pcapfile]",
- Short: "analyzes a pcap file using a bacnet driver",
+// uiCmd represents the ui command
+var uiCmd = &cobra.Command{
+ Use: "ui [pcapfile]",
+ Short: "Start the ui with optional pcapfile",
Long: `Analyzes a pcap file using a bacnet driver
TODO: document me
`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
- return errors.New("requires exactly one arguments")
+ return nil
}
pcapFile := args[0]
if _, err := os.Stat(pcapFile); errors.Is(err, os.ErrNotExist) {
@@ -47,24 +46,26 @@ TODO: document me
return nil
},
Run: func(cmd *cobra.Command, args []string) {
- pcapFile := args[0]
- if !config.BacnetConfigInstance.NoFilter {
- if config.BacnetConfigInstance.Filter == "" && config.BacnetConfigInstance.BacnetFilter != "" {
- log.Debug().Str("filter", config.BacnetConfigInstance.Filter).Msg("Setting bacnet filter")
- config.BacnetConfigInstance.Filter = config.BacnetConfigInstance.BacnetFilter
- }
- } else {
- log.Info().Msg("All filtering disabled")
+ ui.LoadConfig()
+ application := ui.SetupApplication()
+ ui.InitSubsystem()
+ if len(args) > 0 {
+ pcapFile := args[0]
+ go func() {
+ err := ui.OpenFile(pcapFile)
+ if err != nil {
+ log.Error().Err(err).Msg("Error opening argument file")
+ }
+ }()
+ }
+
+ if err := application.Run(); err != nil {
+ panic(err)
}
- analyzer.Analyze(pcapFile, "bacnet")
- println("Done")
+ ui.Shutdown()
},
}
func init() {
- analyzeCmd.AddCommand(bacnetCmd)
-
- bacnetCmd.PersistentFlags().StringVarP(&config.BacnetConfigInstance.BacnetFilter, "default-bacnet-filter", "", "udp port 47808 and udp[4:2] > 29", "Defines the default filter when bacnet is selected")
-
- addAnalyzeFlags(bacnetCmd)
+ rootCmd.AddCommand(uiCmd)
}
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/analyzer/analyzer.go b/plc4go/tools/plc4xpcapanalyzer/internal/analyzer/analyzer.go
index 1e74430fe..22fa9302a 100644
--- a/plc4go/tools/plc4xpcapanalyzer/internal/analyzer/analyzer.go
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/analyzer/analyzer.go
@@ -23,6 +23,7 @@ import (
"bytes"
"encoding/hex"
"fmt"
+ "github.com/apache/plc4x/plc4go/internal/spi"
"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer"
"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer"
@@ -31,16 +32,30 @@ import (
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/k0kubun/go-ansi"
+ "github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/schollz/progressbar/v3"
+ "io"
"net"
+ "os"
"time"
)
-func Analyze(pcapFile, protocolType string) {
+func Analyze(pcapFile, protocolType string) error {
+ return AnalyzeWithOutput(pcapFile, protocolType, os.Stdout, os.Stderr)
+}
+
+func AnalyzeWithOutput(pcapFile, protocolType string, stdout, stderr io.Writer) error {
+ return AnalyzeWithOutputAndCallback(pcapFile, protocolType, stdout, stderr, nil)
+}
+
+func AnalyzeWithOutputAndCallback(pcapFile, protocolType string, stdout, stderr io.Writer, messageCallback func(parsed spi.Message)) error {
log.Info().Msgf("Analyzing pcap file '%s' with protocolType '%s' and filter '%s' now", pcapFile, protocolType, config.AnalyzeConfigInstance.Filter)
- handle, numberOfPackage, timestampToIndexMap := pcaphandler.GetIndexedPcapHandle(pcapFile, config.AnalyzeConfigInstance.Filter)
+ handle, numberOfPackage, timestampToIndexMap, err := pcaphandler.GetIndexedPcapHandle(pcapFile, config.AnalyzeConfigInstance.Filter)
+ if err != nil {
+ return errors.Wrap(err, "Error getting handle")
+ }
log.Info().Msgf("Starting to analyze %d packages", numberOfPackage)
defer handle.Close()
log.Debug().Interface("handle", handle).Int("numberOfPackage", numberOfPackage).Msg("got handle")
@@ -48,10 +63,10 @@ func Analyze(pcapFile, protocolType string) {
var mapPackets = func(in chan gopacket.Packet, packetInformationCreator func(packet gopacket.Packet) common.PacketInformation) chan gopacket.Packet {
return in
}
- var packageParse func(common.PacketInformation, []byte) (interface{}, error)
- var serializePackage func(interface{}) ([]byte, error)
- var prettyPrint = func(item interface{}) {
- fmt.Printf("%v\n", item)
+ var packageParse func(common.PacketInformation, []byte) (spi.Message, error)
+ var serializePackage func(spi.Message) ([]byte, error)
+ var prettyPrint = func(item spi.Message) {
+ _, _ = fmt.Fprintf(stdout, "%v\n", item)
}
var byteOutput = hex.Dump
switch protocolType {
@@ -134,6 +149,9 @@ func Analyze(pcapFile, protocolType string) {
}
continue
} else {
+ if messageCallback != nil {
+ messageCallback(parsed)
+ }
log.Info().Stringer("packetInformation", packetInformation).Msgf("No.[%d] Parsed", realPacketNumber)
if config.AnalyzeConfigInstance.Verbosity > 1 {
prettyPrint(parsed)
@@ -158,16 +176,14 @@ func Analyze(pcapFile, protocolType string) {
// TODO: write report to xml or something
log.Warn().Stringer("packetInformation", packetInformation).Msgf("No.[%d] Bytes don't match.\nOriginal:\n%sSerialized:\n%s", realPacketNumber, byteOutput(payload), byteOutput(serializedBytes))
if config.AnalyzeConfigInstance.Verbosity > 0 {
- println("Original bytes")
- println(hex.Dump(payload))
- println("Serialized bytes")
- println(hex.Dump(serializedBytes))
+ _, _ = fmt.Fprintf(stdout, "Original bytes\n%s\n%s\n", hex.Dump(payload), hex.Dump(serializedBytes))
}
}
}
}
log.Info().Msgf("Done evaluating %d of %d packages (%d failed to parse, %d failed to serialize and %d failed in byte comparison)", currentPackageNum, numberOfPackage, parseFails, serializeFails, compareFails)
+ return nil
}
func createPacketInformation(pcapFile string, packet gopacket.Packet, timestampToIndexMap map[time.Time]int) common.PacketInformation {
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer/analyzer.go b/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer/analyzer.go
index f614ae435..562ae9969 100644
--- a/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer/analyzer.go
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer/analyzer.go
@@ -20,6 +20,7 @@
package bacnetanalyzer
import (
+ "github.com/apache/plc4x/plc4go/internal/spi"
"github.com/apache/plc4x/plc4go/internal/spi/utils"
"github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/common"
@@ -27,7 +28,7 @@ import (
"github.com/rs/zerolog/log"
)
-func PackageParse(packetInformation common.PacketInformation, payload []byte) (interface{}, error) {
+func PackageParse(packetInformation common.PacketInformation, payload []byte) (spi.Message, error) {
log.Debug().Msgf("Parsing %s", packetInformation)
parse, err := model.BVLCParse(utils.NewReadBufferByteBased(payload))
if err != nil {
@@ -37,7 +38,7 @@ func PackageParse(packetInformation common.PacketInformation, payload []byte) (i
return parse, nil
}
-func SerializePackage(bvlc interface{}) ([]byte, error) {
+func SerializePackage(bvlc spi.Message) ([]byte, error) {
if bvlc, ok := bvlc.(model.BVLC); !ok {
log.Fatal().Msgf("Unsupported type %T supplied", bvlc)
panic("unreachable statement")
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer/analyzer.go b/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer/analyzer.go
index 6997dc2b8..addabf8b3 100644
--- a/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer/analyzer.go
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer/analyzer.go
@@ -22,6 +22,7 @@ package cbusanalyzer
import (
"fmt"
"github.com/apache/plc4x/plc4go/internal/cbus"
+ "github.com/apache/plc4x/plc4go/internal/spi"
"github.com/apache/plc4x/plc4go/internal/spi/utils"
"github.com/apache/plc4x/plc4go/protocols/cbus/readwrite/model"
"github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
@@ -58,7 +59,7 @@ func (a *Analyzer) Init() {
a.initialized = true
}
-func (a *Analyzer) PackageParse(packetInformation common.PacketInformation, payload []byte) (interface{}, error) {
+func (a *Analyzer) PackageParse(packetInformation common.PacketInformation, payload []byte) (spi.Message, error) {
if !a.initialized {
log.Warn().Msg("Not initialized... doing that now")
a.Init()
@@ -214,7 +215,7 @@ func filterXOnXOff(payload []byte) []byte {
return payload[:n]
}
-func (a *Analyzer) SerializePackage(message interface{}) ([]byte, error) {
+func (a *Analyzer) SerializePackage(message spi.Message) ([]byte, error) {
if message, ok := message.(model.CBusMessage); !ok {
log.Fatal().Msgf("Unsupported type %T supplied", message)
panic("unreachable statement")
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/extractor/extractor.go b/plc4go/tools/plc4xpcapanalyzer/internal/extractor/extractor.go
index 4495ea7cf..bfd201f95 100644
--- a/plc4go/tools/plc4xpcapanalyzer/internal/extractor/extractor.go
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/extractor/extractor.go
@@ -27,21 +27,30 @@ import (
"github.com/fatih/color"
"github.com/google/gopacket/layers"
"github.com/k0kubun/go-ansi"
+ "github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/schollz/progressbar/v3"
+ "io"
"net"
)
-func Extract(pcapFile, protocolType string) {
+func Extract(pcapFile, protocolType string) error {
+ return ExtractWithOutput(pcapFile, protocolType, ansi.NewAnsiStdout(), ansi.NewAnsiStderr())
+}
+
+func ExtractWithOutput(pcapFile, protocolType string, stdout, stderr io.Writer) error {
log.Info().Msgf("Analyzing pcap file '%s' with protocolType '%s' and filter '%s' now", pcapFile, protocolType, config.ExtractConfigInstance.Filter)
- handle, numberOfPackage, timestampToIndexMap := pcaphandler.GetIndexedPcapHandle(pcapFile, config.ExtractConfigInstance.Filter)
+ handle, numberOfPackage, timestampToIndexMap, err := pcaphandler.GetIndexedPcapHandle(pcapFile, config.ExtractConfigInstance.Filter)
+ if err != nil {
+ return errors.Wrap(err, "Error getting handle")
+ }
log.Info().Msgf("Starting to analyze %d packages", numberOfPackage)
defer handle.Close()
log.Debug().Interface("handle", handle).Int("numberOfPackage", numberOfPackage).Msg("got handle")
source := pcaphandler.GetPacketSource(handle)
var printPayload = func(packetInformation common.PacketInformation, item []byte) {
- fmt.Printf("%x\n", item)
+ _, _ = fmt.Fprintf(stdout, "%x\n", item)
}
switch protocolType {
case "bacnet":
@@ -49,8 +58,6 @@ func Extract(pcapFile, protocolType string) {
case "c-bus":
// c-bus is string based so we consume the string and print it
clientIp := net.ParseIP(config.ExtractConfigInstance.Client)
- stdout := ansi.NewAnsiStdout()
- stderr := ansi.NewAnsiStderr()
serverResponseWriter := color.New(color.FgRed)
serverResponseIndicatorWriter := color.New(color.FgHiRed)
clientRequestWriter := color.New(color.FgGreen)
@@ -143,7 +150,8 @@ func Extract(pcapFile, protocolType string) {
printPayload(packetInformation, payload)
}
}
- println()
+ _, _ = fmt.Fprintf(stdout, "\n")
log.Info().Msgf("Done evaluating %d of %d packages (%d failed to parse, %d failed to serialize and %d failed in byte comparison)", currentPackageNum, numberOfPackage, parseFails, serializeFails, compareFails)
+ return nil
}
diff --git a/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler/pcaphandler.go b/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler/pcaphandler.go
index a94c4cf8f..957d50f8c 100644
--- a/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler/pcaphandler.go
+++ b/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler/pcaphandler.go
@@ -22,6 +22,7 @@ package pcaphandler
import (
"github.com/google/gopacket"
"github.com/google/gopacket/pcap"
+ "github.com/pkg/errors"
"time"
)
@@ -31,11 +32,14 @@ func GetPacketSource(handle *pcap.Handle) *gopacket.PacketSource {
}
// GetIndexedPcapHandle returns a *pcap.Handle, the number of packages found and an index which maps timestamp to
-// absolute package number and panics if an error occurs
-func GetIndexedPcapHandle(file, filterExpression string) (*pcap.Handle, int, map[time.Time]int) {
- timestampToIndexMap := make(map[time.Time]int)
+// absolute package number
+func GetIndexedPcapHandle(file, filterExpression string) (handle *pcap.Handle, numberOfPackages int, timestampToIndexMap map[time.Time]int, err error) {
+ timestampToIndexMap = make(map[time.Time]int)
// Count absolute packages and set timestamp map
- temporaryHandle := GetPcapHandle(file, "")
+ temporaryHandle, err := GetPcapHandle(file, "")
+ if err != nil {
+ return nil, 0, nil, err
+ }
defer temporaryHandle.Close()
packetSource := GetPacketSource(temporaryHandle)
packages := 0
@@ -47,7 +51,10 @@ func GetIndexedPcapHandle(file, filterExpression string) (*pcap.Handle, int, map
timestampToIndexMap[packet.Metadata().Timestamp] = packages
}
// Just count filtered packages
- temporaryFilteredHandle := GetPcapHandle(file, filterExpression)
+ temporaryFilteredHandle, err := GetPcapHandle(file, filterExpression)
+ if err != nil {
+ return nil, 0, nil, err
+ }
defer temporaryFilteredHandle.Close()
filteredPacketSource := GetPacketSource(temporaryFilteredHandle)
packages = 0
@@ -57,24 +64,23 @@ func GetIndexedPcapHandle(file, filterExpression string) (*pcap.Handle, int, map
}
packages++
}
- return GetPcapHandle(file, filterExpression), packages, timestampToIndexMap
+ pcapHandle, err := GetPcapHandle(file, filterExpression)
+ if err != nil {
+ return nil, 0, nil, err
+ }
+ return pcapHandle, packages, timestampToIndexMap, nil
}
// GetPcapHandle returns a *pcap.Handle and panics if an error occurs
-func GetPcapHandle(file, filterExpression string) *pcap.Handle {
- handle := getPcapHandleOrPanic(file)
+func GetPcapHandle(file, filterExpression string) (*pcap.Handle, error) {
+ handle, err := pcap.OpenOffline(file)
+ if err != nil {
+ return nil, errors.Wrap(err, "error open offline")
+ }
if filterExpression != "" {
if err := handle.SetBPFFilter(filterExpression); err != nil {
- panic(err)
+ return nil, errors.Wrap(err, "error setting BPF filter")
}
}
- return handle
-}
-
-func getPcapHandleOrPanic(file string) *pcap.Handle {
- handle, err := pcap.OpenOffline(file)
- if err != nil {
- panic(err)
- }
- return handle
+ return handle, nil
}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/actions.go b/plc4go/tools/plc4xpcapanalyzer/ui/actions.go
new file mode 100644
index 000000000..fbf0625a8
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/actions.go
@@ -0,0 +1,130 @@
+/*
+ * 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 (
+ "fmt"
+ "github.com/apache/plc4x/plc4go/internal/ads"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/cbus"
+ "github.com/apache/plc4x/plc4go/internal/s7"
+ "github.com/apache/plc4x/plc4go/internal/spi"
+ "github.com/apache/plc4x/plc4go/internal/spi/transports/pcap"
+ plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+ "github.com/sruehl/tview"
+ "os"
+ "path"
+ "strings"
+)
+
+func InitSubsystem() {
+ logLevel := zerolog.InfoLevel
+ if configuredLevel := config.LogLevel; configuredLevel != "" {
+ if parsedLevel, err := zerolog.ParseLevel(configuredLevel); err != nil {
+ panic(err)
+ } else {
+ logLevel = parsedLevel
+ }
+ }
+ driverManager = plc4go.NewPlcDriverManager()
+
+ log.Logger = log.
+ //// Enable below if you want to see the filenames
+ //With().Caller().Logger().
+ Output(zerolog.ConsoleWriter{Out: tview.ANSIWriter(consoleOutput)}).
+ Level(logLevel)
+
+ // We offset the commands executed with the last commands
+ commandsExecuted = len(config.History.Last10Commands)
+ outputCommandHistory()
+
+ for _, driver := range config.AutoRegisterDrivers {
+ log.Info().Msgf("Auto register driver %s", driver)
+ if err := validateDriverParam(driver); err != nil {
+ log.Err(err).Msgf("Invalid configuration")
+ continue
+ }
+ _ = registerDriver(driver)
+ }
+}
+
+func OpenFile(pcapFile string) error {
+ if !strings.HasPrefix(pcapFile, string(os.PathSeparator)) {
+ pcapFile = path.Join(currentDir, pcapFile)
+ }
+ var name string
+ if stat, err := os.Stat(pcapFile); err != nil {
+ return err
+ } else if stat.IsDir() {
+ stat.Name()
+ return errors.Errorf("%s is a dir", pcapFile)
+ } else {
+ name = stat.Name()
+ }
+ for _, loadedPcapFile := range loadedPcapFiles {
+ if loadedPcapFile.path == pcapFile {
+ return errors.Errorf("%s already loaded", name)
+ }
+ }
+ addRecentFilesEntry(pcapFile)
+ loadedPcapFiles = append(loadedPcapFiles, loadedPcapFile{
+ name: name,
+ path: pcapFile,
+ })
+ loadedPcapFilesChanged()
+ return nil
+}
+
+func outputCommandHistory() {
+ _, _ = fmt.Fprintln(commandOutput, "[#0000ff]Last 10 commands[white]")
+ for i, command := range config.History.Last10Commands {
+ _, _ = fmt.Fprintf(commandOutput, " [#00ff00]%d[white]: [\"%d\"]%s[\"\"]\n", i, i, tview.Escape(command))
+ }
+}
+
+func validateDriverParam(driver string) error {
+ for _, protocol := range protocolList {
+ if protocol == driver {
+ return nil
+ }
+ }
+ return errors.Errorf("protocol %s not found", driver)
+}
+
+func registerDriver(driver string) error {
+ switch driver {
+ case "ads":
+ driverManager.RegisterDriver(ads.NewDriver())
+ case "bacnetip":
+ driverManager.RegisterDriver(bacnetip.NewDriver())
+ case "c-bus":
+ driverManager.RegisterDriver(cbus.NewDriver())
+ case "s7":
+ driverManager.RegisterDriver(s7.NewDriver())
+ default:
+ return errors.Errorf("Unknown driver %s", driver)
+ }
+ driverManager.(spi.TransportAware).RegisterTransport(pcap.NewTransport())
+ go driverAdded(driver)
+ return nil
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/commands.go b/plc4go/tools/plc4xpcapanalyzer/ui/commands.go
new file mode 100644
index 000000000..a0890874d
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/commands.go
@@ -0,0 +1,613 @@
+/*
+ * 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 (
+ "fmt"
+ "github.com/apache/plc4x/plc4go/internal/spi"
+ plc4x_config "github.com/apache/plc4x/plc4go/pkg/api/config"
+ cliConfig "github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/config"
+ "github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/analyzer"
+ "github.com/apache/plc4x/plc4go/tools/plc4xpcapanalyzer/internal/extractor"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+ "github.com/sruehl/tview"
+ "os"
+ "path"
+ "strings"
+ "time"
+)
+
+const rootCommandIndicator = "rootCommand"
+
+var rootCommand = Command{
+ Name: rootCommandIndicator,
+ subCommands: []Command{
+ {
+ Name: "ls",
+ Description: "list directories",
+ action: func(_ 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(_ 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
+ }
+ 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(_ Command, _ string) error {
+ _, _ = fmt.Fprintf(commandOutput, "current directory: %s\n", currentDir)
+ return nil
+ },
+ },
+ {
+ Name: "open",
+ Description: "open file",
+ action: func(_ 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(), ".pcap") || strings.HasSuffix(name, ".pcapng") {
+ entries = append(entries, name)
+ }
+ }
+ return
+ },
+ },
+ {
+ Name: "analyze",
+ Description: "Analyzes a pcap file using a driver",
+ action: func(_ 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 analyzer.AnalyzeWithOutputAndCallback(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(_ 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(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(_ Command, host string) error {
+ config.HostIp = host
+ return nil
+ },
+ },
+ {
+ Name: "get",
+ action: func(_ Command, host string) error {
+ _, _ = fmt.Fprintf(consoleOutput, "current set host %s", config.HostIp)
+ return nil
+ },
+ },
+ },
+ },
+ {
+ Name: "register",
+ Description: "register a driver in the subsystem",
+ action: func(_ 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(_ Command, _ string) error {
+ _, _ = fmt.Fprintf(commandOutput, "Current log level %s", log.Logger.GetLevel())
+ return nil
+ },
+ },
+ {
+ Name: "set",
+ Description: "Sets a log level",
+ action: func(_ 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: "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(_ Command, _ string) error {
+ plc4x_config.TraceTransactionManagerWorkers = true
+ return nil
+ },
+ },
+ {
+ Name: "off",
+ Description: "trace off",
+ action: func(_ Command, _ string) error {
+ plc4x_config.TraceTransactionManagerWorkers = false
+ return nil
+ },
+ },
+ },
+ },
+ {
+ Name: "TraceTransactionManagerTransactions",
+ Description: "print information about transaction manager transactions",
+ subCommands: []Command{
+ {
+ Name: "on",
+ Description: "trace on",
+ action: func(_ Command, _ string) error {
+ plc4x_config.TraceTransactionManagerTransactions = true
+ return nil
+ },
+ },
+ {
+ Name: "off",
+ Description: "trace off",
+ action: func(_ Command, _ string) error {
+ plc4x_config.TraceTransactionManagerTransactions = false
+ return nil
+ },
+ },
+ },
+ },
+ {
+ Name: "TraceDefaultMessageCodecWorker",
+ Description: "print information about message codec workers",
+ subCommands: []Command{
+ {
+ Name: "on",
+ Description: "trace on",
+ action: func(_ Command, _ string) error {
+ plc4x_config.TraceDefaultMessageCodecWorker = true
+ return nil
+ },
+ },
+ {
+ Name: "off",
+ Description: "trace off",
+ action: func(_ Command, _ string) error {
+ plc4x_config.TraceDefaultMessageCodecWorker = false
+ return nil
+ },
+ },
+ },
+ },
+ {
+ Name: "plc4xpcapanalyzer-debug",
+ Description: "Prints out debug information of the pcap analyzer itself",
+ subCommands: []Command{
+ {
+ Name: "on",
+ Description: "debug on",
+ action: func(_ Command, _ string) error {
+ plc4xpcapanalyzerLog = zerolog.New(zerolog.ConsoleWriter{Out: tview.ANSIWriter(consoleOutput)})
+ return nil
+ },
+ },
+ {
+ Name: "off",
+ Description: "debug off",
+ action: func(_ Command, _ string) error {
+ plc4xpcapanalyzerLog = zerolog.Nop()
+ return nil
+ },
+ },
+ },
+ },
+ {
+ Name: "auto-register",
+ Description: "autoregister driver at startup",
+ subCommands: []Command{
+ {
+ Name: "list",
+ action: func(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(_ 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(_ 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: "history",
+ Description: "outputs the last commands",
+ action: func(_ Command, _ string) error {
+ outputCommandHistory()
+ return nil
+ },
+ },
+ {
+ Name: "clear",
+ Description: "clear all outputs",
+ action: func(_ Command, _ string) error {
+ messageOutputClear()
+ consoleOutputClear()
+ commandOutputClear()
+ return nil
+ },
+ subCommands: []Command{
+ {
+ Name: "message",
+ Description: "clears message output",
+ action: func(_ Command, _ string) error {
+ messageOutputClear()
+ return nil
+ },
+ },
+ {
+ Name: "console",
+ Description: "clears console output",
+ action: func(_ Command, _ string) error {
+ consoleOutputClear()
+ return nil
+ },
+ },
+ {
+ Name: "command",
+ Description: "clears command output",
+ action: func(_ Command, _ string) error {
+ commandOutputClear()
+ 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(_ 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(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().Msgf("%s accepts %t", c, 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(commandText string) error {
+ err := rootCommand.Execute(commandText)
+ if err == nil {
+ addCommandHistoryEntry(commandText)
+ }
+ return err
+}
+
+func (c Command) Execute(commandText string) error {
+ plc4xpcapanalyzerLog.Debug().Msgf("%s executes %s", c, 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().Msgf("%s delegates to sub %s", c, command)
+ return command.Execute(prepareForSubCommandForSubCommand)
+ }
+ }
+ return errors.Errorf("%s not accepted by any subcommands of %s", commandText, c.Name)
+ } else {
+ if c.action == nil {
+ return NotDirectlyExecutable
+ }
+ plc4xpcapanalyzerLog.Debug().Msgf("%s executes %s directly", c, commandText)
+ preparedForParameters := c.prepareForParameters(commandText)
+ return c.action(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
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/common.go b/plc4go/tools/plc4xpcapanalyzer/ui/common.go
new file mode 100644
index 000000000..c716a66d8
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/common.go
@@ -0,0 +1,82 @@
+/*
+ * 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 (
+ "github.com/apache/plc4x/plc4go/internal/spi"
+ plc4go "github.com/apache/plc4x/plc4go/pkg/api"
+ plc4goModel "github.com/apache/plc4x/plc4go/pkg/api/model"
+ "github.com/rs/zerolog"
+ "io"
+ "os"
+ "strings"
+ "sync"
+ "time"
+)
+
+const protocols = "ads,bacnetip,c-bus,s7"
+
+var protocolList = strings.Split(protocols, ",")
+
+var plc4xpcapanalyzerLog = zerolog.Nop()
+
+var driverManager plc4go.PlcDriverManager
+var driverAdded func(string)
+
+type loadedPcapFile struct {
+ name string
+ path string
+}
+
+var loadedPcapFiles []loadedPcapFile
+var loadedPcapFilesChanged func()
+
+var messageReceived func(messageNumber int, receiveTime time.Time, message plc4goModel.PlcMessage)
+var numberOfMessagesReceived int
+var spiMessageReceived func(messageNumber int, receiveTime time.Time, message spi.Message)
+var spiNumberOfMessagesReceived int
+var messageOutput io.Writer
+var messageOutputClear func()
+
+var consoleOutput io.Writer
+var consoleOutputClear func()
+
+var commandsExecuted int
+var commandOutput io.Writer
+var commandOutputClear func()
+
+var currentDirChanged func()
+var currentDir = func() string {
+ dir, _ := os.Getwd()
+ return dir
+}()
+
+var shutdownMutex sync.Mutex
+var hasShutdown bool
+
+func Shutdown() {
+ shutdownMutex.Lock()
+ defer shutdownMutex.Unlock()
+ if hasShutdown {
+ return
+ }
+ saveConfig()
+ hasShutdown = true
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/config.go b/plc4go/tools/plc4xpcapanalyzer/ui/config.go
new file mode 100644
index 000000000..100f8c440
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/config.go
@@ -0,0 +1,179 @@
+/*
+ * 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 (
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+ "gopkg.in/yaml.v3"
+ "os"
+ "path"
+ "time"
+)
+
+var plc4xpcapanalyzerConfigDir string
+var configFile string
+var config Config
+
+type Config struct {
+ HostIp string `yaml:"host_ip"`
+ History struct {
+ Last10Files []string `yaml:"last_hosts"`
+ Last10Commands []string `yaml:"last_commands"`
+ }
+ AutoRegisterDrivers []string `yaml:"auto_register_driver"`
+ LastUpdated time.Time `yaml:"last_updated"`
+ LogLevel string `yaml:"log_level"`
+ MaxConsoleLines int `yaml:"max_console_lines"`
+ MaxOutputLines int `yaml:"max_output_lines"`
+}
+
+func init() {
+ userConfigDir, err := os.UserConfigDir()
+ if err != nil {
+ panic(err)
+ }
+ plc4xpcapanalyzerConfigDir = path.Join(userConfigDir, "plc4xpcapanalyzer")
+ if _, err := os.Stat(plc4xpcapanalyzerConfigDir); os.IsNotExist(err) {
+ err := os.Mkdir(plc4xpcapanalyzerConfigDir, os.ModeDir|os.ModePerm)
+ if err != nil {
+ panic(err)
+ }
+ }
+ configFile = path.Join(plc4xpcapanalyzerConfigDir, "config.yml")
+}
+
+func LoadConfig() {
+ f, err := os.Open(configFile)
+ if err != nil {
+ log.Info().Err(err).Msg("No config file found")
+ return
+ }
+ defer func(f *os.File) {
+ err := f.Close()
+ if err != nil {
+ log.Error().Err(err).Msg("Error closing config file")
+ }
+ }(f)
+
+ decoder := yaml.NewDecoder(f)
+ if err = decoder.Decode(&config); err != nil {
+ log.Warn().Err(err).Msg("Can't decode config file")
+ return
+ }
+}
+
+func saveConfig() {
+ config.LastUpdated = time.Now()
+ f, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE, 0755)
+ if err != nil {
+ log.Warn().Err(err).Msg("Can't save config file")
+ return
+ }
+ encoder := yaml.NewEncoder(f)
+ defer func(encoder *yaml.Encoder) {
+ err := encoder.Close()
+ if err != nil {
+ log.Error().Err(err).Msg("Error closing config file")
+ }
+ }(encoder)
+ if err := encoder.Encode(config); err != nil {
+ log.Warn().Err(err).Msg("Can't encode config file")
+ panic(err)
+ }
+}
+
+func addRecentFilesEntry(pcapFile string) {
+ existingIndex := -1
+ for i, lastPcapFile := range config.History.Last10Files {
+ if lastPcapFile == pcapFile {
+ existingIndex = i
+ break
+ }
+ }
+ if existingIndex >= 0 {
+ config.History.Last10Files = append(config.History.Last10Files[:existingIndex], config.History.Last10Files[existingIndex+1:]...)
+ }
+ if len(config.History.Last10Files) >= 10 {
+ config.History.Last10Files = config.History.Last10Files[1:]
+ }
+ config.History.Last10Files = append(config.History.Last10Files, pcapFile)
+}
+
+func addCommandHistoryEntry(command string) {
+ switch command {
+ case "clear":
+ return
+ case "history":
+ return
+ }
+ existingIndex := -1
+ for i, lastCommand := range config.History.Last10Commands {
+ if lastCommand == command {
+ existingIndex = i
+ break
+ }
+ }
+ if existingIndex >= 0 {
+ config.History.Last10Commands = append(config.History.Last10Commands[:existingIndex], config.History.Last10Commands[existingIndex+1:]...)
+ }
+ if len(config.History.Last10Commands) >= 10 {
+ config.History.Last10Commands = config.History.Last10Commands[1:]
+ }
+ config.History.Last10Commands = append(config.History.Last10Commands, command)
+}
+
+func setLevel(level zerolog.Level) {
+ config.LogLevel = level.String()
+}
+
+func enableAutoRegister(driver string) error {
+ if err := validateDriverParam(driver); err != nil {
+ return err
+ }
+ for _, autoRegisterDriver := range config.AutoRegisterDrivers {
+ if autoRegisterDriver == driver {
+ return errors.Errorf("%s already registered for auto register", driver)
+ }
+ }
+ config.AutoRegisterDrivers = append(config.AutoRegisterDrivers, driver)
+ log.Info().Msgf("Auto register enabled for %s", driver)
+ return nil
+}
+
+func disableAutoRegister(driver string) error {
+ if err := validateDriverParam(driver); err != nil {
+ return err
+ }
+ index := -1
+ for i, autoRegisterDriver := range config.AutoRegisterDrivers {
+ if autoRegisterDriver == driver {
+ index = i
+ break
+ }
+ }
+ if index < 0 {
+ return errors.Errorf("%s not registered for auto register", driver)
+ }
+ config.AutoRegisterDrivers = append(config.AutoRegisterDrivers[:index], config.AutoRegisterDrivers[index+1:]...)
+ log.Info().Msgf("Auto register disabled for %s", driver)
+ return nil
+}
diff --git a/plc4go/tools/plc4xpcapanalyzer/ui/ui.go b/plc4go/tools/plc4xpcapanalyzer/ui/ui.go
new file mode 100644
index 000000000..8ea89fa43
--- /dev/null
+++ b/plc4go/tools/plc4xpcapanalyzer/ui/ui.go
@@ -0,0 +1,310 @@
+/*
+ * 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 (
+ "fmt"
+ "github.com/apache/plc4x/plc4go/internal/spi"
+ "github.com/apache/plc4x/plc4go/pkg/api/model"
+ "github.com/gdamore/tcell/v2"
+ "github.com/pkg/errors"
+ "github.com/sruehl/tview"
+ "regexp"
+ "strconv"
+ "time"
+)
+
+func SetupApplication() *tview.Application {
+ application := tview.NewApplication()
+
+ newPrimitive := func(text string) tview.Primitive {
+ return tview.NewTextView().
+ SetTextAlign(tview.AlignCenter).
+ SetText(text)
+ }
+ fileArea := buildFileArea(newPrimitive, application)
+ outputArea := buildOutputArea(newPrimitive, application)
+ commandArea := buildCommandArea(newPrimitive, application)
+
+ grid := tview.NewGrid().
+ SetRows(1, 0, 1).
+ SetColumns(30, 0, 30).
+ SetBorders(true).
+ AddItem(newPrimitive("PLC4X PCAP Analyzer"), 0, 0, 1, 3, 0, 0, false).
+ AddItem(newPrimitive("https://github.com/apache/plc4x"), 2, 0, 1, 3, 0, 0, false)
+
+ // Layout for screens narrower than 100 cells (fileArea and side bar are hidden).
+ grid.AddItem(fileArea, 0, 0, 0, 0, 0, 0, false).
+ AddItem(outputArea, 1, 0, 1, 3, 0, 0, false).
+ AddItem(commandArea, 0, 0, 0, 0, 0, 0, true)
+
+ // Layout for screens wider than 100 cells.
+ grid.AddItem(fileArea, 1, 0, 1, 1, 0, 100, false).
+ AddItem(outputArea, 1, 1, 1, 1, 0, 100, false).
+ AddItem(commandArea, 1, 2, 1, 1, 0, 100, false)
+
+ application.SetRoot(grid, true).EnableMouse(true)
+
+ return application
+}
+
+func buildFileArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) tview.Primitive {
+ connectionAreaHeader := newPrimitive("Files")
+ connectionArea := tview.NewGrid().
+ SetRows(3, 0, 10).
+ SetColumns(0).
+ AddItem(connectionAreaHeader, 0, 0, 1, 1, 0, 0, false)
+ {
+ fileList := tview.NewList()
+ loadedPcapFilesChanged = func() {
+ application.QueueUpdateDraw(func() {
+ fileList.Clear()
+ for _, pcapFile := range loadedPcapFiles {
+ fileList.AddItem(pcapFile.name, pcapFile.path, 0x0, func() {
+ //TODO: disconnect popup
+ _ = pcapFile
+ })
+ }
+ })
+ }
+ connectionArea.AddItem(fileList, 1, 0, 1, 1, 0, 0, false)
+ {
+ registeredDriverAreaHeader := newPrimitive("Registered drivers")
+ registeredDriverArea := tview.NewGrid().
+ SetRows(3, 0).
+ SetColumns(0).
+ AddItem(registeredDriverAreaHeader, 0, 0, 1, 1, 0, 0, false)
+ {
+ driverList := tview.NewList()
+ driverAdded = func(driver string) {
+ application.QueueUpdateDraw(func() {
+ driverList.AddItem(driver, "", 0x0, func() {
+ //TODO: disconnect popup
+ })
+ })
+ }
+ registeredDriverArea.AddItem(driverList, 1, 0, 1, 1, 0, 0, false)
+ }
+ connectionArea.AddItem(registeredDriverArea, 2, 0, 1, 1, 0, 0, false)
+ }
+
+ }
+ return connectionArea
+}
+
+func buildCommandArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) tview.Primitive {
+ commandAreaHeader := newPrimitive("Commands")
+ commandArea := tview.NewGrid().
+ SetRows(3, 0, 3).
+ SetColumns(0).
+ AddItem(commandAreaHeader, 0, 0, 1, 1, 0, 0, false)
+ {
+ enteredCommandsView := tview.NewTextView().
+ SetDynamicColors(true).
+ SetRegions(true).
+ SetWordWrap(true).
+ SetChangedFunc(func() {
+ application.Draw()
+ })
+ commandOutput = enteredCommandsView
+ commandOutputClear = func() {
+ enteredCommandsView.SetText("")
+ }
+
+ commandArea.AddItem(enteredCommandsView, 1, 0, 1, 1, 0, 0, false)
+
+ commandInputField := tview.NewInputField().
+ SetLabel("$").
+ SetFieldWidth(30)
+ application.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ switch event.Key() {
+ case tcell.KeyCtrlC:
+ commandInputField.SetText("")
+ application.SetFocus(commandInputField)
+ return nil
+ case tcell.KeyCtrlD:
+ // TODO: maybe add a modal here
+ application.Stop()
+ return nil
+ }
+ return event
+ })
+ commandInputField.
+ SetDoneFunc(func(key tcell.Key) {
+ commandText := commandInputField.GetText()
+ if commandText == "quit" {
+ // TODO: maybe add a modal here
+ application.Stop()
+ return
+ }
+ commandsExecuted++
+ go func() {
+ commandHistoryShortcut, _ := regexp.Compile("^[0-9]$")
+ if commandHistoryShortcut.MatchString(commandText) {
+ atoi, _ := strconv.Atoi(commandHistoryShortcut.FindString(commandText))
+ if atoi < len(config.History.Last10Commands) {
+ commandText = config.History.Last10Commands[atoi]
+ } else {
+ _, _ = fmt.Fprintf(enteredCommandsView, "[#ff0000]%s %s[white]\n", time.Now().Format("04:05"), errors.Errorf("No such elements %d in command history", atoi))
+ return
+ }
+ }
+ _, _ = fmt.Fprintf(enteredCommandsView, "%s [\"%d\"]%s[\"\"]\n", time.Now().Format("04:05"), commandsExecuted, commandText)
+ if err := Execute(commandText); err != nil {
+ _, _ = fmt.Fprintf(enteredCommandsView, "[#ff0000]%s %s[white]\n", time.Now().Format("04:05"), err)
+ return
+ }
+ application.QueueUpdateDraw(func() {
+ commandInputField.SetText("")
+ })
+ }()
+ })
+ commandInputField.SetAutocompleteFunc(rootCommand.Completions)
+
+ enteredCommandsView.SetDoneFunc(func(key tcell.Key) {
+ currentSelection := enteredCommandsView.GetHighlights()
+ if key == tcell.KeyEnter {
+ if len(currentSelection) > 0 {
+ enteredCommandsView.Highlight()
+ } else {
+ enteredCommandsView.Highlight("0").ScrollToHighlight()
+ }
+ if len(currentSelection) == 1 {
+ // TODO: currently this is broken due to https://github.com/rivo/tview/issues/751 (workaround active with sruehl fix fork)
+ commandInputField.SetText(enteredCommandsView.GetRegionText(currentSelection[0]))
+ application.SetFocus(commandInputField)
+ }
+ } else if len(currentSelection) > 0 {
+ index, _ := strconv.Atoi(currentSelection[0])
+ if key == tcell.KeyTab {
+ index = (index + 1) % commandsExecuted
+ } else if key == tcell.KeyBacktab {
+ index = (index - 1 + commandsExecuted) % commandsExecuted
+ } else {
+ return
+ }
+ enteredCommandsView.Highlight(strconv.Itoa(index)).ScrollToHighlight()
+ }
+ })
+
+ commandArea.AddItem(commandInputField, 2, 0, 1, 1, 0, 0, true)
+ }
+ return commandArea
+}
+
+func buildOutputArea(newPrimitive func(text string) tview.Primitive, application *tview.Application) *tview.Grid {
+ outputAreaHeader := newPrimitive("Output")
+ outputArea := tview.NewGrid().
+ SetRows(3, 0, 10).
+ SetColumns(0, 30).
+ AddItem(outputAreaHeader, 0, 0, 1, 1, 0, 0, false)
+ {
+ var jumpToMessageItem func(messageNumber int) bool
+ {
+ outputView := tview.NewTextView().
+ SetDynamicColors(true). // TODO: currently this is broken due to https://github.com/rivo/tview/issues/751 (workaround active with sruehl fix fork)
+ SetRegions(true).
+ SetWordWrap(false).
+ SetWrap(false).
+ SetChangedFunc(func() {
+ application.Draw()
+ })
+ jumpToMessageItem = func(messageNumber int) bool {
+ regionId := strconv.Itoa(messageNumber)
+ if outputView.GetRegionText(regionId) == "" {
+ return false
+ }
+ outputView.Highlight(regionId).ScrollToHighlight()
+ return true
+ }
+ messageOutput = outputView
+ messageOutputClear = func() {
+ outputView.SetText("")
+ }
+
+ outputView.SetDoneFunc(func(key tcell.Key) {
+ currentSelection := outputView.GetHighlights()
+ if key == tcell.KeyEnter {
+ if len(currentSelection) > 0 {
+ outputView.Highlight()
+ } else {
+ outputView.Highlight("0").ScrollToHighlight()
+ }
+ } else if len(currentSelection) > 0 {
+ index, _ := strconv.Atoi(currentSelection[0])
+ if key == tcell.KeyTab {
+ index = (index + 1) % numberOfMessagesReceived
+ } else if key == tcell.KeyBacktab {
+ index = (index - 1 + numberOfMessagesReceived) % numberOfMessagesReceived
+ } else {
+ return
+ }
+ outputView.Highlight(strconv.Itoa(index)).ScrollToHighlight()
+ }
+ })
+ outputView.SetBorder(false)
+ outputArea.AddItem(outputView, 1, 0, 1, 1, 0, 0, false)
+ }
+
+ {
+ consoleView := tview.NewTextView().
+ SetDynamicColors(true).
+ SetMaxLines(config.MaxConsoleLines).
+ SetChangedFunc(func() {
+ application.Draw()
+ })
+ consoleOutput = consoleView
+ consoleOutputClear = func() {
+ consoleView.SetText("")
+ }
+
+ consoleView.SetBorder(false)
+ outputArea.AddItem(consoleView, 2, 0, 1, 1, 0, 0, false)
+ }
+
+ {
+ receivedMessagesList := tview.NewList()
+ messageReceived = func(messageNumber int, receiveTime time.Time, message model.PlcMessage) {
+ application.QueueUpdateDraw(func() {
+ receivedMessagesList.AddItem(fmt.Sprintf("No %d @%s (api)", messageNumber, receiveTime.Format("15:04:05.999999")), "", 0x0, func() {
+ if ok := jumpToMessageItem(messageNumber); !ok {
+ plc4xpcapanalyzerLog.Debug().Msgf("Adding new message to console output")
+ _, _ = fmt.Fprintf(messageOutput, "Message nr: %d\n[\"%d\"]%s[\"\"]\n", messageNumber, messageNumber, message)
+ jumpToMessageItem(messageNumber)
+ }
+ })
+ })
+ }
+ spiMessageReceived = func(messageNumber int, receiveTime time.Time, message spi.Message) {
+ application.QueueUpdateDraw(func() {
+ receivedMessagesList.AddItem(fmt.Sprintf("No %d @%s (spi)", messageNumber, receiveTime.Format("15:04:05.999999")), "", 0x0, func() {
+ if ok := jumpToMessageItem(messageNumber); !ok {
+ plc4xpcapanalyzerLog.Debug().Msgf("Adding new spi message to console output")
+ _, _ = fmt.Fprintf(messageOutput, "Message nr: %d\n[\"%d\"]%s[\"\"]\n", messageNumber, messageNumber, message)
+ jumpToMessageItem(messageNumber)
+ }
+ })
+ })
+ }
+ outputArea.AddItem(receivedMessagesList, 0, 1, 3, 1, 0, 0, false)
+ }
+ }
+ return outputArea
+}