plc4go/tools/plc4xbrowser/ui/commands.go (753 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 (
"fmt"
"net/url"
"runtime/debug"
"strings"
"time"
plc4xConfig "github.com/apache/plc4x/plc4go/pkg/api/config"
apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
"github.com/pkg/errors"
"github.com/rivo/tview"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
const rootCommandIndicator = "rootCommand"
var commands = map[inputMode]Command{
normalMode: rootCommand,
readEditMode: rootCommand,
writeEditMode: rootCommand,
subscribeEditMode: rootCommand,
}
var rootCommand = Command{
Name: rootCommandIndicator,
subCommands: []Command{
{
Name: "discover",
Description: "Discovers devices",
action: func(_ Command, driverId string) error {
if driver, ok := registeredDrivers[driverId]; ok {
if !driver.SupportsDiscovery() {
return errors.Errorf("%s doesn't support discovery", driverId)
}
return driver.Discover(func(event apiModel.PlcDiscoveryItem) {
_, _ = fmt.Fprintf(messageOutput, "%v\n", event)
})
} else {
return errors.Errorf("%s not registered", driverId)
}
},
parameterSuggestions: func(currentText string) (entries []string) {
for _, protocol := range protocolList {
entries = append(entries, protocol)
}
return
},
},
{
Name: "connect",
Description: "Connects to a device",
action: func(_ Command, connectionString string) error {
log.Info().Str("connectionString", connectionString).Msg("connect connectionString")
connectionUrl, err := url.Parse(connectionString)
if err != nil {
return errors.Wrapf(err, "can't parse connection url %s", connectionString)
}
addHostHistoryEntry(connectionUrl.Host)
connectionId := fmt.Sprintf("%s://%s", connectionUrl.Scheme, connectionUrl.Host)
if _, ok := connections[connectionId]; ok {
return errors.Errorf("%s already connected", connectionId)
}
connectionResult := <-driverManager.GetConnection(connectionString)
if err := connectionResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s can't connect to", connectionUrl.Host)
}
log.Info().Str("connectionId", connectionId).Msg("connected")
connections[connectionId] = connectionResult.GetConnection()
connectionsChanged()
return nil
},
parameterSuggestions: func(currentText string) (entries []string) {
for _, protocol := range protocolList {
if strings.HasPrefix(currentText, protocol) {
for _, host := range config.History.Last10Hosts {
entries = append(entries, protocol+"://"+host)
}
entries = append(entries, currentText)
} else {
entries = append(entries, protocol)
}
}
return
},
},
{
Name: "disconnect",
Description: "Disconnect a connection",
action: func(_ Command, connectionString string) error {
if connection, ok := connections[connectionString]; !ok {
return errors.Errorf("%s not connected", connectionString)
} else {
closeResult := <-connection.Close()
log.Info().Str("connectionString", connectionString).Msg("connectionString disconnected")
delete(connections, connectionString)
connectionsChanged()
if err := closeResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s can't close", connectionString)
}
}
return nil
},
parameterSuggestions: func(currentText string) (entries []string) {
for connectionsString := range connections {
entries = append(entries, connectionsString)
}
return
},
},
{
Name: "read",
Description: "Starts a read request (switched mode to read edit)",
action: func(_ Command, connectionsString string) error {
if connection, ok := connections[connectionsString]; !ok {
return errors.Errorf("%s not connected", connectionsString)
} else {
return errors.Errorf("%s mode switch not yet implemented", connection)
}
},
parameterSuggestions: func(currentText string) (entries []string) {
for connectionsString := range connections {
entries = append(entries, connectionsString)
}
return
},
},
{
Name: "read-direct",
Description: "Builds a read request with the supplied field",
action: func(c Command, connectionsStringAndFieldQuery string) error {
split := strings.Split(connectionsStringAndFieldQuery, " ")
if len(split) != 2 {
return errors.Errorf("%s expects exactly two arguments [connection url] [fieldQuery]", c)
}
connectionsString := split[0]
if connection, ok := connections[connectionsString]; !ok {
return errors.Errorf("%s not connected", connectionsString)
} else {
start := time.Now()
readRequest, err := connection.ReadRequestBuilder().
AddTagAddress("readField", split[1]).
Build()
if err != nil {
return errors.Wrapf(err, "%s can't read", connectionsString)
}
readRequestResult := <-readRequest.Execute()
if err := readRequestResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s can't read", connectionsString)
}
plc4xBrowserLog.Debug().TimeDiff("runtime", time.Now(), start).Msg("read took runtime")
if err := readRequestResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s error reading", connectionsString)
}
numberOfMessagesReceived++
messageReceived(numberOfMessagesReceived, time.Now(), readRequestResult.GetResponse())
}
return nil
},
parameterSuggestions: func(currentText string) (entries []string) {
for connectionsString := range connections {
if strings.HasPrefix(currentText, connectionsString+"") {
parse, _ := url.Parse(connectionsString)
switch parse.Scheme {
// TODO: add to protocol suggestor so it can be reused.
}
} else {
entries = append(entries, connectionsString)
}
}
return
},
},
{
Name: "write",
Description: "Starts a write request (switched mode to write edit)",
action: func(_ Command, connectionsString string) error {
if connection, ok := connections[connectionsString]; !ok {
return errors.Errorf("%s not connected", connectionsString)
} else {
return errors.Errorf("%s mode switch not yet implemented", connection)
}
},
parameterSuggestions: func(currentText string) (entries []string) {
for connectionsString := range connections {
entries = append(entries, connectionsString)
}
return
},
},
{
Name: "write-direct",
Description: "Builds a write request with the supplied field",
action: func(c Command, connectionsStringAndFieldQuery string) error {
split := strings.Split(connectionsStringAndFieldQuery, " ")
if len(split) != 3 {
return errors.Errorf("%s expects exactly three arguments [connection url] [fieldQuery] [value]", c)
}
connectionsString := split[0]
if connection, ok := connections[connectionsString]; !ok {
return errors.Errorf("%s not connected", connectionsString)
} else {
start := time.Now()
writeRequest, err := connection.WriteRequestBuilder().
AddTagAddress("writeField", split[1], split[2]).
Build()
if err != nil {
return errors.Wrapf(err, "%s can't write", connectionsString)
}
writeRequestResult := <-writeRequest.Execute()
if err := writeRequestResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s can't write", connectionsString)
}
plc4xBrowserLog.Debug().TimeDiff("runtime", time.Now(), start).Msg("write took runtime")
if err := writeRequestResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s error writing", connectionsString)
}
numberOfMessagesReceived++
messageReceived(numberOfMessagesReceived, time.Now(), writeRequestResult.GetResponse())
}
return nil
},
parameterSuggestions: func(currentText string) (entries []string) {
for connectionsString := range connections {
if strings.HasPrefix(currentText, connectionsString+"") {
parse, _ := url.Parse(connectionsString)
switch parse.Scheme {
// TODO: add to protocol suggestor so it can be reused.
}
} else {
entries = append(entries, connectionsString)
}
}
return
},
},
{
Name: "browse",
Description: "Starts a browse request (switched mode to browse edit)",
action: func(_ Command, connectionsString string) error {
if connection, ok := connections[connectionsString]; !ok {
return errors.Errorf("%s not connected", connectionsString)
} else {
return errors.Errorf("%s mode switch not yet implemented", connection)
}
},
parameterSuggestions: func(currentText string) (entries []string) {
for connectionsString := range connections {
entries = append(entries, connectionsString)
}
return
},
},
{
Name: "browse-direct",
Description: "Builds a browse request with the supplied field",
action: func(c Command, connectionsStringAndFieldQuery string) error {
split := strings.Split(connectionsStringAndFieldQuery, " ")
if len(split) != 2 {
return errors.Errorf("%s expects exactly three arguments [connection url] [fieldQuery]", c)
}
connectionsString := split[0]
if connection, ok := connections[connectionsString]; !ok {
return errors.Errorf("%s not connected", connectionsString)
} else {
start := time.Now()
browseRequest, err := connection.BrowseRequestBuilder().
AddQuery("browseField", split[1]).
Build()
if err != nil {
return errors.Wrapf(err, "%s can't browse", connectionsString)
}
browseRequestResult := <-browseRequest.ExecuteWithInterceptor(func(result apiModel.PlcBrowseItem) bool {
// TODO: Disabled for now ... not quite sure what this is for ...
//numberOfMessagesReceived++
//messageReceived(numberOfMessagesReceived, time.Now(), result)
return true
})
if err := browseRequestResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s can't browse", connectionsString)
}
plc4xBrowserLog.Debug().TimeDiff("runtime", time.Now(), start).Msg("write took runtime")
if err := browseRequestResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s error browse", connectionsString)
}
numberOfMessagesReceived++
messageReceived(numberOfMessagesReceived, time.Now(), browseRequestResult.GetResponse())
}
return nil
},
parameterSuggestions: func(currentText string) (entries []string) {
for connectionsString := range connections {
if strings.HasPrefix(currentText, connectionsString+"") {
parse, _ := url.Parse(connectionsString)
switch parse.Scheme {
// TODO: add to protocol suggestor so it can be reused.
case "c-bus":
entries = append(entries, connectionsString+" info/*/*")
}
} else {
entries = append(entries, connectionsString)
}
}
return
},
},
{
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: "subscribe",
Description: "Starts a subscription request (switched mode to subscribe edit)",
action: func(_ Command, connectionsString string) error {
if connection, ok := connections[connectionsString]; !ok {
return errors.Errorf("%s not connected", connectionsString)
} else {
return errors.Errorf("%s mode switch not yet implemented", connection)
}
},
parameterSuggestions: func(currentText string) (entries []string) {
for connectionsString := range connections {
entries = append(entries, connectionsString)
}
return
},
},
{
Name: "subscribe-direct",
Description: "Builds a subscriptions request with the supplied field",
action: func(c Command, connectionsStringAndFieldQuery string) error {
split := strings.Split(connectionsStringAndFieldQuery, " ")
if len(split) != 2 {
return errors.Errorf("%s expects exactly two arguments [connection url] [fieldQuery]", c)
}
connectionsString := split[0]
if connection, ok := connections[connectionsString]; !ok {
return errors.Errorf("%s not connected", connectionsString)
} else {
subscriptionRequest, err := connection.SubscriptionRequestBuilder().
AddEventTagAddress("subscriptionField", split[1]).
AddPreRegisteredConsumer("subscriptionField", func(event apiModel.PlcSubscriptionEvent) {
numberOfMessagesReceived++
messageReceived(numberOfMessagesReceived, time.Now(), event)
}).
Build()
if err != nil {
return errors.Wrapf(err, "%s can't subscribe", connectionsString)
}
subscriptionRequestResult := <-subscriptionRequest.Execute()
if err := subscriptionRequestResult.GetErr(); err != nil {
return errors.Wrapf(err, "%s can't subscribe", connectionsString)
}
log.Info().Stringer("response", subscriptionRequestResult.GetResponse()).Msg("subscription result")
}
return nil
},
parameterSuggestions: func(currentText string) (entries []string) {
for connectionsString := range connections {
entries = append(entries, connectionsString)
if strings.HasPrefix(currentText, connectionsString) {
parse, _ := url.Parse(connectionsString)
switch parse.Scheme {
// TODO: add to protocol suggestor so it can be reused.
case "c-bus":
entries = append(entries, connectionsString+" salmonitor/*/*")
entries = append(entries, connectionsString+" mmimonitor/*/*")
}
}
}
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 {
plc4xConfig.TraceTransactionManagerWorkers = true
return nil
},
},
{
Name: "off",
Description: "trace off",
action: func(_ 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(_ Command, _ string) error {
plc4xConfig.TraceTransactionManagerTransactions = true
return nil
},
},
{
Name: "off",
Description: "trace off",
action: func(_ 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(_ Command, _ string) error {
plc4xConfig.TraceDefaultMessageCodecWorker = true
return nil
},
},
{
Name: "off",
Description: "trace off",
action: func(_ Command, _ string) error {
plc4xConfig.TraceDefaultMessageCodecWorker = false
return nil
},
},
},
},
{
Name: "plc4xbrowser-debug",
Description: "Prints out debug information of the browser itself",
subCommands: []Command{
{
Name: "on",
Description: "debug on",
action: func(_ Command, _ string) error {
plc4xBrowserLog = zerolog.New(zerolog.ConsoleWriter{Out: tview.ANSIWriter(consoleOutput)})
return nil
},
},
{
Name: "off",
Description: "debug off",
action: func(_ Command, _ string) error {
plc4xBrowserLog = 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 {
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
plc4xBrowserLog.Debug().
Stringer("c", c).
Bool("accepts", accepts).
Msg("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 {
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) (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)
}
}()
plc4xBrowserLog.Debug().
Stringer("c", c).
Str("commandText", commandText).
Msg("%s executes %s")
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) {
plc4xBrowserLog.Debug().
Stringer("c", c).
Stringer("command", command).
Msg("c delegates to sub 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
}
plc4xBrowserLog.Debug().
Stringer("c", c).
Str("commandText", commandText).
Msg("c executes commandText directly")
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
}